mirror of https://github.com/vector-im/riot-web
Merge remote-tracking branch 'upstream/develop' into task/colors-4
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>pull/21833/head
commit
71eeb836d9
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -1,3 +1,70 @@
|
|||
Changes in [3.32.1](https://github.com/vector-im/element-desktop/releases/tag/v3.32.1) (2021-10-12)
|
||||
===================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Upgrade to matrix-js-sdk#14.0.1
|
||||
|
||||
Changes in [3.32.0](https://github.com/vector-im/element-desktop/releases/tag/v3.32.0) (2021-10-11)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Decrease profile button touch target ([\#6900](https://github.com/matrix-org/matrix-react-sdk/pull/6900)). Contributed by [ColonisationCaptain](https://github.com/ColonisationCaptain).
|
||||
* Don't let click events propagate out of context menus ([\#6892](https://github.com/matrix-org/matrix-react-sdk/pull/6892)).
|
||||
* Allow closing Dropdown via its chevron ([\#6885](https://github.com/matrix-org/matrix-react-sdk/pull/6885)). Fixes vector-im/element-web#19030 and vector-im/element-web#19030.
|
||||
* Improve AUX panel behaviour ([\#6699](https://github.com/matrix-org/matrix-react-sdk/pull/6699)). Fixes vector-im/element-web#18787 and vector-im/element-web#18787. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* A nicer opening animation for the Image View ([\#6454](https://github.com/matrix-org/matrix-react-sdk/pull/6454)). Fixes vector-im/element-web#18186 and vector-im/element-web#18186. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix space hierarchy pagination ([\#6910](https://github.com/matrix-org/matrix-react-sdk/pull/6910)).
|
||||
* Fix leaving space via other client leaving you in undefined-land ([\#6891](https://github.com/matrix-org/matrix-react-sdk/pull/6891)). Fixes vector-im/element-web#18455 and vector-im/element-web#18455.
|
||||
* Handle newer voice message encrypted event format for chat export ([\#6893](https://github.com/matrix-org/matrix-react-sdk/pull/6893)). Contributed by [jaiwanth-v](https://github.com/jaiwanth-v).
|
||||
* Fix pagination when filtering space hierarchy ([\#6876](https://github.com/matrix-org/matrix-react-sdk/pull/6876)). Fixes vector-im/element-web#19235 and vector-im/element-web#19235.
|
||||
* Fix spaces null-guard breaking the dispatcher settings watching ([\#6886](https://github.com/matrix-org/matrix-react-sdk/pull/6886)). Fixes vector-im/element-web#19223 and vector-im/element-web#19223.
|
||||
* Fix space children without specific `order` being sorted after those with one ([\#6878](https://github.com/matrix-org/matrix-react-sdk/pull/6878)). Fixes vector-im/element-web#19192 and vector-im/element-web#19192.
|
||||
* Ensure that sub-spaces aren't considered for notification badges ([\#6881](https://github.com/matrix-org/matrix-react-sdk/pull/6881)). Fixes vector-im/element-web#18975 and vector-im/element-web#18975.
|
||||
* Fix timeline autoscroll with non-standard DPI settings. ([\#6880](https://github.com/matrix-org/matrix-react-sdk/pull/6880)). Fixes vector-im/element-web#18984 and vector-im/element-web#18984.
|
||||
* Pluck out JoinRuleSettings styles so they apply in space settings too ([\#6879](https://github.com/matrix-org/matrix-react-sdk/pull/6879)). Fixes vector-im/element-web#19164 and vector-im/element-web#19164.
|
||||
* Null guard around the matrixClient in SpaceStore ([\#6874](https://github.com/matrix-org/matrix-react-sdk/pull/6874)).
|
||||
* Fix issue (https ([\#6871](https://github.com/matrix-org/matrix-react-sdk/pull/6871)). Fixes vector-im/element-web#19138 and vector-im/element-web#19138. Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Fix pills being cut off in message bubble layout ([\#6865](https://github.com/matrix-org/matrix-react-sdk/pull/6865)). Fixes vector-im/element-web#18627 and vector-im/element-web#18627. Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix space admin check false positive on multiple admins ([\#6824](https://github.com/matrix-org/matrix-react-sdk/pull/6824)).
|
||||
* Fix the User View ([\#6860](https://github.com/matrix-org/matrix-react-sdk/pull/6860)). Fixes vector-im/element-web#19158 and vector-im/element-web#19158.
|
||||
* Fix spacing for message composer buttons ([\#6852](https://github.com/matrix-org/matrix-react-sdk/pull/6852)). Fixes vector-im/element-web#18999 and vector-im/element-web#18999.
|
||||
* Always show root event of a thread in room's timeline ([\#6842](https://github.com/matrix-org/matrix-react-sdk/pull/6842)). Fixes vector-im/element-web#19016 and vector-im/element-web#19016.
|
||||
|
||||
Changes in [3.32.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v3.32.0-rc.2) (2021-10-08)
|
||||
=============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix space hierarchy pagination ([\#6910](https://github.com/matrix-org/matrix-react-sdk/pull/6910)).
|
||||
|
||||
Changes in [3.32.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v3.32.0-rc.1) (2021-10-04)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Decrease profile button touch target ([\#6900](https://github.com/matrix-org/matrix-react-sdk/pull/6900)). Contributed by [ColonisationCaptain](https://github.com/ColonisationCaptain).
|
||||
* Don't let click events propagate out of context menus ([\#6892](https://github.com/matrix-org/matrix-react-sdk/pull/6892)).
|
||||
* Allow closing Dropdown via its chevron ([\#6885](https://github.com/matrix-org/matrix-react-sdk/pull/6885)). Fixes vector-im/element-web#19030 and vector-im/element-web#19030.
|
||||
* Improve AUX panel behaviour ([\#6699](https://github.com/matrix-org/matrix-react-sdk/pull/6699)). Fixes vector-im/element-web#18787 and vector-im/element-web#18787. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* A nicer opening animation for the Image View ([\#6454](https://github.com/matrix-org/matrix-react-sdk/pull/6454)). Fixes vector-im/element-web#18186 and vector-im/element-web#18186. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix leaving space via other client leaving you in undefined-land ([\#6891](https://github.com/matrix-org/matrix-react-sdk/pull/6891)). Fixes vector-im/element-web#18455 and vector-im/element-web#18455.
|
||||
* Handle newer voice message encrypted event format for chat export ([\#6893](https://github.com/matrix-org/matrix-react-sdk/pull/6893)). Contributed by [jaiwanth-v](https://github.com/jaiwanth-v).
|
||||
* Fix pagination when filtering space hierarchy ([\#6876](https://github.com/matrix-org/matrix-react-sdk/pull/6876)). Fixes vector-im/element-web#19235 and vector-im/element-web#19235.
|
||||
* Fix spaces null-guard breaking the dispatcher settings watching ([\#6886](https://github.com/matrix-org/matrix-react-sdk/pull/6886)). Fixes vector-im/element-web#19223 and vector-im/element-web#19223.
|
||||
* Fix space children without specific `order` being sorted after those with one ([\#6878](https://github.com/matrix-org/matrix-react-sdk/pull/6878)). Fixes vector-im/element-web#19192 and vector-im/element-web#19192.
|
||||
* Ensure that sub-spaces aren't considered for notification badges ([\#6881](https://github.com/matrix-org/matrix-react-sdk/pull/6881)). Fixes vector-im/element-web#18975 and vector-im/element-web#18975.
|
||||
* Fix timeline autoscroll with non-standard DPI settings. ([\#6880](https://github.com/matrix-org/matrix-react-sdk/pull/6880)). Fixes vector-im/element-web#18984 and vector-im/element-web#18984.
|
||||
* Pluck out JoinRuleSettings styles so they apply in space settings too ([\#6879](https://github.com/matrix-org/matrix-react-sdk/pull/6879)). Fixes vector-im/element-web#19164 and vector-im/element-web#19164.
|
||||
* Null guard around the matrixClient in SpaceStore ([\#6874](https://github.com/matrix-org/matrix-react-sdk/pull/6874)).
|
||||
* Fix issue (https ([\#6871](https://github.com/matrix-org/matrix-react-sdk/pull/6871)). Fixes vector-im/element-web#19138 and vector-im/element-web#19138. Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Fix pills being cut off in message bubble layout ([\#6865](https://github.com/matrix-org/matrix-react-sdk/pull/6865)). Fixes vector-im/element-web#18627 and vector-im/element-web#18627. Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix space admin check false positive on multiple admins ([\#6824](https://github.com/matrix-org/matrix-react-sdk/pull/6824)).
|
||||
* Fix the User View ([\#6860](https://github.com/matrix-org/matrix-react-sdk/pull/6860)). Fixes vector-im/element-web#19158 and vector-im/element-web#19158.
|
||||
* Fix spacing for message composer buttons ([\#6852](https://github.com/matrix-org/matrix-react-sdk/pull/6852)). Fixes vector-im/element-web#18999 and vector-im/element-web#18999.
|
||||
|
||||
Changes in [3.31.0](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0) (2021-09-27)
|
||||
===================================================================================================
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.31.0",
|
||||
"version": "3.32.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -134,6 +134,7 @@
|
|||
"@types/counterpart": "^0.18.1",
|
||||
"@types/css-font-loading-module": "^0.0.6",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/enzyme": "^3.10.9",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/jest": "^26.0.20",
|
||||
|
@ -158,6 +159,7 @@
|
|||
"chokidar": "^3.5.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||
|
@ -186,6 +188,7 @@
|
|||
"@types/react": "17.0.14"
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": ["enzyme-to-json/serializer"],
|
||||
"testEnvironment": "./__test-utils__/environment.js",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
@import "./structures/_ViewSource.scss";
|
||||
@import "./structures/auth/_CompleteSecurity.scss";
|
||||
@import "./structures/auth/_Login.scss";
|
||||
@import "./structures/auth/_SetupEncryptionBody.scss";
|
||||
@import "./views/audio_messages/_AudioPlayer.scss";
|
||||
@import "./views/audio_messages/_PlayPauseButton.scss";
|
||||
@import "./views/audio_messages/_PlaybackContainer.scss";
|
||||
|
@ -73,6 +74,7 @@
|
|||
@import "./views/dialogs/_ChangelogDialog.scss";
|
||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
||||
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
|
||||
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.scss";
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
|
@ -201,6 +203,7 @@
|
|||
@import "./views/right_panel/_UserInfo.scss";
|
||||
@import "./views/right_panel/_VerificationPanel.scss";
|
||||
@import "./views/right_panel/_WidgetCard.scss";
|
||||
@import "./views/right_panel/_ThreadPanel.scss";
|
||||
@import "./views/room_settings/_AliasSettings.scss";
|
||||
@import "./views/rooms/_AppsDrawer.scss";
|
||||
@import "./views/rooms/_Autocomplete.scss";
|
||||
|
@ -269,6 +272,7 @@
|
|||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||
@import "./views/spaces/_SpaceBasicSettings.scss";
|
||||
@import "./views/spaces/_SpaceChildrenPicker.scss";
|
||||
@import "./views/spaces/_SpaceCreateMenu.scss";
|
||||
@import "./views/spaces/_SpacePublicShare.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
|
|
|
@ -34,4 +34,5 @@ limitations under the License.
|
|||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -79,6 +79,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_threadsButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/thread.svg');
|
||||
}
|
||||
|
||||
.mx_RightPanel_notifsButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/notifications.svg');
|
||||
mask-position: center;
|
||||
|
|
|
@ -422,7 +422,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
.mx_SpaceRoomView_inviteTeammates {
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||
padding: 58px 16px 16px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
@ -465,8 +465,13 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton {
|
||||
color: $accent-color;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,19 @@ limitations under the License.
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_skip {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_body {
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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_SetupEncryptionBody_reset {
|
||||
color: $light-fg-color;
|
||||
margin-top: $font-14px;
|
||||
|
||||
a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
|
@ -58,10 +58,6 @@ limitations under the License.
|
|||
background-color: $authpage-body-bg-color;
|
||||
}
|
||||
|
||||
.mx_Field label {
|
||||
color: $authpage-primary-color;
|
||||
}
|
||||
|
||||
.mx_Field_labelAlwaysTopLeft label,
|
||||
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
|
||||
.mx_Field input:focus + label,
|
||||
|
|
|
@ -75,7 +75,7 @@ limitations under the License.
|
|||
@mixin ProgressBarBorderRadius 8px;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpace_progressText {
|
||||
.mx_AddExistingToSpaceDialog_progressText {
|
||||
margin-top: 8px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
.mx_ConfirmSpaceUserActionDialog_wrapper {
|
||||
.mx_Dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ConfirmSpaceUserActionDialog {
|
||||
width: 440px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
height: 520px;
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin: 12px 0;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_ConfirmUserActionDialog_reasonField {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mx_ConfirmSpaceUserActionDialog_warning {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 12px 8px 12px 42px;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: calc(50% - 8px); // vertical centering
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: $secondary-content;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ConfirmUserActionDialog .mx_Dialog_content {
|
||||
.mx_ConfirmUserActionDialog .mx_Dialog_content .mx_ConfirmUserActionDialog_user {
|
||||
min-height: 48px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ limitations under the License.
|
|||
.mx_ConfirmUserActionDialog_avatar {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.mx_ConfirmUserActionDialog_name {
|
||||
padding-top: 2px;
|
||||
font-size: $font-18px;
|
||||
}
|
||||
|
||||
|
@ -37,16 +37,4 @@ limitations under the License.
|
|||
font-size: $font-14px;
|
||||
color: $primary-content;
|
||||
background-color: $background;
|
||||
|
||||
border-radius: 3px;
|
||||
border: solid 1px $input-border-color;
|
||||
line-height: $font-36px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
margin-bottom: 24px;
|
||||
|
||||
width: 90%;
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ limitations under the License.
|
|||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
margin-top: -13px; // match height of buttons to prevent height changing
|
||||
|
||||
.mx_ProgressBar {
|
||||
height: 8px;
|
||||
|
|
|
@ -27,33 +27,13 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
max-height: 520px;
|
||||
height: 520px;
|
||||
|
||||
.mx_Dialog_content {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.mx_RadioButton + .mx_RadioButton {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mx_SearchBox {
|
||||
// To match the space around the title
|
||||
margin: 0 0 15px 0;
|
||||
flex-grow: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_LeaveSpaceDialog_noResults {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mx_LeaveSpaceDialog_section {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.mx_LeaveSpaceDialog_section_warning {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
|
|
|
@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +17,22 @@ limitations under the License.
|
|||
.mx_RoomUpgradeWarningDialog {
|
||||
max-width: 38vw;
|
||||
width: 38vw;
|
||||
|
||||
.mx_RoomUpgradeWarningDialog_progress {
|
||||
.mx_ProgressBar {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
|
||||
@mixin ProgressBarBorderRadius 8px;
|
||||
}
|
||||
|
||||
.mx_RoomUpgradeWarningDialog_progressText {
|
||||
margin-top: 8px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomUpgradeWarningDialog .mx_SettingsFlag {
|
||||
|
|
|
@ -100,7 +100,6 @@ limitations under the License.
|
|||
color 0.25s ease-out 0.1s,
|
||||
transform 0.25s ease-out 0.1s,
|
||||
background-color 0.25s ease-out 0.1s;
|
||||
color: $primary-content;
|
||||
background-color: transparent;
|
||||
font-size: $font-14px;
|
||||
transform: translateY(0);
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
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_ThreadPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mx_BaseCard_header {
|
||||
padding: 6px 0;
|
||||
|
||||
.mx_BaseCard_close {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton.mx_BaseCard_back {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: calc(100% - 40px);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_wrapper {
|
||||
// It's added here due to some weird error if I pass it directly in the style, even though it's a numeric value, so it's being passed 0 instead.
|
||||
// The error: react_devtools_backend.js:2526 Warning: `NaN` is an invalid value for the `top` css style property.
|
||||
top: 43px;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu {
|
||||
position: initial;
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_Header_FilterOptionItem {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding-left: 30px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: $event-selected-color;
|
||||
}
|
||||
&[aria-selected="true"] {
|
||||
&::before {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
mask-image: url("$(res)/img/feather-customised/check.svg");
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 10px;
|
||||
background-color: $primary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_messageListWrapper {
|
||||
background-color: $background;
|
||||
border-radius: 8px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.mx_ScrollPanel {
|
||||
.mx_RoomView_MessageList {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile, .mx_EventListSummary {
|
||||
// Account for scrollbar when hovering
|
||||
width: calc(100% - 3px);
|
||||
margin: 0 2px;
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
// We need to add !important here due to some enormous selectors overriding it anyways
|
||||
// See: _EventTile.scss:241
|
||||
left: unset !important;
|
||||
right: 0 !important;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.mx_EventTile_line.mx_EventTile_line {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.mx_ThreadInfo {
|
||||
position: relative;
|
||||
padding-right: 11px;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -16px;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $message-action-bar-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DateSeparator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -252,6 +252,10 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_poll::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/poll.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_voiceMessage::before {
|
||||
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
|
||||
}
|
||||
|
|
|
@ -22,6 +22,12 @@ limitations under the License.
|
|||
display: none;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist_minimized) {
|
||||
.mx_RoomSublist_headerContainer {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist_headerContainer {
|
||||
// Create a flexbox to make alignment easy
|
||||
display: flex;
|
||||
|
@ -41,9 +47,7 @@ limitations under the License.
|
|||
// The combined height must be set in the LeftPanel component for sticky headers
|
||||
// to work correctly.
|
||||
padding-bottom: 8px;
|
||||
// Allow the container to collapse on itself if its children
|
||||
// are not in the normal document flow
|
||||
max-height: 24px;
|
||||
height: 24px;
|
||||
color: $roomlist-header-color;
|
||||
|
||||
.mx_RoomSublist_stickable {
|
||||
|
@ -172,14 +176,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// In the general case, we reserve space for each sublist header to prevent
|
||||
// scroll jumps when they become sticky. However, that leaves a gap when
|
||||
// scrolled to the top above the first sublist (whose header can only ever
|
||||
// stick to top), so we make sure to exclude the first visible sublist.
|
||||
&:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.mx_RoomSublist_resizeBox {
|
||||
position: relative;
|
||||
|
||||
|
@ -395,7 +391,7 @@ limitations under the License.
|
|||
.mx_RoomSublist_skeletonUI {
|
||||
position: relative;
|
||||
margin-left: 4px;
|
||||
height: 288px;
|
||||
height: 240px;
|
||||
|
||||
&::before {
|
||||
background: $roomsublist-skeleton-ui-bg;
|
||||
|
@ -410,3 +406,8 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/roomlist/skeleton-ui.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist_minimized .mx_RoomSublist_skeletonUI {
|
||||
width: 32px; // cut off the horizontal lines in the svg
|
||||
margin-left: 10px; // align with sublist + buttons
|
||||
}
|
||||
|
|
|
@ -15,23 +15,26 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_DevicesPanel {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 880px;
|
||||
// Normally the panel is 880px, however this can easily overflow the container.
|
||||
// TODO: Fix the table to not be squishy
|
||||
width: auto;
|
||||
max-width: 880px;
|
||||
border-spacing: 10px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header {
|
||||
display: table-header-group;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header > .mx_DevicesPanel_deviceButtons {
|
||||
.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons {
|
||||
height: 48px; // make this tall so the table doesn't move down when the delete button appears
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header > div {
|
||||
display: table-cell;
|
||||
.mx_DevicesPanel_header th {
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -43,16 +46,9 @@ limitations under the License.
|
|||
width: 30%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_device {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_device > div {
|
||||
display: table-cell;
|
||||
.mx_DevicesPanel_device td {
|
||||
vertical-align: baseline;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_myDevice {
|
||||
|
|
|
@ -67,5 +67,7 @@ limitations under the License.
|
|||
|
||||
> .mx_AccessibleButton_kind_link {
|
||||
padding-left: 0; // to align with left side
|
||||
padding-right: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SecurityUserSettingsTab .mx_DevicesPanel {
|
||||
// Normally the panel is 880px, however this can easily overflow the container.
|
||||
// TODO: Fix the table to not be squishy
|
||||
width: auto;
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_deviceInfo {
|
||||
display: table;
|
||||
padding-left: 0;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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_SpaceChildrenPicker {
|
||||
margin: 16px 0;
|
||||
|
||||
.mx_RadioButton + .mx_RadioButton {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mx_SearchBox {
|
||||
// To match the space around the title
|
||||
margin: 0 0 15px 0;
|
||||
flex-grow: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_SpaceChildrenPicker_noResults {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9.5C3 9.22386 3.22386 9 3.5 9H6.5C6.77614 9 7 9.22386 7 9.5V22H3V9.5Z" fill="#C1C6CD"/>
|
||||
<path d="M17 13.5C17 13.2239 17.2239 13 17.5 13H20.5C20.7761 13 21 13.2239 21 13.5V22H17V13.5Z" fill="#C1C6CD"/>
|
||||
<path d="M10 2.5C10 2.22386 10.2239 2 10.5 2H13.5C13.7761 2 14 2.22386 14 2.5V22H10V2.5Z" fill="#C1C6CD"/>
|
||||
</svg>
|
After Width: | Height: | Size: 423 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="#17191C" fill-rule="evenodd" d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H7.667a1 1 0 0 0-.6.2L3.6 22.8A1 1 0 0 1 2 22V5Zm3 4a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm1 3a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H6Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 357 B |
|
@ -12,9 +12,6 @@ $font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial
|
|||
|
||||
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||
|
||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||
$system: #F4F6FA;
|
||||
|
||||
// unified palette
|
||||
// try to use these colors when possible
|
||||
$accent-color: #03b381;
|
||||
|
@ -288,13 +285,24 @@ $progressbar-bg-color: rgba(141, 151, 165, 0.2);
|
|||
$room-warning-bg-color: $yellow-background;
|
||||
|
||||
// Legacy theme backports
|
||||
|
||||
// Legacy theme backports
|
||||
$accent: #0DBD8B;
|
||||
$alert: #FF5B55;
|
||||
$links: #0086e6;
|
||||
$primary-content: $primary-fg-color;
|
||||
$secondary-content: $secondary-fg-color;
|
||||
$tertiary-content: $tertiary-fg-color;
|
||||
$quaternary-content: #6F7882;
|
||||
$quinary-content: $quaternary-content;
|
||||
$system: #F4F6FA;
|
||||
$background: $primary-bg-color;
|
||||
$panels: rgba($system, 0.9);
|
||||
$panel-base: #8D97A5; // This color is not intended for use in the app
|
||||
$panel-selected: rgba($tertiary-content, 0.3);
|
||||
$panel-hover: rgba($tertiary-content, 0.1);
|
||||
$panel-actions: $roomtile-selected-bg-color;
|
||||
$space-nav: rgba($tertiary-content, 0.15);
|
||||
// Legacy theme backports
|
||||
|
||||
$memberstatus-placeholder-color: $muted-fg-color;
|
||||
|
|
|
@ -18,18 +18,18 @@ $font-family: var(--font-family, $font-family);
|
|||
$monospace-font-family: var(--font-family-monospace, $monospace-font-family);
|
||||
|
||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741
|
||||
$accent: var(--accent);
|
||||
$alert: var(--alert);
|
||||
$links: var(--links);
|
||||
$primary-content: var(--primary-content);
|
||||
$secondary-content: var(--secondary-content);
|
||||
$tertiary-content: var(--tertiary-content);
|
||||
$quaternary-content: var(--quaternary-content);
|
||||
$quinary-content: var(--quinary-content);
|
||||
$system: var(--system);
|
||||
$background: var(--background);
|
||||
$accent: var(--accent, $accent);
|
||||
$alert: var(--alert, $alert);
|
||||
$links: var(--links, $links);
|
||||
$primary-content: var(--primary-content, $primary-content);
|
||||
$secondary-content: var(--secondary-content, $secondary-content);
|
||||
$tertiary-content: var(--tertiary-content, $tertiary-content);
|
||||
$quaternary-content: var(--quaternary-content, $quaternary-content);
|
||||
$quinary-content: var(--quinary-content, $quinary-content);
|
||||
$system: var(--system, $system);
|
||||
$background: var(--background, $background);
|
||||
$panels: rgba($system, 0.9);
|
||||
$panel-base: var(--panel-base); // This color is not intended for use in the app
|
||||
$panel-base: var(--panel-base, $tertiary-content); // This color is not intended for use in the app
|
||||
$panel-selected: rgba($panel-base, 0.3);
|
||||
$panel-hover: rgba($panel-base, 0.1);
|
||||
$panel-actions: rgba($panel-base, 0.2);
|
||||
|
|
|
@ -21,7 +21,7 @@ import Modal from './Modal';
|
|||
import { _t } from './languageHandler';
|
||||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
|
||||
import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk";
|
||||
import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
function getIdServerDomain(): string {
|
||||
|
|
|
@ -142,15 +142,11 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||
// space rooms cannot be DMs so skip the rest
|
||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
|
||||
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
// If the room is not a DM don't fallback to a member avatar
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;
|
||||
|
||||
// If there are only two members in the DM use the avatar of the other member
|
||||
const otherMember = room.getAvatarFallbackMember();
|
||||
if (otherMember?.getMxcAvatarUrl()) {
|
||||
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
|
|
|
@ -42,10 +42,15 @@ export interface IInviteResult {
|
|||
*
|
||||
* @param {string} roomId The ID of the room to invite to
|
||||
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @param {function} progressCallback optional callback, fired after each invite.
|
||||
* @returns {Promise} Promise
|
||||
*/
|
||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||
const inviter = new MultiInviter(roomId);
|
||||
export function inviteMultipleToRoom(
|
||||
roomId: string,
|
||||
addresses: string[],
|
||||
progressCallback?: () => void,
|
||||
): Promise<IInviteResult> {
|
||||
const inviter = new MultiInviter(roomId, progressCallback);
|
||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||
}
|
||||
|
||||
|
@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||
export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise<void> {
|
||||
return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -452,7 +452,9 @@ function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string)
|
|||
});
|
||||
}
|
||||
|
||||
function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, level: number): void {
|
||||
async function setBotPower(
|
||||
event: MessageEvent<any>, roomId: string, userId: string, level: number, ignoreIfGreater?: boolean,
|
||||
): Promise<void> {
|
||||
if (!(Number.isInteger(level) && level >= 0)) {
|
||||
sendError(event, _t('Power level must be positive integer.'));
|
||||
return;
|
||||
|
@ -465,22 +467,34 @@ function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, l
|
|||
return;
|
||||
}
|
||||
|
||||
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
||||
const powerEvent = new MatrixEvent(
|
||||
try {
|
||||
const powerLevels = await client.getStateEvent(roomId, "m.room.power_levels", "");
|
||||
|
||||
// If the PL is equal to or greater than the requested PL, ignore.
|
||||
if (ignoreIfGreater === true) {
|
||||
// As per https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels
|
||||
const currentPl = (
|
||||
powerLevels.content.users && powerLevels.content.users[userId]
|
||||
) || powerLevels.content.users_default || 0;
|
||||
|
||||
if (currentPl >= level) {
|
||||
return sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
await client.setPowerLevel(roomId, userId, level, new MatrixEvent(
|
||||
{
|
||||
type: "m.room.power_levels",
|
||||
content: powerLevels,
|
||||
},
|
||||
);
|
||||
|
||||
client.setPowerLevel(roomId, userId, level, powerEvent).then(() => {
|
||||
sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
}, (err) => {
|
||||
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||
));
|
||||
return sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||
}
|
||||
}
|
||||
|
||||
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
|
@ -678,7 +692,7 @@ const onMessage = function(event: MessageEvent<any>): void {
|
|||
setBotOptions(event, roomId, userId);
|
||||
break;
|
||||
case Action.SetBotPower:
|
||||
setBotPower(event, roomId, userId, event.data.level);
|
||||
setBotPower(event, roomId, userId, event.data.level, event.data.ignoreIfGreater);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||
|
|
|
@ -44,7 +44,7 @@ import { Action } from "./dispatcher/actions";
|
|||
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { UIFeature } from "./settings/UIFeature";
|
||||
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||
import { CHAT_EFFECTS } from "./effects";
|
||||
import CallHandler from "./CallHandler";
|
||||
import { guessAndSetDMRoom } from "./Rooms";
|
||||
|
@ -56,6 +56,7 @@ import InfoDialog from "./components/views/dialogs/InfoDialog";
|
|||
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -403,6 +404,7 @@ export const Commands = [
|
|||
command: 'invite',
|
||||
args: '<user-id> [<reason>]',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
isEnabled: () => shouldShowComponent(UIComponent.InviteUsers),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const [address, reason] = args.split(/\s+(.+)/);
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
import { Key } from "../../Keyboard";
|
||||
import { Writeable } from "../../@types/common";
|
||||
|
@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
|
|||
return container;
|
||||
}
|
||||
|
||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||
|
||||
export interface IPosition {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
|
@ -84,6 +83,10 @@ export interface IProps extends IPosition {
|
|||
// it will be mounted to a container at the root of the DOM.
|
||||
mountAsChild?: boolean;
|
||||
|
||||
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
|
||||
// within an existing FocusLock e.g inside a modal.
|
||||
focusLock?: boolean;
|
||||
|
||||
// Function to be called on menu close
|
||||
onFinished();
|
||||
// on resize callback
|
||||
|
@ -99,7 +102,7 @@ interface IState {
|
|||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
@replaceableComponent("structures.ContextMenu")
|
||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
private initialFocus: HTMLElement;
|
||||
private readonly initialFocus: HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
hasBackground: true,
|
||||
|
@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
contextMenuElem: null,
|
||||
};
|
||||
|
@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
this.initialFocus.focus();
|
||||
}
|
||||
|
||||
private collectContextMenuRect = (element) => {
|
||||
private collectContextMenuRect = (element: HTMLDivElement) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
let first = element.querySelector('[role^="menuitem"]');
|
||||
if (!first) {
|
||||
first = element.querySelector('[tab-index]');
|
||||
}
|
||||
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
|
||||
|| element.querySelector<HTMLElement>('[tab-index]');
|
||||
|
||||
if (first) {
|
||||
first.focus();
|
||||
}
|
||||
|
@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
descending = true;
|
||||
}
|
||||
}
|
||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
|
@ -383,6 +386,17 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let body = <>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
</>;
|
||||
|
||||
if (props.focusLock) {
|
||||
body = <FocusLock>
|
||||
{ body }
|
||||
</FocusLock>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
|
@ -397,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
{ body }
|
||||
</div>
|
||||
{ background }
|
||||
</div>
|
||||
|
|
|
@ -37,6 +37,7 @@ import TimelinePanel from "./TimelinePanel";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -57,6 +58,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
// added to the timeline.
|
||||
private decryptingEvents = new Set<string>();
|
||||
public noRoom: boolean;
|
||||
static contextType = RoomContext;
|
||||
|
||||
state = {
|
||||
timelineSet: null,
|
||||
|
@ -249,38 +251,46 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
|
||||
if (this.state.timelineSet) {
|
||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview={false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape={TileShape.FileGrid}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
}}>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview={false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape={TileShape.FileGrid}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
}}>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ interface IState {
|
|||
stageState?: IStageStatus;
|
||||
busy: boolean;
|
||||
errorText?: string;
|
||||
stageErrorText?: string;
|
||||
errorCode?: string;
|
||||
submitButtonEnabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
errorCode: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
|
||||
|
@ -145,6 +145,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg,
|
||||
errorCode: error.errcode,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -186,6 +187,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
authStage: stageType,
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
errorCode: stageState.errcode,
|
||||
}, () => {
|
||||
if (oldStage !== stageType) {
|
||||
this.setFocus();
|
||||
|
@ -208,7 +210,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
errorCode: null,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
|
@ -235,7 +237,15 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
|
||||
};
|
||||
|
||||
private renderCurrentStage(): JSX.Element {
|
||||
private onAuthStageFailed = (e: Error): void => {
|
||||
this.props.onAuthFinished(false, e);
|
||||
};
|
||||
|
||||
private setEmailSid = (sid: string): void => {
|
||||
this.authLogic.setEmailSid(sid);
|
||||
};
|
||||
|
||||
render() {
|
||||
const stage = this.state.authStage;
|
||||
if (!stage) {
|
||||
if (this.state.busy) {
|
||||
|
@ -255,7 +265,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
clientSecret={this.authLogic.getClientSecret()}
|
||||
stageParams={this.authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this.submitAuthDict}
|
||||
errorText={this.state.stageErrorText}
|
||||
errorText={this.state.errorText}
|
||||
errorCode={this.state.errorCode}
|
||||
busy={this.state.busy}
|
||||
inputs={this.props.inputs}
|
||||
stageState={this.state.stageState}
|
||||
|
@ -269,32 +280,4 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private onAuthStageFailed = (e: Error): void => {
|
||||
this.props.onAuthFinished(false, e);
|
||||
};
|
||||
|
||||
private setEmailSid = (sid: string): void => {
|
||||
this.authLogic.setEmailSid(sid);
|
||||
};
|
||||
|
||||
render() {
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{ this.renderCurrentStage() }
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ import { PosthogAnalytics } from '../../PosthogAnalytics';
|
|||
import { initSentry } from "../../sentry";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { showSpaceInvite } from "../../utils/space";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -741,9 +742,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'view_create_chat':
|
||||
showStartChatInviteDialog(payload.initialText || "");
|
||||
break;
|
||||
case 'view_invite':
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
case 'view_invite': {
|
||||
const room = MatrixClientPeg.get().getRoom(payload.roomId);
|
||||
if (room?.isSpaceRoom()) {
|
||||
showSpaceInvite(room);
|
||||
} else {
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'view_last_screen':
|
||||
// This function does what we want, despite the name. The idea is that it shows
|
||||
// the last room we were looking at or some reasonable default/guess. We don't
|
||||
|
|
|
@ -26,7 +26,7 @@ import shouldHideEvent from '../../shouldHideEvent';
|
|||
import { wantsDateSeparator } from '../../DateUtils';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { _t } from "../../languageHandler";
|
||||
import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
|
||||
|
@ -66,7 +66,9 @@ export function shouldFormContinuation(
|
|||
prevEvent: MatrixEvent,
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
timelineRenderingType?: TimelineRenderingType,
|
||||
): boolean {
|
||||
if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false;
|
||||
// sanity check inputs
|
||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||
// check if within the max continuation period
|
||||
|
@ -269,9 +271,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
componentDidMount() {
|
||||
this.calculateRoomMembersCount();
|
||||
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain());
|
||||
}
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
|
@ -461,8 +460,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
// Checking if the message has a "parentEventId" as we do not
|
||||
// want to hide the root event of the thread
|
||||
if (mxEv.replyInThread && mxEv.parentEventId
|
||||
&& this.props.hideThreadedMessages
|
||||
if (mxEv.isThreadRoot && this.props.hideThreadedMessages
|
||||
&& SettingsStore.getValue("feature_thread")) {
|
||||
return false;
|
||||
}
|
||||
|
@ -722,7 +720,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation = !wantsDateSeparator &&
|
||||
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
|
||||
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents, this.context.timelineRenderingType);
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
@ -794,6 +792,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean {
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
|
||||
return false;
|
||||
}
|
||||
if (prevEvent == null) {
|
||||
// first event in the panel: depends if we could back-paginate from
|
||||
// here.
|
||||
|
|
|
@ -24,6 +24,7 @@ import TimelinePanel from "./TimelinePanel";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -34,6 +35,7 @@ interface IProps {
|
|||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
static contextType = RoomContext;
|
||||
render() {
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{ _t('You’re all caught up') }</h2>
|
||||
|
@ -61,8 +63,13 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
return <RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Notification,
|
||||
}}>
|
||||
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
{ content }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ import { throttle } from 'lodash';
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
|
@ -199,10 +199,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId;
|
||||
const isViewingThread = this.state.phase === RightPanelPhases.ThreadView;
|
||||
if (isChangingRoom && isViewingThread) {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.ThreadPanel,
|
||||
});
|
||||
dispatchShowThreadsPanelEvent();
|
||||
}
|
||||
|
||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||
|
|
|
@ -15,17 +15,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {
|
||||
Dispatch,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
useContext,
|
||||
SetStateAction,
|
||||
Dispatch,
|
||||
} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||
|
@ -33,7 +33,8 @@ import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
|||
import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { sortBy } from "lodash";
|
||||
import { sortBy, uniqBy } from "lodash";
|
||||
import { GuestAccess, HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -333,6 +334,30 @@ interface IHierarchyLevelProps {
|
|||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => {
|
||||
const history = cli.getRoomUpgradeHistory(room.room_id, true);
|
||||
const cliRoom = history[history.length - 1];
|
||||
if (cliRoom) {
|
||||
return {
|
||||
...room,
|
||||
room_id: cliRoom.roomId,
|
||||
room_type: cliRoom.getType(),
|
||||
name: cliRoom.name,
|
||||
topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic,
|
||||
avatar_url: cliRoom.getMxcAvatarUrl(),
|
||||
canonical_alias: cliRoom.getCanonicalAlias(),
|
||||
aliases: cliRoom.getAltAliases(),
|
||||
world_readable: cliRoom.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")?.getContent()
|
||||
.history_visibility === HistoryVisibility.WorldReadable,
|
||||
guest_can_join: cliRoom.currentState.getStateEvents(EventType.RoomGuestAccess, "")?.getContent()
|
||||
.guest_access === GuestAccess.CanJoin,
|
||||
num_joined_members: cliRoom.getJoinedMemberCount(),
|
||||
};
|
||||
}
|
||||
|
||||
return room;
|
||||
};
|
||||
|
||||
export const HierarchyLevel = ({
|
||||
root,
|
||||
roomSet,
|
||||
|
@ -353,7 +378,7 @@ export const HierarchyLevel = ({
|
|||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
||||
const room = hierarchy.roomMap.get(ev.state_key);
|
||||
if (room && roomSet.has(room)) {
|
||||
result[room.room_type === RoomType.Space ? 0 : 1].push(room);
|
||||
result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room));
|
||||
}
|
||||
return result;
|
||||
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
||||
|
@ -361,7 +386,7 @@ export const HierarchyLevel = ({
|
|||
const newParents = new Set(parents).add(root.room_id);
|
||||
return <React.Fragment>
|
||||
{
|
||||
childRooms.map(room => (
|
||||
uniqBy(childRooms, "room_id").map(room => (
|
||||
<Tile
|
||||
key={room.room_id}
|
||||
room={room}
|
||||
|
@ -410,50 +435,39 @@ export const HierarchyLevel = ({
|
|||
|
||||
const INITIAL_PAGE_SIZE = 20;
|
||||
|
||||
export const useSpaceSummary = (space: Room): {
|
||||
export const useRoomHierarchy = (space: Room): {
|
||||
loading: boolean;
|
||||
rooms: IHierarchyRoom[];
|
||||
hierarchy: RoomHierarchy;
|
||||
loadMore(pageSize?: number): Promise <void>;
|
||||
} => {
|
||||
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||
|
||||
const resetHierarchy = useCallback(() => {
|
||||
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
||||
setHierarchy(hierarchy);
|
||||
|
||||
let discard = false;
|
||||
hierarchy.load().then(() => {
|
||||
if (discard) return;
|
||||
if (space !== hierarchy.root) return; // discard stale results
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
discard = true;
|
||||
};
|
||||
setHierarchy(hierarchy);
|
||||
}, [space]);
|
||||
useEffect(resetHierarchy, [resetHierarchy]);
|
||||
|
||||
useDispatcher(defaultDispatcher, (payload => {
|
||||
if (payload.action === Action.UpdateSpaceHierarchy) {
|
||||
setLoading(true);
|
||||
setRooms([]); // TODO
|
||||
resetHierarchy();
|
||||
}
|
||||
}));
|
||||
|
||||
const loadMore = useCallback(async (pageSize?: number) => {
|
||||
if (loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
|
||||
setLoading(true);
|
||||
if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
await hierarchy.load(pageSize);
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
}, [loading, hierarchy]);
|
||||
}, [hierarchy]);
|
||||
|
||||
const loading = hierarchy?.loading ?? true;
|
||||
return { loading, rooms, hierarchy, loadMore };
|
||||
};
|
||||
|
||||
|
@ -587,7 +601,7 @@ const SpaceHierarchy = ({
|
|||
|
||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||
|
||||
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
||||
const { loading, rooms, hierarchy, loadMore } = useRoomHierarchy(space);
|
||||
|
||||
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||
if (!rooms?.length) return new Set();
|
||||
|
|
|
@ -52,6 +52,7 @@ import {
|
|||
showAddExistingRooms,
|
||||
showCreateNewRoom,
|
||||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
|
@ -79,8 +80,11 @@ import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import { useRoomState } from "../../hooks/useRoomState";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -124,7 +128,7 @@ const useMyRoomMembership = (room: Room) => {
|
|||
};
|
||||
|
||||
const SpaceInfo = ({ space }) => {
|
||||
const joinRule = space.getJoinRule();
|
||||
const joinRule = useRoomState(space, state => state.getJoinRule());
|
||||
|
||||
let visibilitySection;
|
||||
if (joinRule === "public") {
|
||||
|
@ -204,8 +208,9 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
|||
|
||||
const spacesEnabled = SpaceStore.spacesEnabled;
|
||||
|
||||
const joinRule = useRoomState(space, state => state.getJoinRule());
|
||||
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||
&& space.getJoinRule() !== JoinRule.Public;
|
||||
&& joinRule !== JoinRule.Public;
|
||||
|
||||
let inviterSection;
|
||||
let joinButtons;
|
||||
|
@ -405,19 +410,21 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
</>;
|
||||
};
|
||||
|
||||
const SpaceLanding = ({ space }) => {
|
||||
const SpaceLanding = ({ space }: { space: Room }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
const userId = cli.getUserId();
|
||||
|
||||
let inviteButton;
|
||||
if (myMembership === "join" && space.canInvite(userId)) {
|
||||
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
className="mx_SpaceRoomView_landing_inviteButton"
|
||||
onClick={() => {
|
||||
showRoomInviteDialog(space.roomId);
|
||||
showSpaceInvite(space);
|
||||
}}
|
||||
>
|
||||
{ _t("Invite") }
|
||||
|
@ -729,7 +736,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
||||
<BetaPill />
|
||||
{ _t("<b>This is an experimental feature.</b> For now, " +
|
||||
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
|
|
|
@ -14,17 +14,26 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import EventTile from '../views/rooms/EventTile';
|
||||
import EventTile, { TileShape } from '../views/rooms/EventTile';
|
||||
import MatrixClientContext from '../../contexts/MatrixClientContext';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
|
||||
import ContextMenu, { useContextMenu } from './ContextMenu';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import { Layout } from '../../settings/Layout';
|
||||
import { useEventEmitter } from '../../hooks/useEventEmitter';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -32,62 +41,199 @@ interface IProps {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
threads?: Thread[];
|
||||
export const ThreadPanelItem: React.FC<{ event: MatrixEvent }> = ({ event }) => {
|
||||
return <EventTile
|
||||
key={event.getId()}
|
||||
mxEvent={event}
|
||||
enableFlair={false}
|
||||
showReadReceipts={false}
|
||||
as="div"
|
||||
tileShape={TileShape.Thread}
|
||||
alwaysShowTimestamps={true}
|
||||
/>;
|
||||
};
|
||||
|
||||
export enum ThreadFilterType {
|
||||
"My",
|
||||
"All"
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadPanel extends React.Component<IProps, IState> {
|
||||
private room: Room;
|
||||
type ThreadPanelHeaderOption = {
|
||||
label: string;
|
||||
description: string;
|
||||
key: ThreadFilterType;
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
}
|
||||
const useFilteredThreadsTimelinePanel = ({
|
||||
threads,
|
||||
room,
|
||||
filterOption,
|
||||
userId,
|
||||
updateTimeline,
|
||||
}: {
|
||||
threads: Map<string, Thread>;
|
||||
room: Room;
|
||||
userId: string;
|
||||
filterOption: ThreadFilterType;
|
||||
updateTimeline: () => void;
|
||||
}) => {
|
||||
const timelineSet = useMemo(() => new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
}), [room]);
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.room.on(ThreadEvent.Update, this.onThreadEventReceived);
|
||||
this.room.on(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||
}
|
||||
useEffect(() => {
|
||||
let filteredThreads = Array.from(threads);
|
||||
if (filterOption === ThreadFilterType.My) {
|
||||
filteredThreads = filteredThreads.filter(([id, thread]) => {
|
||||
return thread.rootEvent.getSender() === userId;
|
||||
});
|
||||
}
|
||||
// NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved
|
||||
// The proper list order should be top-to-bottom, like in social-media newsfeeds.
|
||||
filteredThreads.reverse().forEach(([id, thread]) => {
|
||||
const event = thread.rootEvent;
|
||||
if (timelineSet.findEventById(event.getId()) || event.status !== null) return;
|
||||
timelineSet.addEventToTimeline(
|
||||
event,
|
||||
timelineSet.getLiveTimeline(),
|
||||
true,
|
||||
);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room, timelineSet]);
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.room.removeListener(ThreadEvent.Update, this.onThreadEventReceived);
|
||||
this.room.removeListener(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||
}
|
||||
useEventEmitter(room, ThreadEvent.Update, (thread) => {
|
||||
const event = thread.rootEvent;
|
||||
if (
|
||||
// If that's a reply and not an event
|
||||
event !== thread.replyToEvent &&
|
||||
timelineSet.findEventById(event.getId()) ||
|
||||
event.status !== null
|
||||
) return;
|
||||
if (event !== thread.events[thread.events.length - 1]) {
|
||||
timelineSet.removeEvent(thread.events[thread.events.length - 1]);
|
||||
timelineSet.removeEvent(event);
|
||||
}
|
||||
timelineSet.addEventToTimeline(
|
||||
event,
|
||||
timelineSet.getLiveTimeline(),
|
||||
false,
|
||||
);
|
||||
updateTimeline();
|
||||
});
|
||||
|
||||
private onThreadEventReceived = () => this.updateThreads();
|
||||
return timelineSet;
|
||||
};
|
||||
|
||||
private updateThreads = (callback?: () => void): void => {
|
||||
this.setState({
|
||||
threads: this.room.getThreads(),
|
||||
}, callback);
|
||||
};
|
||||
export const ThreadPanelHeaderFilterOptionItem = ({
|
||||
label,
|
||||
description,
|
||||
onClick,
|
||||
isSelected,
|
||||
}: ThreadPanelHeaderOption & {
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
return <AccessibleButton
|
||||
aria-selected={isSelected}
|
||||
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span>{ label }</span>
|
||||
<span>{ description }</span>
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
private renderEventTile(event: MatrixEvent): JSX.Element {
|
||||
return <EventTile
|
||||
key={event.getId()}
|
||||
mxEvent={event}
|
||||
enableFlair={false}
|
||||
showReadReceipts={false}
|
||||
as="div"
|
||||
/>;
|
||||
}
|
||||
export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
||||
filterOption: ThreadFilterType;
|
||||
setFilterOption: (filterOption: ThreadFilterType) => void;
|
||||
}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||
const options: readonly ThreadPanelHeaderOption[] = [
|
||||
{
|
||||
label: _t("My threads"),
|
||||
description: _t("Shows all threads you’ve participated in"),
|
||||
key: ThreadFilterType.My,
|
||||
},
|
||||
{
|
||||
label: _t("All threads"),
|
||||
description: _t('Shows all threads from current room'),
|
||||
key: ThreadFilterType.All,
|
||||
},
|
||||
];
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
const value = options.find(option => option.key === filterOption);
|
||||
const contextMenuOptions = options.map(opt => <ThreadPanelHeaderFilterOptionItem
|
||||
key={opt.key}
|
||||
label={opt.label}
|
||||
description={opt.description}
|
||||
onClick={() => {
|
||||
setFilterOption(opt.key);
|
||||
closeMenu();
|
||||
}}
|
||||
isSelected={opt === value}
|
||||
/>);
|
||||
const contextMenu = menuDisplayed ? <ContextMenu top={0} right={25} onFinished={closeMenu} managed={false}>
|
||||
{ contextMenuOptions }
|
||||
</ContextMenu> : null;
|
||||
return <div className="mx_ThreadPanel__header">
|
||||
<span>{ _t("Threads") }</span>
|
||||
<ContextMenuButton inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||
{ `${_t('Show:')} ${value.label}` }
|
||||
</ContextMenuButton>
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
||||
const mxClient = useContext(MatrixClientContext);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const room = mxClient.getRoom(roomId);
|
||||
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
||||
const ref = useRef<TimelinePanel>();
|
||||
|
||||
const filteredTimelineSet = useFilteredThreadsTimelinePanel({
|
||||
threads: room.threads,
|
||||
room,
|
||||
filterOption,
|
||||
userId: mxClient.getUserId(),
|
||||
updateTimeline: () => ref.current?.refreshTimeline(),
|
||||
});
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...roomContext,
|
||||
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||
liveTimeline: filteredTimelineSet.getLiveTimeline(),
|
||||
showHiddenEventsInTimeline: true,
|
||||
}}>
|
||||
<BaseCard
|
||||
header={<ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />}
|
||||
className="mx_ThreadPanel"
|
||||
onClose={this.props.onClose}
|
||||
onClose={onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
{
|
||||
this.state?.threads.map((thread: Thread) => {
|
||||
if (thread.ready) {
|
||||
return this.renderEventTile(thread.rootEvent);
|
||||
}
|
||||
})
|
||||
}
|
||||
<TimelinePanel
|
||||
ref={ref}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={filteredTimelineSet}
|
||||
showUrlPreview={true}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
membersLoaded={true}
|
||||
tileShape={TileShape.ThreadPanel}
|
||||
/>
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
};
|
||||
export default ThreadPanel;
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
|
@ -156,7 +157,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
previousPhase={RightPanelPhases.ThreadPanel}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
|
@ -185,8 +186,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
relation={{
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: this.state.thread.id,
|
||||
}}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as sdk from '../../../index';
|
|||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
|
@ -27,6 +28,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.CompleteSecurity")
|
||||
|
@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this.onStoreUpdate);
|
||||
store.start();
|
||||
this.state = { phase: store.phase };
|
||||
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||
}
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({ phase: store.phase });
|
||||
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
|
||||
};
|
||||
|
||||
private onSkipClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const { phase } = this.state;
|
||||
const { phase, lostKeys } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === Phase.Intro) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
if (lostKeys) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Unable to verify this login");
|
||||
} else {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
|
@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
} else if (phase === Phase.Busy) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Really reset verification keys?");
|
||||
} else if (phase === Phase.Finished) {
|
||||
// SetupEncryptionBody will take care of calling onFinished, we don't need to do anything
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
let skipButton;
|
||||
if (phase === Phase.Intro || phase === Phase.ConfirmReset) {
|
||||
skipButton = (
|
||||
<AccessibleButton onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("Skip verification for now")} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<CompleteSecurityBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{ icon }
|
||||
{ title }
|
||||
{ skipButton }
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
|
|
|
@ -46,6 +46,7 @@ interface IState {
|
|||
phase: Phase;
|
||||
verificationRequest: VerificationRequest;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
|
@ -62,6 +63,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -105,11 +108,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
});
|
||||
};
|
||||
|
||||
private onSkipClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
private onSkipConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skipConfirm();
|
||||
|
@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
store.returnAfterSkip();
|
||||
};
|
||||
|
||||
private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
ev.preventDefault();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.reset();
|
||||
};
|
||||
|
||||
private onResetConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.resetConfirm();
|
||||
};
|
||||
|
||||
private onResetBackClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.returnAfterReset();
|
||||
};
|
||||
|
||||
private onDoneClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
|
@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
public render() {
|
||||
const {
|
||||
phase,
|
||||
lostKeys,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
|
@ -143,43 +158,67 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
isRoomEncrypted={false}
|
||||
/>;
|
||||
} else if (phase === Phase.Intro) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
if (lostKeys) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"It looks like you don't have a Security Key or any other devices you can " +
|
||||
"verify against. This device will not be able to access old encrypted messages. " +
|
||||
"In order to verify your identity on this device, you'll need to reset " +
|
||||
"your verification keys.",
|
||||
) }</p>
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Use another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||
{ _t("Skip") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Verify with another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
</div>
|
||||
<div className="mx_SetupEncryptionBody_reset">
|
||||
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
|
||||
a: (sub) => <a
|
||||
href=""
|
||||
onClick={this.onResetClick}
|
||||
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>,
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
|
@ -215,14 +254,13 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
) }</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
kind="danger_outline"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{ _t("Skip") }
|
||||
{ _t("I'll verify later") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
kind="primary"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{ _t("Go Back") }
|
||||
|
@ -230,6 +268,30 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Resetting your verification keys cannot be undone. After resetting, " +
|
||||
"you won't have access to old encrypted messages, and any friends who " +
|
||||
"have previously verified you will see security warnings until you " +
|
||||
"re-verify with them.",
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Please only proceed if you're sure you've lost all of your other " +
|
||||
"devices and your security key.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
|
||||
{ _t("Go Back") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||
return <Spinner />;
|
||||
} else {
|
||||
|
|
|
@ -41,7 +41,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
*
|
||||
* matrixClient: A matrix client. May be a different one to the one
|
||||
* currently being used generally (eg. to register with
|
||||
* one HS whilst beign a guest on another).
|
||||
* one HS whilst being a guest on another).
|
||||
* loginType: the login type of the auth stage being attempted
|
||||
* authSessionId: session id from the server
|
||||
* clientSecret: The client secret in use for identity server auth sessions
|
||||
|
@ -84,6 +84,7 @@ interface IAuthEntryProps {
|
|||
loginType: string;
|
||||
authSessionId: string;
|
||||
errorText?: string;
|
||||
errorCode?: string;
|
||||
// Is the auth logic currently waiting for something to happen?
|
||||
busy?: boolean;
|
||||
onPhaseChange: (phase: number) => void;
|
||||
|
@ -427,18 +428,29 @@ export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEn
|
|||
}
|
||||
|
||||
render() {
|
||||
let errorSection;
|
||||
// ignore the error when errcode is M_UNAUTHORIZED as we expect that error until the link is clicked.
|
||||
if (this.props.errorText && this.props.errorCode !== "M_UNAUTHORIZED") {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// This component is now only displayed once the token has been requested,
|
||||
// so we know the email has been sent. It can also get loaded after the user
|
||||
// has clicked the validation link if the server takes a while to propagate
|
||||
// the validation internally. If we're in the session spawned from clicking
|
||||
// the validation link, we won't know the email address, so if we don't have it,
|
||||
// assume that the link has been clicked and the server will realise when we poll.
|
||||
if (this.props.inputs.emailAddress === undefined) {
|
||||
return <Spinner />;
|
||||
} else if (this.props.stageState?.emailSid) {
|
||||
// we only have a session ID if the user has clicked the link in their email,
|
||||
// so show a loading state instead of "an email has been sent to..." because
|
||||
// that's confusing when you've already read that email.
|
||||
// We only have a session ID if the user has clicked the link in their email,
|
||||
// so show a loading state instead of "an email has been sent to..." because
|
||||
// that's confusing when you've already read that email.
|
||||
if (this.props.inputs.emailAddress === undefined || this.props.stageState?.emailSid) {
|
||||
if (errorSection) {
|
||||
return errorSection;
|
||||
}
|
||||
return <Spinner />;
|
||||
} else {
|
||||
return (
|
||||
|
@ -448,6 +460,7 @@ export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEn
|
|||
) }
|
||||
</p>
|
||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
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, { ComponentProps, useMemo, useState } from 'react';
|
||||
|
||||
import ConfirmUserActionDialog from "./ConfirmUserActionDialog";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||
|
||||
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
|
||||
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
|
||||
space: Room;
|
||||
allLabel: string;
|
||||
specificLabel: string;
|
||||
noneLabel?: string;
|
||||
warningMessage?: string;
|
||||
onFinished(success: boolean, reason?: string, rooms?: Room[]): void;
|
||||
spaceChildFilter?(child: Room): boolean;
|
||||
}
|
||||
|
||||
const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
|
||||
space,
|
||||
spaceChildFilter,
|
||||
allLabel,
|
||||
specificLabel,
|
||||
noneLabel,
|
||||
warningMessage,
|
||||
onFinished,
|
||||
...props
|
||||
}) => {
|
||||
const spaceChildren = useMemo(() => {
|
||||
const children = SpaceStore.instance.getChildren(space.roomId);
|
||||
if (spaceChildFilter) {
|
||||
return children.filter(spaceChildFilter);
|
||||
}
|
||||
return children;
|
||||
}, [space.roomId, spaceChildFilter]);
|
||||
|
||||
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
|
||||
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
|
||||
|
||||
let warning: JSX.Element;
|
||||
if (warningMessage) {
|
||||
warning = <div className="mx_ConfirmSpaceUserActionDialog_warning">
|
||||
{ warningMessage }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmUserActionDialog
|
||||
{...props}
|
||||
onFinished={(success: boolean, reason?: string) => {
|
||||
onFinished(success, reason, roomsToLeave);
|
||||
}}
|
||||
className="mx_ConfirmSpaceUserActionDialog"
|
||||
>
|
||||
{ warning }
|
||||
<SpaceChildrenPicker
|
||||
space={space}
|
||||
spaceChildren={spaceChildren}
|
||||
selected={selectedRooms}
|
||||
allLabel={allLabel}
|
||||
specificLabel={specificLabel}
|
||||
noneLabel={noneLabel}
|
||||
onChange={setRoomsToLeave}
|
||||
/>
|
||||
</ConfirmUserActionDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSpaceUserActionDialog;
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ChangeEvent, ReactNode } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -25,12 +27,13 @@ import MemberAvatar from '../avatars/MemberAvatar';
|
|||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Field from '../elements/Field';
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: RoomMember;
|
||||
member?: RoomMember;
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType;
|
||||
groupMember?: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient;
|
||||
action: string; // eg. 'Ban'
|
||||
|
@ -41,9 +44,15 @@ interface IProps {
|
|||
// be the string entered.
|
||||
askReason?: boolean;
|
||||
danger?: boolean;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
onFinished: (success: boolean, reason?: string) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
* Takes a user ID and a verb, displays the target user prominently
|
||||
|
@ -53,37 +62,50 @@ interface IProps {
|
|||
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
||||
export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
danger: false,
|
||||
askReason: false,
|
||||
};
|
||||
|
||||
public onOk = (): void => {
|
||||
this.props.onFinished(true, this.reasonField.current?.value);
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
};
|
||||
}
|
||||
|
||||
private onOk = (): void => {
|
||||
this.props.onFinished(true, this.state.reason);
|
||||
};
|
||||
|
||||
public onCancel = (): void => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onReasonChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
reason: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||
|
||||
let reasonBox;
|
||||
if (this.props.askReason) {
|
||||
reasonBox = (
|
||||
<div>
|
||||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this.reasonField}
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<form onSubmit={this.onOk}>
|
||||
<Field
|
||||
type="text"
|
||||
onChange={this.onReasonChange}
|
||||
value={this.state.reason}
|
||||
className="mx_ConfirmUserActionDialog_reasonField"
|
||||
label={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -105,19 +127,23 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
|||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ConfirmUserActionDialog"
|
||||
className={classNames("mx_ConfirmUserActionDialog", this.props.className)}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
{ avatar }
|
||||
<div className="mx_ConfirmUserActionDialog_user">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
{ avatar }
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
|
||||
{ reasonBox }
|
||||
{ this.props.children }
|
||||
</div>
|
||||
{ reasonBox }
|
||||
<DialogButtons primaryButton={this.props.action}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={confirmButtonClass}
|
||||
|
|
|
@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -90,10 +92,22 @@ export interface IGroupSummary {
|
|||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
enum Progress {
|
||||
NotStarted,
|
||||
ValidatingInputs,
|
||||
FetchingData,
|
||||
CreatingSpace,
|
||||
InvitingUsers,
|
||||
// anything beyond here is inviting user n - 4
|
||||
}
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [progress, setProgress] = useState(Progress.NotStarted);
|
||||
const [numInvites, setNumInvites] = useState(0);
|
||||
const busy = progress > 0;
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
|
@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
setProgress(Progress.ValidatingInputs);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
setBusy(false);
|
||||
setProgress(0);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
setBusy(false);
|
||||
setProgress(0);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProgress(Progress.FetchingData);
|
||||
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
setNumInvites(members.length + invitedMembers.length);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
|
@ -167,6 +185,8 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
}
|
||||
}
|
||||
|
||||
setProgress(Progress.CreatingSpace);
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
|
@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
||||
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
setProgress(Progress.InvitingUsers);
|
||||
|
||||
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||
await inviteUsersToRoom(roomId, userIds, () => setProgress(p => p + 1));
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
|
@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
setError(e);
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
setProgress(Progress.NotStarted);
|
||||
};
|
||||
|
||||
let footer;
|
||||
|
@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else if (busy) {
|
||||
let description: string;
|
||||
switch (progress) {
|
||||
case Progress.ValidatingInputs:
|
||||
case Progress.FetchingData:
|
||||
description = _t("Fetching data...");
|
||||
break;
|
||||
case Progress.CreatingSpace:
|
||||
description = _t("Creating Space...");
|
||||
break;
|
||||
case Progress.InvitingUsers:
|
||||
default:
|
||||
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||
count: numInvites,
|
||||
progress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
footer = <span>
|
||||
<ProgressBar
|
||||
value={progress > Progress.FetchingData ? progress : 0}
|
||||
max={numInvites + Progress.InvitingUsers}
|
||||
/>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
|
||||
{ description }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
||||
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
|
@ -22,108 +22,7 @@ import { _t } from '../../../languageHandler';
|
|||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { Entry } from "./AddExistingToSpaceDialog";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
|
||||
enum RoomsToLeave {
|
||||
All = "All",
|
||||
Specific = "Specific",
|
||||
None = "None",
|
||||
}
|
||||
|
||||
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
if (!lcQuery) {
|
||||
return rooms;
|
||||
}
|
||||
|
||||
const matcher = new QueryMatcher<Room>(rooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
|
||||
return matcher.match(lcQuery);
|
||||
}, [rooms, lcQuery]);
|
||||
|
||||
return <div className="mx_LeaveSpaceDialog_section">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={filterPlaceholder}
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
||||
{ filteredRooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={(checked) => {
|
||||
onChange(checked, room);
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
|
||||
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
|
||||
const [state, setState] = useState<string>(RoomsToLeave.None);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === RoomsToLeave.All) {
|
||||
setRoomsToLeave(spaceChildren);
|
||||
} else {
|
||||
setRoomsToLeave([]);
|
||||
}
|
||||
}, [setRoomsToLeave, state, spaceChildren]);
|
||||
|
||||
return <div className="mx_LeaveSpaceDialog_section">
|
||||
<StyledRadioGroup
|
||||
name="roomsToLeave"
|
||||
value={state}
|
||||
onChange={setState}
|
||||
definitions={[
|
||||
{
|
||||
value: RoomsToLeave.None,
|
||||
label: _t("Don't leave any rooms"),
|
||||
}, {
|
||||
value: RoomsToLeave.All,
|
||||
label: _t("Leave all rooms"),
|
||||
}, {
|
||||
value: RoomsToLeave.Specific,
|
||||
label: _t("Leave some rooms"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{ state === RoomsToLeave.Specific && (
|
||||
<SpaceChildPicker
|
||||
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
|
||||
rooms={spaceChildren}
|
||||
selected={selected}
|
||||
onChange={(selected: boolean, room: Room) => {
|
||||
if (selected) {
|
||||
setRoomsToLeave([room, ...roomsToLeave]);
|
||||
} else {
|
||||
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
</div>;
|
||||
};
|
||||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -144,6 +43,7 @@ const isOnlyAdmin = (room: Room): boolean => {
|
|||
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
|
||||
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
|
||||
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
|
||||
|
||||
let rejoinWarning;
|
||||
if (space.getJoinRule() !== JoinRule.Public) {
|
||||
|
@ -180,12 +80,17 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
|||
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
|
||||
</p>
|
||||
|
||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
||||
space={space}
|
||||
spaceChildren={spaceChildren}
|
||||
roomsToLeave={roomsToLeave}
|
||||
setRoomsToLeave={setRoomsToLeave}
|
||||
/> }
|
||||
{ spaceChildren.length > 0 && (
|
||||
<SpaceChildrenPicker
|
||||
space={space}
|
||||
spaceChildren={spaceChildren}
|
||||
selected={selectedRooms}
|
||||
onChange={setRoomsToLeave}
|
||||
noneLabel={_t("Don't leave any rooms")}
|
||||
allLabel={_t("Leave all rooms")}
|
||||
specificLabel={_t("Leave some rooms")}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
|
||||
{ onlyAdminWarning }
|
||||
|
|
|
@ -44,18 +44,31 @@ interface IProps {
|
|||
initialTabId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
roomName: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||
export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||
export default class RoomSettingsDialog extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = { roomName: '' };
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
this.onRoomName();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
}
|
||||
|
||||
private onAction = (payload): void => {
|
||||
|
@ -66,6 +79,12 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomName = (): void => {
|
||||
this.setState({
|
||||
roomName: MatrixClientPeg.get().getRoom(this.props.roomId).name,
|
||||
});
|
||||
};
|
||||
|
||||
private getTabs(): Tab[] {
|
||||
const tabs: Tab[] = [];
|
||||
|
||||
|
@ -122,7 +141,7 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
||||
const roomName = this.state.roomName;
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_RoomSettingsDialog'
|
||||
|
|
|
@ -28,15 +28,25 @@ import { IDialogProps } from "./IDialogProps";
|
|||
import BugReportDialog from './BugReportDialog';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
|
||||
export interface IFinishedOpts {
|
||||
continue: boolean;
|
||||
invite: boolean;
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: string;
|
||||
targetVersion: string;
|
||||
description?: ReactNode;
|
||||
doUpgrade?(opts: IFinishedOpts, fn: (progressText: string, progress: number, total: number) => void): Promise<void>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
inviteUsersToNewRoom: boolean;
|
||||
progressText?: string;
|
||||
progress?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
||||
|
@ -50,15 +60,30 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
|||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
||||
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
||||
this.currentVersion = room?.getVersion() || "1";
|
||||
this.currentVersion = room?.getVersion();
|
||||
|
||||
this.state = {
|
||||
inviteUsersToNewRoom: true,
|
||||
};
|
||||
}
|
||||
|
||||
private onProgressCallback = (progressText: string, progress: number, total: number): void => {
|
||||
this.setState({ progressText, progress, total });
|
||||
};
|
||||
|
||||
private onContinue = () => {
|
||||
this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
|
||||
const opts = {
|
||||
continue: true,
|
||||
invite: this.isPrivate && this.state.inviteUsersToNewRoom,
|
||||
};
|
||||
|
||||
if (this.props.doUpgrade) {
|
||||
this.props.doUpgrade(opts, this.onProgressCallback).then(() => {
|
||||
this.props.onFinished(opts);
|
||||
});
|
||||
} else {
|
||||
this.props.onFinished(opts);
|
||||
}
|
||||
};
|
||||
|
||||
private onCancel = () => {
|
||||
|
@ -118,6 +143,23 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
|||
);
|
||||
}
|
||||
|
||||
let footer: JSX.Element;
|
||||
if (this.state.progressText) {
|
||||
footer = <span className="mx_RoomUpgradeWarningDialog_progress">
|
||||
<ProgressBar value={this.state.progress} max={this.state.total} />
|
||||
<div className="mx_RoomUpgradeWarningDialog_progressText">
|
||||
{ this.state.progressText }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <DialogButtons
|
||||
primaryButton={_t("Upgrade")}
|
||||
onPrimaryButtonClick={this.onContinue}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this.onCancel}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_RoomUpgradeWarningDialog'
|
||||
|
@ -154,12 +196,7 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
|||
</p>
|
||||
{ inviteToggle }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Upgrade")}
|
||||
onPrimaryButtonClick={this.onContinue}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
{ footer }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -268,7 +268,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
|||
};
|
||||
|
||||
const buttonRect = handle.current.getBoundingClientRect();
|
||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} focusLock>
|
||||
<div className="mx_NetworkDropdown_menu">
|
||||
{ options }
|
||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||
|
|
|
@ -143,6 +143,10 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
|
||||
public focus() {
|
||||
this.input.focus();
|
||||
// programmatic does not fire onFocus handler
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
}
|
||||
|
||||
private onFocus = (ev) => {
|
||||
|
|
|
@ -21,7 +21,6 @@ import classNames from 'classnames';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
|
||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
|
@ -36,6 +35,7 @@ import Spinner from './Spinner';
|
|||
import ReplyTile from "../rooms/ReplyTile";
|
||||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
/**
|
||||
* This number is based on the previous behavior - if we have message of height
|
||||
|
@ -225,10 +225,10 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
return { body, html };
|
||||
}
|
||||
|
||||
public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
|
||||
public static makeReplyMixIn(ev: MatrixEvent) {
|
||||
if (!ev) return {};
|
||||
|
||||
const replyMixin = {
|
||||
const mixin: any = {
|
||||
'm.relates_to': {
|
||||
'm.in_reply_to': {
|
||||
'event_id': ev.getId(),
|
||||
|
@ -237,16 +237,20 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Rendering hint for threads, only attached if true to make
|
||||
* sure that Element does not start sending that property for all events
|
||||
* If the event replied is part of a thread
|
||||
* Add the `m.thread` relation so that clients
|
||||
* that know how to handle that relation will
|
||||
* be able to render them more accurately
|
||||
*/
|
||||
if (replyInThread) {
|
||||
const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
|
||||
inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
|
||||
if (ev.isThreadRelation) {
|
||||
mixin['m.relates_to'] = {
|
||||
...mixin['m.relates_to'],
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: ev.threadRootId,
|
||||
};
|
||||
}
|
||||
|
||||
return replyMixin;
|
||||
return mixin;
|
||||
}
|
||||
|
||||
public static hasThreadReply(event: MatrixEvent) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from "react";
|
|||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
|
||||
|
@ -36,6 +36,7 @@ interface IProps {
|
|||
interface IState {
|
||||
loading: boolean;
|
||||
blob?: Blob;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DownloadActionButton")
|
||||
|
@ -47,12 +48,17 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
|||
|
||||
this.state = {
|
||||
loading: false,
|
||||
tooltip: _td("Downloading"),
|
||||
};
|
||||
}
|
||||
|
||||
private onDownloadClick = async () => {
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (this.props.mediaEventHelperGet().media.isEncrypted) {
|
||||
this.setState({ tooltip: _td("Decrypting") });
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
if (this.state.blob) {
|
||||
|
@ -87,7 +93,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
|||
|
||||
return <RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={spinner ? _t("Decrypting") : _t("Download")}
|
||||
title={spinner ? _t(this.state.tooltip) : _t("Download")}
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={!!spinner}
|
||||
>
|
||||
|
|
|
@ -289,7 +289,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
// Like the resend button, the react and reply buttons need to appear before the edit.
|
||||
// The only catch is we do the reply button first so that we can make sure the react
|
||||
// button is the very first button without having to do length checks for `splice()`.
|
||||
if (this.context.canReply && this.context.timelineRenderingType === TimelineRenderingType.Room) {
|
||||
if (this.context.canReply && this.context.timelineRenderingType !== TimelineRenderingType.Thread) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
@ -325,6 +325,19 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
/>);
|
||||
}
|
||||
}
|
||||
// Show thread icon even for deleted messages, but only within main timeline
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Room &&
|
||||
SettingsStore.getValue("feature_thread") &&
|
||||
this.props.mxEvent.getThread() &&
|
||||
!isContentActionable(this.props.mxEvent)
|
||||
) {
|
||||
toolbarOpts.unshift(<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
|
||||
title={_t("Thread")}
|
||||
onClick={this.onThreadClick}
|
||||
key="thread"
|
||||
/>);
|
||||
}
|
||||
|
||||
if (allowCancel) {
|
||||
toolbarOpts.push(cancelSendingButton);
|
||||
|
|
|
@ -49,16 +49,18 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
isSelfVerification,
|
||||
}: IProps) => {
|
||||
let content: JSX.Element;
|
||||
if (waitingForOtherParty || waitingForNetwork) {
|
||||
if (waitingForOtherParty && isSelfVerification) {
|
||||
content = (
|
||||
<div>
|
||||
{ _t("To proceed, please accept the verification request on your other login.") }
|
||||
</div>
|
||||
);
|
||||
} else if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text: string;
|
||||
if (waitingForOtherParty) {
|
||||
if (isSelfVerification) {
|
||||
text = _t("Accept on your other login…");
|
||||
} else {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Accepting…");
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ import RightPanelStore from "../../../stores/RightPanelStore";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -122,6 +124,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_thread") && <HeaderButton
|
||||
name="threadsButton"
|
||||
title={_t("Threads")}
|
||||
onClick={dispatchShowThreadsPanelEvent}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.ThreadPanel)}
|
||||
analytics={['Right Panel', 'Threads List Button', 'click']}
|
||||
/> }
|
||||
<HeaderButton
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
|
|
|
@ -48,6 +48,7 @@ import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widget
|
|||
import RoomName from "../elements/RoomName";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -221,13 +222,6 @@ const onRoomFilesClick = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const onRoomThreadsClick = () => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.ThreadPanel,
|
||||
});
|
||||
};
|
||||
|
||||
const onRoomSettingsClick = () => {
|
||||
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
@ -291,7 +285,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
{ _t("Export chat") }
|
||||
</Button>
|
||||
{ SettingsStore.getValue("feature_thread") && (
|
||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={dispatchShowThreadsPanelEvent}>
|
||||
{ _t("Show threads") }
|
||||
</Button>
|
||||
) }
|
||||
|
|
|
@ -70,8 +70,12 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
|||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
|
||||
import { bulkSpaceBehaviour } from "../../../utils/space";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -393,7 +397,7 @@ const UserOptionsSection: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
if (canInvite && (!member || !member.membership || member.membership === 'leave')) {
|
||||
if (canInvite && (member?.membership ?? 'leave') === 'leave' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
|
||||
const onInviteUserButton = async () => {
|
||||
try {
|
||||
|
@ -532,7 +536,7 @@ interface IBaseProps {
|
|||
stopUpdating(): void;
|
||||
}
|
||||
|
||||
const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
|
||||
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// check if user can be kicked/disinvited
|
||||
|
@ -542,21 +546,38 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
|
|||
const { finished } = Modal.createTrackedDialog(
|
||||
'Confirm User Action Dialog',
|
||||
'onKick',
|
||||
ConfirmUserActionDialog,
|
||||
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
|
||||
{
|
||||
member,
|
||||
action: member.membership === "invite" ? _t("Disinvite") : _t("Kick"),
|
||||
title: member.membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"),
|
||||
title: member.membership === "invite"
|
||||
? _t("Disinvite from %(roomName)s", { roomName: room.name })
|
||||
: _t("Kick from %(roomName)s", { roomName: room.name }),
|
||||
askReason: member.membership === "join",
|
||||
danger: true,
|
||||
// space-specific props
|
||||
space: room,
|
||||
spaceChildFilter: (child: Room) => {
|
||||
// Return true if the target member is not banned and we have sufficient PL to ban them
|
||||
const myMember = child.getMember(cli.credentials.userId);
|
||||
const theirMember = child.getMember(member.userId);
|
||||
return myMember && theirMember && theirMember.membership === member.membership &&
|
||||
myMember.powerLevel > theirMember.powerLevel &&
|
||||
child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel);
|
||||
},
|
||||
allLabel: _t("Kick them from everything I'm able to"),
|
||||
specificLabel: _t("Kick them from specific things I'm able to"),
|
||||
warningMessage: _t("They'll still be able to access whatever you're not an admin of."),
|
||||
},
|
||||
room.isSpaceRoom() ? "mx_ConfirmSpaceUserActionDialog_wrapper" : undefined,
|
||||
);
|
||||
|
||||
const [proceed, reason] = await finished;
|
||||
const [proceed, reason, rooms = []] = await finished;
|
||||
if (!proceed) return;
|
||||
|
||||
startUpdating();
|
||||
cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
|
||||
|
||||
bulkSpaceBehaviour(room, rooms, room => cli.kick(room.roomId, member.userId, reason || undefined)).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
logger.log("Kick success");
|
||||
|
@ -656,34 +677,69 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
|
|||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
|
||||
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const isBanned = member.membership === "ban";
|
||||
const onBanOrUnban = async () => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Confirm User Action Dialog',
|
||||
'onBanOrUnban',
|
||||
ConfirmUserActionDialog,
|
||||
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
|
||||
{
|
||||
member,
|
||||
action: member.membership === 'ban' ? _t("Unban") : _t("Ban"),
|
||||
title: member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"),
|
||||
askReason: member.membership !== 'ban',
|
||||
danger: member.membership !== 'ban',
|
||||
action: isBanned ? _t("Unban") : _t("Ban"),
|
||||
title: isBanned
|
||||
? _t("Unban from %(roomName)s", { roomName: room.name })
|
||||
: _t("Ban from %(roomName)s", { roomName: room.name }),
|
||||
askReason: !isBanned,
|
||||
danger: !isBanned,
|
||||
// space-specific props
|
||||
space: room,
|
||||
spaceChildFilter: isBanned
|
||||
? (child: Room) => {
|
||||
// Return true if the target member is banned and we have sufficient PL to unban
|
||||
const myMember = child.getMember(cli.credentials.userId);
|
||||
const theirMember = child.getMember(member.userId);
|
||||
return myMember && theirMember && theirMember.membership === "ban" &&
|
||||
myMember.powerLevel > theirMember.powerLevel &&
|
||||
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
|
||||
}
|
||||
: (child: Room) => {
|
||||
// Return true if the target member isn't banned and we have sufficient PL to ban
|
||||
const myMember = child.getMember(cli.credentials.userId);
|
||||
const theirMember = child.getMember(member.userId);
|
||||
return myMember && theirMember && theirMember.membership !== "ban" &&
|
||||
myMember.powerLevel > theirMember.powerLevel &&
|
||||
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
|
||||
},
|
||||
allLabel: isBanned
|
||||
? _t("Unban them from everything I'm able to")
|
||||
: _t("Ban them from everything I'm able to"),
|
||||
specificLabel: isBanned
|
||||
? _t("Unban them from specific things I'm able to")
|
||||
: _t("Ban them from specific things I'm able to"),
|
||||
warningMessage: isBanned
|
||||
? _t("They won't be able to access whatever you're not an admin of.")
|
||||
: _t("They'll still be able to access whatever you're not an admin of."),
|
||||
},
|
||||
room.isSpaceRoom() ? "mx_ConfirmSpaceUserActionDialog_wrapper" : undefined,
|
||||
);
|
||||
|
||||
const [proceed, reason] = await finished;
|
||||
const [proceed, reason, rooms = []] = await finished;
|
||||
if (!proceed) return;
|
||||
|
||||
startUpdating();
|
||||
let promise;
|
||||
if (member.membership === 'ban') {
|
||||
promise = cli.unban(member.roomId, member.userId);
|
||||
} else {
|
||||
promise = cli.ban(member.roomId, member.userId, reason || undefined);
|
||||
}
|
||||
promise.then(() => {
|
||||
|
||||
const fn = (roomId: string) => {
|
||||
if (isBanned) {
|
||||
return cli.unban(roomId, member.userId);
|
||||
} else {
|
||||
return cli.ban(roomId, member.userId, reason || undefined);
|
||||
}
|
||||
};
|
||||
|
||||
bulkSpaceBehaviour(room, rooms, room => fn(room.roomId)).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
logger.log("Ban success");
|
||||
|
@ -699,12 +755,12 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
|
|||
};
|
||||
|
||||
let label = _t("Ban");
|
||||
if (member.membership === 'ban') {
|
||||
if (isBanned) {
|
||||
label = _t("Unban");
|
||||
}
|
||||
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: member.membership !== 'ban',
|
||||
mx_UserInfo_destructive: !isBanned,
|
||||
});
|
||||
|
||||
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
||||
|
@ -817,18 +873,28 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
const isMe = me.userId === member.userId;
|
||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||
|
||||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton
|
||||
room={room}
|
||||
member={member}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}
|
||||
/>;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||
banButton = <BanToggleButton
|
||||
room={room}
|
||||
member={member}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}
|
||||
/>;
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||
muteButton = (
|
||||
<MuteToggleButton
|
||||
member={member}
|
||||
|
|
|
@ -35,7 +35,7 @@ interface IState {
|
|||
avatarFile: File;
|
||||
originalTopic: string;
|
||||
topic: string;
|
||||
enableProfileSave: boolean;
|
||||
profileFieldsTouched: Record<string, boolean>;
|
||||
canSetName: boolean;
|
||||
canSetTopic: boolean;
|
||||
canSetAvatar: boolean;
|
||||
|
@ -71,7 +71,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
avatarFile: null,
|
||||
originalTopic: topic,
|
||||
topic: topic,
|
||||
enableProfileSave: false,
|
||||
profileFieldsTouched: {},
|
||||
canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()),
|
||||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||
|
@ -88,17 +88,24 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
this.setState({
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private isSaveEnabled = () => {
|
||||
return Boolean(Object.values(this.state.profileFieldsTouched).length);
|
||||
};
|
||||
|
||||
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
if (!this.isSaveEnabled()) return;
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
profileFieldsTouched: {},
|
||||
displayName: this.state.originalDisplayName,
|
||||
topic: this.state.originalTopic,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
|
@ -110,8 +117,8 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this.setState({ enableProfileSave: false });
|
||||
if (!this.isSaveEnabled()) return;
|
||||
this.setState({ profileFieldsTouched: {} });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
|
@ -156,18 +163,38 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ displayName: e.target.value });
|
||||
if (this.state.originalDisplayName === e.target.value) {
|
||||
this.setState({ enableProfileSave: false });
|
||||
this.setState({
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
name: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({ enableProfileSave: true });
|
||||
this.setState({
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ topic: e.target.value });
|
||||
if (this.state.originalTopic === e.target.value) {
|
||||
this.setState({ enableProfileSave: false });
|
||||
this.setState({
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
topic: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({ enableProfileSave: true });
|
||||
this.setState({
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
topic: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -176,7 +203,10 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
this.setState({
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
enableProfileSave: false,
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
avatar: false,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -187,7 +217,10 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
this.setState({
|
||||
avatarUrl: String(ev.target.result),
|
||||
avatarFile: file,
|
||||
enableProfileSave: true,
|
||||
profileFieldsTouched: {
|
||||
...this.state.profileFieldsTouched,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
@ -205,14 +238,14 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
<AccessibleButton
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
disabled={!this.isSaveEnabled()}
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this.saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
disabled={!this.isSaveEnabled()}
|
||||
>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -35,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
@ -46,7 +46,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -70,7 +70,6 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
|||
function createEditContent(
|
||||
model: EditorModel,
|
||||
editedEvent: MatrixEvent,
|
||||
renderingContext?: TimelineRenderingType,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
|
@ -112,10 +111,6 @@ function createEditContent(
|
|||
},
|
||||
};
|
||||
|
||||
if (renderingContext === TimelineRenderingType.Thread) {
|
||||
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
|
||||
}
|
||||
|
||||
return Object.assign(relation, contentBody);
|
||||
}
|
||||
|
||||
|
@ -143,8 +138,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
const isRestored = this.createEditorModel();
|
||||
const ev = this.props.editState.getEvent();
|
||||
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, ev, renderingContext);
|
||||
const editContent = createEditContent(this.model, ev);
|
||||
this.state = {
|
||||
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
|
||||
};
|
||||
|
@ -369,8 +363,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, editedEvent, renderingContext);
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
|
|
@ -56,9 +56,9 @@ import ReadReceiptMarker from "./ReadReceiptMarker";
|
|||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from '../messages/ReactionsRow';
|
||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -193,6 +193,7 @@ export enum TileShape {
|
|||
FileGrid = "file_grid",
|
||||
Pinned = "pinned",
|
||||
Thread = "thread",
|
||||
ThreadPanel = "thread_list"
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -511,6 +512,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
|
||||
}
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
this.props.mxEvent.off(ThreadEvent.Ready, this.updateThread);
|
||||
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
|
@ -541,13 +546,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
<div
|
||||
className="mx_ThreadInfo"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.ThreadView,
|
||||
refireParams: {
|
||||
event: this.props.mxEvent,
|
||||
},
|
||||
});
|
||||
dispatchShowThreadEvent(this.props.mxEvent);
|
||||
}}
|
||||
>
|
||||
<span className="mx_EventListSummary_avatars">
|
||||
|
|
|
@ -44,6 +44,9 @@ import MemberTile from "./MemberTile";
|
|||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import { throttle } from 'lodash';
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`;
|
||||
|
||||
|
@ -169,7 +172,11 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
private get canInvite(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
return room && room.canInvite(cli.getUserId());
|
||||
|
||||
return (
|
||||
room?.canInvite(cli.getUserId()) ||
|
||||
(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public)
|
||||
);
|
||||
}
|
||||
|
||||
private getMembersState(members: Array<RoomMember>): IState {
|
||||
|
@ -530,7 +537,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
let inviteButtonText = _t("Invite to this room");
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import React, { createRef } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -52,6 +52,9 @@ import EditorModel from "../../../editor/model";
|
|||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
@ -193,12 +196,37 @@ class UploadButton extends React.Component<IUploadButtonProps> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: [polls] Make this component actually do something
|
||||
class PollButton extends React.PureComponent {
|
||||
private onCreateClick = () => {
|
||||
Modal.createTrackedDialog('Polls', 'Not Yet Implemented', InfoDialog, {
|
||||
// XXX: Deliberately not translated given this dialog is meant to be replaced and we don't
|
||||
// want to clutter the language files with short-lived strings.
|
||||
title: "Polls are currently in development",
|
||||
description: "" +
|
||||
"Thanks for testing polls! We haven't quite gotten a chance to write the feature yet " +
|
||||
"though. Check back later for updates.",
|
||||
hasCloseButton: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_poll"
|
||||
onClick={this.onCreateClick}
|
||||
title={_t('Create poll')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
replyToEvent?: MatrixEvent;
|
||||
replyInThread?: boolean;
|
||||
relation?: IEventRelation;
|
||||
showReplyPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
compact?: boolean;
|
||||
|
@ -225,7 +253,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private instanceId: number;
|
||||
|
||||
static defaultProps = {
|
||||
replyInThread: false,
|
||||
showReplyPreview: true,
|
||||
compact: false,
|
||||
};
|
||||
|
@ -351,9 +378,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private renderPlaceholderText = () => {
|
||||
if (this.props.replyToEvent) {
|
||||
if (this.props.replyInThread && this.props.e2eStatus) {
|
||||
const replyingToThread = this.props.relation?.rel_type === RelationType.Thread;
|
||||
if (replyingToThread && this.props.e2eStatus) {
|
||||
return _t('Reply to encrypted thread…');
|
||||
} else if (this.props.replyInThread) {
|
||||
} else if (replyingToThread) {
|
||||
return _t('Reply to thread…');
|
||||
} else if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
|
@ -432,6 +460,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
if (!this.state.haveRecording) {
|
||||
if (SettingsStore.getValue("feature_polls")) {
|
||||
buttons.push(
|
||||
<PollButton key="polls" />,
|
||||
);
|
||||
}
|
||||
buttons.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
|
@ -526,7 +559,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyInThread={this.props.replyInThread}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
|
|
|
@ -28,15 +28,17 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { showSpaceInvite } from "../../../utils/space";
|
||||
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import EventTileBubble from "../messages/EventTileBubble";
|
||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
|
@ -150,7 +152,7 @@ const NewRoomIntro = () => {
|
|||
{ _t("Invite to just this room") }
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
} else if (room.canInvite(cli.getUserId())) {
|
||||
} else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
|
|
|
@ -155,58 +155,55 @@ export default class RoomHeader extends React.Component<IProps> {
|
|||
/>;
|
||||
}
|
||||
|
||||
let forgetButton;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")} />;
|
||||
}
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
let appsButton;
|
||||
if (this.props.onAppsClick) {
|
||||
appsButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")} />;
|
||||
}
|
||||
|
||||
let searchButton;
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
searchButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")} />;
|
||||
}
|
||||
|
||||
let voiceCallButton;
|
||||
let videoCallButton;
|
||||
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
|
||||
voiceCallButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
|
||||
title={_t("Voice call")} />;
|
||||
videoCallButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
|
||||
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
|
||||
title={_t("Video call")} />;
|
||||
const voiceCallButton = <AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
|
||||
title={_t("Voice call")}
|
||||
/>;
|
||||
const videoCallButton = <AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
|
||||
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
|
||||
title={_t("Video call")}
|
||||
/>;
|
||||
buttons.push(voiceCallButton, videoCallButton);
|
||||
}
|
||||
|
||||
if (this.props.onForgetClick) {
|
||||
const forgetButton = <AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
/>;
|
||||
buttons.push(forgetButton);
|
||||
}
|
||||
|
||||
if (this.props.onAppsClick) {
|
||||
const appsButton = <AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||
/>;
|
||||
buttons.push(appsButton);
|
||||
}
|
||||
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
const searchButton = <AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
/>;
|
||||
buttons.push(searchButton);
|
||||
}
|
||||
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ videoCallButton }
|
||||
{ voiceCallButton }
|
||||
{ forgetButton }
|
||||
{ appsButton }
|
||||
{ searchButton }
|
||||
{ buttons }
|
||||
</div>;
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||
|
|
|
@ -49,6 +49,9 @@ import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -133,32 +136,38 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
MatrixClientPeg.get().getUserId());
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
{
|
||||
shouldShowComponent(UIComponent.CreateRooms)
|
||||
? (<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconBrowse"
|
||||
|
@ -449,8 +458,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private renderSublists(): React.ReactElement[] {
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering
|
||||
const showSkeleton = !this.state.isNameFiltering &&
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
|
||||
const showSkeleton = !this.state.isNameFiltering && !this.state.suggestedRooms?.length &&
|
||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||
|
||||
return TAG_ORDER.reduce((tags, tagId) => {
|
||||
|
@ -521,19 +530,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (
|
||||
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
|
||||
this.props.activeSpace?.canInvite(userId) ||
|
||||
this.props.activeSpace?.getMyMembership() === "join" ||
|
||||
this.props.activeSpace?.getJoinRule() === JoinRule.Public
|
||||
) {
|
||||
const spaceName = this.props.activeSpace.name;
|
||||
const canInvite = this.props.activeSpace?.canInvite(userId) ||
|
||||
this.props.activeSpace?.getJoinRule() === JoinRule.Public;
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ this.props.activeSpace.canInvite(userId) && <AccessibleTooltipButton
|
||||
{ canInvite && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
title={_t("Invite to %(spaceName)s", { spaceName })}
|
||||
>
|
||||
{ _t("Invite people") }
|
||||
</AccessibleTooltipButton> }
|
||||
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleTooltipButton
|
||||
{ this.props.activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore %(spaceName)s", { spaceName })}
|
||||
|
|
|
@ -35,6 +35,8 @@ import InviteReason from "../elements/InviteReason";
|
|||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
|
@ -339,8 +341,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
title = _t("Join the conversation with an account");
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
}
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
if (this.props.previewLoading) {
|
||||
|
|
|
@ -55,6 +55,8 @@ import { ListNotificationState } from "../../../stores/notifications/ListNotific
|
|||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
|
@ -675,7 +677,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
);
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
|
||||
addRoomButton = (
|
||||
<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
|
@ -687,6 +689,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
/>
|
||||
);
|
||||
} else if (this.props.addRoomContextMenu) {
|
||||
// We assume that shouldShowComponent() is checked by the context menu itself.
|
||||
addRoomButton = (
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { IContent, MatrixEvent, IEventRelation } from 'matrix-js-sdk/src/models/event';
|
||||
import { DebouncedFunc, throttle } from 'lodash';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -61,10 +61,10 @@ import RoomContext from '../../../contexts/RoomContext';
|
|||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
replyToEvent: MatrixEvent,
|
||||
replyInThread: boolean,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
relation?: IEventRelation,
|
||||
): void {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread);
|
||||
const replyContent = ReplyThread.makeReplyMixIn(replyToEvent);
|
||||
Object.assign(content, replyContent);
|
||||
|
||||
// Part of Replies fallback support - prepend the text we're sending
|
||||
|
@ -76,13 +76,20 @@ function addReplyToMessageContent(
|
|||
}
|
||||
content.body = nestedReply.body + content.body;
|
||||
}
|
||||
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...relation, // the composer can have a default
|
||||
...content['m.relates_to'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export function createMessageContent(
|
||||
model: EditorModel,
|
||||
replyToEvent: MatrixEvent,
|
||||
replyInThread: boolean,
|
||||
relation: IEventRelation,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
|
@ -106,7 +113,14 @@ export function createMessageContent(
|
|||
}
|
||||
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator);
|
||||
addReplyToMessageContent(content, replyToEvent, permalinkCreator);
|
||||
}
|
||||
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...relation,
|
||||
...content['m.relates_to'],
|
||||
};
|
||||
}
|
||||
|
||||
return content;
|
||||
|
@ -134,7 +148,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
|
|||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
replyInThread?: boolean;
|
||||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
disabled?: boolean;
|
||||
onChange?(model: EditorModel): void;
|
||||
|
@ -143,6 +157,8 @@ interface ISendMessageComposerProps extends MatrixClientProps {
|
|||
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private model: EditorModel = null;
|
||||
|
@ -162,12 +178,11 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
|
||||
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
|
||||
if (replyToEventChanged) {
|
||||
this.model.reset([]);
|
||||
}
|
||||
const replyingToThread = this.props.relation?.key === RelationType.Thread;
|
||||
const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;
|
||||
|
||||
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
|
||||
const threadChanged = replyingToThread && (differentEventTarget);
|
||||
if (threadChanged) {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model.reset(parts);
|
||||
|
@ -180,6 +195,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
if (this.editorRef.current?.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
const replyingToThread = this.props.relation?.key === RelationType.Thread;
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.Send:
|
||||
|
@ -201,7 +217,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const events =
|
||||
this.context.liveTimeline.getEvents()
|
||||
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
|
||||
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
|
||||
const editEvent = findEditableEvent({
|
||||
events,
|
||||
isForward: false,
|
||||
|
@ -279,7 +295,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.context.liveTimeline();
|
||||
const timeline = this.context.liveTimeline;
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
|
@ -393,8 +409,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
addReplyToMessageContent(
|
||||
content,
|
||||
replyToEvent,
|
||||
this.props.replyInThread,
|
||||
this.props.permalinkCreator,
|
||||
this.props.relation,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -441,7 +457,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
content = createMessageContent(
|
||||
model,
|
||||
replyToEvent,
|
||||
this.props.replyInThread,
|
||||
this.props.relation,
|
||||
this.props.permalinkCreator,
|
||||
);
|
||||
}
|
||||
|
@ -515,7 +531,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
|
||||
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
|
||||
if (this.props.replyInThread && !this.props.replyToEvent) {
|
||||
const replyingToThread = this.props.relation?.key === RelationType.Thread;
|
||||
if (replyingToThread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -218,17 +218,21 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_DevicesPanel_header">
|
||||
<div className="mx_DevicesPanel_deviceId">{ _t("ID") }</div>
|
||||
<div className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</div>
|
||||
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
||||
</div>
|
||||
</div>
|
||||
{ devices.map(this.renderDevice) }
|
||||
</div>
|
||||
<table className={classes}>
|
||||
<thead className="mx_DevicesPanel_header">
|
||||
<tr>
|
||||
<th className="mx_DevicesPanel_deviceId">{ _t("ID") }</th>
|
||||
<th className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</th>
|
||||
<th className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</th>
|
||||
<th className="mx_DevicesPanel_deviceButtons">
|
||||
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ devices.map(this.renderDevice) }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,23 +66,23 @@ export default class DevicesPanelEntry extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
<div className="mx_DevicesPanel_deviceId">
|
||||
<tr className={"mx_DevicesPanel_device" + myDeviceClass}>
|
||||
<td className="mx_DevicesPanel_deviceId">
|
||||
{ device.device_id }
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceName">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_deviceName">
|
||||
<EditableTextContainer initialValue={device.display_name}
|
||||
onSubmit={this.onDisplayNameChanged}
|
||||
placeholder={device.device_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_lastSeen">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_lastSeen">
|
||||
{ lastSeen }
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
</td>
|
||||
<td className="mx_DevicesPanel_deviceButtons">
|
||||
<StyledCheckbox onChange={this.onDeviceToggled} checked={this.props.selected} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,7 @@ import SpaceStore from "../../../stores/SpaceStore";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import RoomUpgradeWarningDialog, { IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog";
|
||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { arrayHasDiff } from "../../../utils/arrays";
|
||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||
|
@ -210,47 +209,70 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
|||
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||
const targetVersion = preferredRestrictionVersion;
|
||||
|
||||
const modal = Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||
roomId: room.roomId,
|
||||
targetVersion,
|
||||
description: _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite."),
|
||||
});
|
||||
|
||||
const [resp] = await modal.finished;
|
||||
if (!resp?.continue) return;
|
||||
|
||||
let warning: JSX.Element;
|
||||
const userId = cli.getUserId();
|
||||
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
|
||||
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
|
||||
if (unableToUpdateSomeParents) {
|
||||
const modal = Modal.createTrackedDialog<[boolean]>('Parent relink warning', '', QuestionDialog, {
|
||||
title: _t("Before you upgrade"),
|
||||
description: (
|
||||
<div>{ _t("This room is in some spaces you’re not an admin of. " +
|
||||
"In those spaces, the old room will still be shown, " +
|
||||
"but people will be prompted to join the new one.") }</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Upgrade anyway"),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [shouldUpgrade] = await modal.finished;
|
||||
if (!shouldUpgrade) return;
|
||||
warning = <b>
|
||||
{ _t("This room is in some spaces you’re not an admin of. " +
|
||||
"In those spaces, the old room will still be shown, " +
|
||||
"but people will be prompted to join the new one.") }
|
||||
</b>;
|
||||
}
|
||||
|
||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||
closeSettingsFn();
|
||||
// switch to the new room in the background
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
// open new settings on this tab
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||
roomId: room.roomId,
|
||||
targetVersion,
|
||||
description: <>
|
||||
{ _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite.") }
|
||||
{ warning }
|
||||
</>,
|
||||
doUpgrade: async (
|
||||
opts: IFinishedOpts,
|
||||
fn: (progressText: string, progress: number, total: number) => void,
|
||||
): Promise<void> => {
|
||||
const roomId = await upgradeRoom(
|
||||
room,
|
||||
targetVersion,
|
||||
opts.invite,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
progress => {
|
||||
const total = 2 + progress.updateSpacesTotal + progress.inviteUsersTotal;
|
||||
if (!progress.roomUpgraded) {
|
||||
fn(_t("Upgrading room"), 0, total);
|
||||
} else if (!progress.roomSynced) {
|
||||
fn(_t("Loading new room"), 1, total);
|
||||
} else if (progress.inviteUsersProgress < progress.inviteUsersTotal) {
|
||||
fn(_t("Sending invites... (%(progress)s out of %(count)s)", {
|
||||
progress: progress.inviteUsersProgress,
|
||||
count: progress.inviteUsersTotal,
|
||||
}), 2 + progress.inviteUsersProgress, total);
|
||||
} else if (progress.updateSpacesProgress < progress.updateSpacesTotal) {
|
||||
fn(_t("Updating spaces... (%(progress)s out of %(count)s)", {
|
||||
progress: progress.updateSpacesProgress,
|
||||
count: progress.updateSpacesTotal,
|
||||
}), 2 + progress.inviteUsersProgress + progress.updateSpacesProgress, total);
|
||||
}
|
||||
},
|
||||
);
|
||||
closeSettingsFn();
|
||||
|
||||
// switch to the new room in the background
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
// open new settings on this tab
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
|
@ -268,7 +268,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
|||
<AccessibleButton
|
||||
onClick={this.onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
disabled={this.state.continueDisabled || this.state.newPhoneNumberCode.length === 0}
|
||||
>
|
||||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ContextType } from 'react';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -38,9 +38,10 @@ interface IState {
|
|||
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
||||
export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
context: ContextType<typeof MatrixClientContext>;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false, // loaded async
|
||||
|
@ -89,6 +90,18 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||
</>;
|
||||
}
|
||||
|
||||
let leaveSection;
|
||||
if (room.getMyMembership() === "join") {
|
||||
leaveSection = <>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
|
@ -108,13 +121,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
|
||||
{ flairSection }
|
||||
{ urlPreviewSettings }
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ leaveSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { Entry } from "../dialogs/AddExistingToSpaceDialog";
|
||||
|
||||
enum Target {
|
||||
All = "All",
|
||||
Specific = "Specific",
|
||||
None = "None",
|
||||
}
|
||||
|
||||
interface ISpecificChildrenPickerProps {
|
||||
filterPlaceholder: string;
|
||||
rooms: Room[];
|
||||
selected: Set<Room>;
|
||||
onChange(selected: boolean, room: Room): void;
|
||||
}
|
||||
|
||||
const SpecificChildrenPicker = ({ filterPlaceholder, rooms, selected, onChange }: ISpecificChildrenPickerProps) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
if (!lcQuery) {
|
||||
return rooms;
|
||||
}
|
||||
|
||||
const matcher = new QueryMatcher<Room>(rooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
|
||||
return matcher.match(lcQuery);
|
||||
}, [rooms, lcQuery]);
|
||||
|
||||
return <div className="mx_SpaceChildrenPicker">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={filterPlaceholder}
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar>
|
||||
{ filteredRooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={(checked) => {
|
||||
onChange(checked, room);
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
{ filteredRooms.length < 1 ? <span className="mx_SpaceChildrenPicker_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
spaceChildren: Room[];
|
||||
selected: Set<Room>;
|
||||
noneLabel?: string;
|
||||
allLabel: string;
|
||||
specificLabel: string;
|
||||
onChange(rooms: Room[]): void;
|
||||
}
|
||||
|
||||
const SpaceChildrenPicker = ({
|
||||
space,
|
||||
spaceChildren,
|
||||
selected,
|
||||
onChange,
|
||||
noneLabel,
|
||||
allLabel,
|
||||
specificLabel,
|
||||
}: IProps) => {
|
||||
const [state, setState] = useState<string>(noneLabel ? Target.None : Target.All);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === Target.All) {
|
||||
onChange(spaceChildren);
|
||||
} else {
|
||||
onChange([]);
|
||||
}
|
||||
}, [onChange, state, spaceChildren]);
|
||||
|
||||
return <>
|
||||
<div className="mx_SpaceChildrenPicker">
|
||||
<StyledRadioGroup
|
||||
name="roomsToLeave"
|
||||
value={state}
|
||||
onChange={setState}
|
||||
definitions={[
|
||||
{
|
||||
value: Target.None,
|
||||
label: noneLabel,
|
||||
}, {
|
||||
value: Target.All,
|
||||
label: allLabel,
|
||||
}, {
|
||||
value: Target.Specific,
|
||||
label: specificLabel,
|
||||
},
|
||||
].filter(d => d.label)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ state === Target.Specific && (
|
||||
<SpecificChildrenPicker
|
||||
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
|
||||
rooms={spaceChildren}
|
||||
selected={selected}
|
||||
onChange={(isSelected: boolean, room: Room) => {
|
||||
if (isSelected) {
|
||||
onChange([room, ...selected]);
|
||||
} else {
|
||||
onChange([...selected].filter(r => r !== room));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
</>;
|
||||
};
|
||||
|
||||
export default SpaceChildrenPicker;
|
|
@ -17,9 +17,8 @@ limitations under the License.
|
|||
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
@ -215,7 +214,6 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
|||
};
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
|
@ -239,13 +237,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// validate the space alias field but do not require it
|
||||
const aliasLocalpart = alias.substring(1, alias.length - cli.getDomain().length - 1);
|
||||
if (visibility === Visibility.Public && aliasLocalpart &&
|
||||
(await spaceAliasField.current.validate({ allowEmpty: true })) === false
|
||||
) {
|
||||
if (visibility === Visibility.Public && !(await spaceAliasField.current.validate({ allowEmpty: false }))) {
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
spaceAliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
@ -254,7 +248,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
await createSpace(
|
||||
name,
|
||||
visibility === Visibility.Public,
|
||||
aliasLocalpart ? alias : undefined,
|
||||
alias,
|
||||
topic,
|
||||
avatar,
|
||||
);
|
||||
|
@ -361,9 +355,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
||||
managed={false}
|
||||
>
|
||||
<FocusLock returnFocus={true}>
|
||||
{ body }
|
||||
</FocusLock>
|
||||
{ body }
|
||||
</ContextMenu>;
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import { copyPlaintext } from "../../../utils/strings";
|
|||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { showRoomInviteDialog } from "../../../RoomInvite";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -51,16 +53,18 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
|||
<h3>{ _t("Share invite link") }</h3>
|
||||
<span>{ copiedText }</span>
|
||||
</AccessibleButton>
|
||||
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
if (onFinished) onFinished();
|
||||
showRoomInviteDialog(space.roomId);
|
||||
}}
|
||||
>
|
||||
<h3>{ _t("Invite people") }</h3>
|
||||
<span>{ _t("Invite with email or username") }</span>
|
||||
</AccessibleButton> : null }
|
||||
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)
|
||||
? <AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
if (onFinished) onFinished();
|
||||
showRoomInviteDialog(space.roomId);
|
||||
}}
|
||||
>
|
||||
<h3>{ _t("Invite people") }</h3>
|
||||
<span>{ _t("Invite with email or username") }</span>
|
||||
</AccessibleButton>
|
||||
: null }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -121,24 +121,24 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
let confirm;
|
||||
if (this.state.pending || this.state.cancelling) {
|
||||
if (this.state.pending && this.props.isSelf) {
|
||||
let text;
|
||||
// device shouldn't be null in this situation but it can be, eg. if the device is
|
||||
// logged out during verification
|
||||
if (this.props.device) {
|
||||
text = _t("Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…", {
|
||||
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
|
||||
deviceId: this.props.device ? this.props.device.deviceId : '',
|
||||
});
|
||||
} else {
|
||||
text = _t("Waiting for you to verify on your other session…");
|
||||
}
|
||||
confirm = <p>{ text }</p>;
|
||||
} else if (this.state.pending || this.state.cancelling) {
|
||||
let text;
|
||||
if (this.state.pending) {
|
||||
if (this.props.isSelf) {
|
||||
// device shouldn't be null in this situation but it can be, eg. if the device is
|
||||
// logged out during verification
|
||||
if (this.props.device) {
|
||||
text = _t("Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…", {
|
||||
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
|
||||
deviceId: this.props.device ? this.props.device.deviceId : '',
|
||||
});
|
||||
} else {
|
||||
text = _t("Waiting for your other session to verify…");
|
||||
}
|
||||
} else {
|
||||
const { displayName } = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", { displayName });
|
||||
}
|
||||
const { displayName } = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", { displayName });
|
||||
} else {
|
||||
text = _t("Cancelling…");
|
||||
}
|
||||
|
|
|
@ -21,7 +21,10 @@ import { Layout } from "../settings/Layout";
|
|||
|
||||
export enum TimelineRenderingType {
|
||||
Room,
|
||||
Thread
|
||||
Thread,
|
||||
ThreadsList,
|
||||
File,
|
||||
Notification,
|
||||
}
|
||||
|
||||
const RoomContext = createContext<IRoomState>({
|
||||
|
|
|
@ -219,7 +219,19 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
|
||||
let roomId;
|
||||
return client.createRoom(createOpts).finally(function() {
|
||||
return client.createRoom(createOpts).catch(function(err) {
|
||||
// NB This checks for the Synapse-specific error condition of a room creation
|
||||
// having been denied because the requesting user wanted to publish the room,
|
||||
// but the server denies them that permission (via room_list_publication_rules).
|
||||
// The check below responds by retrying without publishing the room.
|
||||
if (err.httpStatus === 403 && err.errcode === "M_UNKNOWN" && err.data.error === "Not allowed to publish room") {
|
||||
console.warn("Failed to publish room, try again without publishing it");
|
||||
createOpts.visibility = Visibility.Private;
|
||||
return client.createRoom(createOpts);
|
||||
} else {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}).finally(function() {
|
||||
if (modal) modal.close();
|
||||
}).then(function(res) {
|
||||
roomId = res.room_id;
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Dev note: this customisation point is heavily inspired by UIFeature flags, though
|
||||
// with an intention of being used for more complex switching on whether or not a feature
|
||||
// should be shown.
|
||||
|
||||
// Populate this class with the details of your customisations when copying it.
|
||||
|
||||
import { UIComponent } from "../settings/UIFeature";
|
||||
|
||||
/**
|
||||
* Determines whether or not the active MatrixClient user should be able to use
|
||||
* the given UI component. If shown, the user might still not be able to use the
|
||||
* component depending on their contextual permissions. For example, invite options
|
||||
* might be shown to the user but they won't have permission to invite users to
|
||||
* the current room: the button will appear disabled.
|
||||
* @param {UIComponent} component The component to check visibility for.
|
||||
* @returns {boolean} True (default) if the user is able to see the component, false
|
||||
* otherwise.
|
||||
*/
|
||||
function shouldShowComponent(component: UIComponent): boolean {
|
||||
return true; // default to visible
|
||||
}
|
||||
|
||||
// This interface summarises all available customisation points and also marks
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface IComponentVisibilityCustomisations {
|
||||
shouldShowComponent?: typeof shouldShowComponent;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up the interface above.
|
||||
export const ComponentVisibilityCustomisations: IComponentVisibilityCustomisations = {
|
||||
// while we don't specify the functions here, their defaults are described
|
||||
// in their pseudo-implementations above.
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
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 { UIComponent } from "../../settings/UIFeature";
|
||||
import { ComponentVisibilityCustomisations } from "../ComponentVisibility";
|
||||
|
||||
export function shouldShowComponent(component: UIComponent): boolean {
|
||||
return ComponentVisibilityCustomisations.shouldShowComponent?.(component) ?? true;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../actions";
|
||||
import dis from '../dispatcher';
|
||||
import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload";
|
||||
|
||||
export const dispatchShowThreadEvent = (event: MatrixEvent) => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.ThreadView,
|
||||
refireParams: {
|
||||
event,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const dispatchShowThreadsPanelEvent = () => {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.ThreadPanel,
|
||||
});
|
||||
};
|
||||
|
|
@ -20,8 +20,9 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { walkDOMDepthFirst } from "./dom";
|
||||
import { checkBlockNode } from "../HtmlUtils";
|
||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||
import { PartCreator, Type } from "./parts";
|
||||
import { Part, PartCreator, Type } from "./parts";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { textToHtmlRainbow } from "../utils/colour";
|
||||
|
||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||
const ATROOM = "@room";
|
||||
|
@ -213,12 +214,12 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
|
|||
}
|
||||
}
|
||||
|
||||
function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessage: boolean) {
|
||||
function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessage: boolean): Part[] {
|
||||
// no nodes from parsing here should be inserted in the document,
|
||||
// as scripts in event handlers, etc would be executed then.
|
||||
// we're only taking text, so that is fine
|
||||
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
|
||||
const parts = [];
|
||||
const parts: Part[] = [];
|
||||
let lastNode;
|
||||
let inQuote = isQuotedMessage;
|
||||
const state: IState = {
|
||||
|
@ -233,7 +234,7 @@ function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessag
|
|||
inQuote = true;
|
||||
}
|
||||
|
||||
const newParts = [];
|
||||
const newParts: Part[] = [];
|
||||
if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
|
||||
newParts.push(partCreator.newline());
|
||||
}
|
||||
|
@ -288,7 +289,7 @@ function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessag
|
|||
return parts;
|
||||
}
|
||||
|
||||
export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage?: boolean) {
|
||||
export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage?: boolean): Part[] {
|
||||
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
|
||||
return lines.reduce((parts, line, i) => {
|
||||
if (isQuotedMessage) {
|
||||
|
@ -300,19 +301,31 @@ export function parsePlainTextMessage(body: string, partCreator: PartCreator, is
|
|||
parts.push(partCreator.newline());
|
||||
}
|
||||
return parts;
|
||||
}, []);
|
||||
}, [] as Part[]);
|
||||
}
|
||||
|
||||
export function parseEvent(event: MatrixEvent, partCreator: PartCreator, { isQuotedMessage = false } = {}) {
|
||||
const content = event.getContent();
|
||||
let parts;
|
||||
let parts: Part[];
|
||||
const isEmote = content.msgtype === "m.emote";
|
||||
let isRainbow = false;
|
||||
|
||||
if (content.format === "org.matrix.custom.html") {
|
||||
parts = parseHtmlMessage(content.formatted_body || "", partCreator, isQuotedMessage);
|
||||
if (content.body && content.formatted_body && textToHtmlRainbow(content.body) === content.formatted_body) {
|
||||
isRainbow = true;
|
||||
}
|
||||
} else {
|
||||
parts = parsePlainTextMessage(content.body || "", partCreator, isQuotedMessage);
|
||||
}
|
||||
if (content.msgtype === "m.emote") {
|
||||
|
||||
if (isEmote && isRainbow) {
|
||||
parts.unshift(partCreator.plain("/rainbowme "));
|
||||
} else if (isRainbow) {
|
||||
parts.unshift(partCreator.plain("/rainbow "));
|
||||
} else if (isEmote) {
|
||||
parts.unshift(partCreator.plain("/me "));
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,8 @@ export default class EditorModel {
|
|||
}
|
||||
|
||||
public clone(): EditorModel {
|
||||
return new EditorModel(this._parts, this._partCreator, this.updateCallback);
|
||||
const clonedParts = this.parts.map(p => this.partCreator.deserializePart(p.serialize()));
|
||||
return new EditorModel(clonedParts, this._partCreator, this.updateCallback);
|
||||
}
|
||||
|
||||
private insertPart(index: number, part: Part): void {
|
||||
|
|
|
@ -74,7 +74,7 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
|||
// If there's ever a gap in shortcode coverage, we fudge it by
|
||||
// filling it in with the emoji's CLDR annotation
|
||||
const shortcodeData = SHORTCODES[emojiData.hexcode] ??
|
||||
[emojiData.annotation.toLowerCase().replace(/ /g, "_")];
|
||||
[emojiData.annotation.toLowerCase().replace(/\W+/g, "_")];
|
||||
|
||||
const emoji: IEmoji = {
|
||||
...emojiData,
|
||||
|
|
|
@ -2901,7 +2901,7 @@
|
|||
"e.g. my-space": "např. můj-prostor",
|
||||
"Silence call": "Ztlumit zvonění",
|
||||
"Sound on": "Zvuk zapnutý",
|
||||
"Show all rooms in Home": "Zobrazit všechny místnosti na domácí obrazovce",
|
||||
"Show all rooms in Home": "Zobrazit všechny místnosti na úvodní obrazovce",
|
||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Prototyp Nahlášování moderátorům. V místnostech, které podporují moderování, vám tlačítko `nahlásit` umožní nahlásit zneužití moderátorům místnosti",
|
||||
"%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s změnil(a) <a>připnuté zprávy</a> v místnosti.",
|
||||
"%(senderName)s kicked %(targetName)s": "%(senderName)s vykopl(a) uživatele %(targetName)s",
|
||||
|
@ -3056,7 +3056,7 @@
|
|||
"Want to add a new space instead?": "Chcete místo toho přidat nový prostor?",
|
||||
"Decrypting": "Dešifrování",
|
||||
"Show all rooms": "Zobrazit všechny místnosti",
|
||||
"All rooms you're in will appear in Home.": "Všechny místnosti, ve kterých se nacházíte, se zobrazí na domovské obrazovce.",
|
||||
"All rooms you're in will appear in Home.": "Všechny místnosti, ve kterých se nacházíte, se zobrazí na úvodní obrazovce.",
|
||||
"Send pseudonymous analytics data": "Odeslat pseudonymní analytická data",
|
||||
"Missed call": "Zmeškaný hovor",
|
||||
"Call declined": "Hovor odmítnut",
|
||||
|
@ -3158,9 +3158,51 @@
|
|||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Tato místnost se nachází v některých prostorech, jejichž nejste správcem. V těchto prostorech bude stará místnost stále zobrazena, ale lidé budou vyzváni, aby se připojili k nové místnosti.",
|
||||
"Before you upgrade": "Než provedete aktualizaci",
|
||||
"To join a space you'll need an invite.": "Pro připojení k prostoru potřebujete pozvánku.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Můžete také vytvořit prostory ze <a>skupin</a>.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Prostory můžete vytvořit také ze <a>skupin</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Dočasně zobrazit skupiny místo prostorů pro tuto relaci. Podpora bude v blízké budoucnosti odstraněna. Toto provede přenačtení Elementu.",
|
||||
"Display Communities instead of Spaces": "Zobrazit skupiny místo prostorů",
|
||||
"Joining space …": "Připojování k prostoru…",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagoval(a) na %(content)s"
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagoval(a) na %(content)s",
|
||||
"Would you like to leave the rooms in this space?": "Chcete odejít z místností v tomto prostoru?",
|
||||
"You are about to leave <spaceName/>.": "Odcházíte z <spaceName/>.",
|
||||
"Leave some rooms": "Odejít z některých místností",
|
||||
"Don't leave any rooms": "Neodcházet z žádné místnosti",
|
||||
"Leave all rooms": "Odejít ze všech místností",
|
||||
"Expand quotes │ ⇧+click": "Rozbalit uvozovky │ ⇧+kliknutí",
|
||||
"Collapse quotes │ ⇧+click": "Sbalit uvozovky │ ⇧+kliknutí",
|
||||
"Include Attachments": "Zahrnout přílohy",
|
||||
"Size Limit": "Omezení velikosti",
|
||||
"Format": "Formát",
|
||||
"Select from the options below to export chats from your timeline": "Vyberte jednu z níže uvedených možností pro export chatů z časové osy",
|
||||
"Export Chat": "Exportovat chat",
|
||||
"Exporting your data": "Exportování dat",
|
||||
"Stop": "Zastavit",
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Opravdu chcete ukončit export dat? Pokud ano, budete muset začít znovu.",
|
||||
"Your export was successful. Find it in your Downloads folder.": "Váš export proběhl úspěšně. Najdete jej ve složce pro stažené soubory.",
|
||||
"The export was cancelled successfully": "Export byl úspěšně zrušen",
|
||||
"Export Successful": "Export proběhl úspěšně",
|
||||
"MB": "MB",
|
||||
"Number of messages": "Počet zpráv",
|
||||
"Number of messages can only be a number between %(min)s and %(max)s": "Počet zpráv může být pouze číslo mezi %(min)s a %(max)s",
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB": "Velikost může být pouze číslo mezi %(min)s MB a %(max)s MB",
|
||||
"Enter a number between %(min)s and %(max)s": "Zadejte číslo mezi %(min)s a %(max)s",
|
||||
"In reply to <a>this message</a>": "V odpovědi na <a>tuto zprávu</a>",
|
||||
"Export chat": "Exportovat chat",
|
||||
"File Attached": "Přiložený soubor",
|
||||
"Error fetching file": "Chyba při načítání souboru",
|
||||
"Topic: %(topic)s": "Téma: %(topic)s",
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "Toto je začátek exportu <roomName/>. Exportováno pomocí <exporterDetails/> v %(exportDate)s.",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s vytvořil(a) tuto místnost.",
|
||||
"Media omitted - file size limit exceeded": "Vynechaná média - překročen limit velikosti souboru",
|
||||
"Media omitted": "Vynechaná média",
|
||||
"Current Timeline": "Aktuální časová osa",
|
||||
"Specify a number of messages": "Zadejte počet zpráv",
|
||||
"From the beginning": "Od začátku",
|
||||
"Plain Text": "Prostý text",
|
||||
"JSON": "JSON",
|
||||
"HTML": "HTML",
|
||||
"Are you sure you want to exit during this export?": "Opravdu chcete skončit během tohoto exportu?",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s poslal(a) nálepku.",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s změnil(a) avatar místnosti.",
|
||||
"%(date)s at %(time)s": "%(date)s v %(time)s"
|
||||
}
|
||||
|
|
|
@ -3134,5 +3134,26 @@
|
|||
"Joining space …": "Space beitreten…",
|
||||
"To join a space you'll need an invite.": "Um einem Space beizutreten brauchst du eine Einladung.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "In dieser Sitzung temporär Communities statt Spaces anzeigen. Unterstützung hierfür wird in naher Zukunft entfernt. Dies wird Element neu laden.",
|
||||
"Display Communities instead of Spaces": "Communities statt Spaces anzeigen"
|
||||
"Display Communities instead of Spaces": "Communities statt Spaces anzeigen",
|
||||
"To join this Space, hide communities in your <a>preferences</a>": "<a>Deaktiviere Communities in den Einstellungen</a>, um diesen Space beizutreten.",
|
||||
"To view this Space, hide communities in your <a>preferences</a>": "<a>Deaktiviere Communities in den Einstellungen</a>, um diesen Space anzuzeigen.",
|
||||
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "Um %(communityName)s beizutreten, <a>aktiviere Communities in den Einstellungen</a>",
|
||||
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "Um %(communityName)s anzuzeigen, <a>aktiviere Communities in den Einstellungen</a>",
|
||||
"Private community": "Private Community",
|
||||
"Public community": "Öffentliche Community",
|
||||
"You are about to leave <spaceName/>.": "Du bist dabei, <spaceName/> zu verlassen.",
|
||||
"Leave some rooms": "Zu verlassende Räume auswählen",
|
||||
"Leave all rooms": "Alle Räume verlassen",
|
||||
"Don't leave any rooms": "Räume nicht verlassen",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s hat mit %(content)s reagiert",
|
||||
"Some encryption parameters have been changed.": "Einige Verschlüsselungsoptionen wurden geändert.",
|
||||
"Message": "Nachricht",
|
||||
"Message didn't send. Click for info.": "Nachricht nicht gesendet. Klicke für Details.",
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "<a>Erstelle einen neuen Raum für deine Konversation</a>, um diese Probleme zu umgehen.",
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Es ist nicht sinnvoll, verschlüsselte Räume öffentlich zu machen.</b> Da jeder den Raum betreten kann, kann auch jeder Nachrichten lesen, was die Verschlüsselung sinnlos macht. Außerdem wird das Senden und Empfangen von Nachrichten langsamer werden.",
|
||||
"Select the roles required to change various parts of the space": "Wähle, von wem folgende Aktionen ausgeführt werden können",
|
||||
"Upgrade anyway": "Trotzdem upgraden",
|
||||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Dieser Raum ist in einigen Spaces, in denen du nicht Admin bist. Daher wird dort noch der alte Raum angezeigt, die Leute werden aber auf den neuen Raum hingewiesen.",
|
||||
"Before you upgrade": "Bevor du upgradest",
|
||||
"You can also make Spaces from <a>communities</a>.": "Du kannst Spaces auch aus <a>Communities</a> erstellen."
|
||||
}
|
||||
|
|
|
@ -823,6 +823,7 @@
|
|||
"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",
|
||||
"Send pseudonymous analytics data": "Send pseudonymous analytics data",
|
||||
"Polls (under active development)": "Polls (under active development)",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||
"Don't send read receipts": "Don't send read receipts",
|
||||
|
@ -947,8 +948,8 @@
|
|||
"Verify this session by confirming the following number appears on its screen.": "Verify this session by confirming the following number appears on its screen.",
|
||||
"Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.",
|
||||
"Unable to find a supported verification method.": "Unable to find a supported verification method.",
|
||||
"Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…",
|
||||
"Waiting for your other session to verify…": "Waiting for your other session to verify…",
|
||||
"Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…": "Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…",
|
||||
"Waiting for you to verify on your other session…": "Waiting for you to verify on your other session…",
|
||||
"Waiting for %(displayName)s to verify…": "Waiting for %(displayName)s to verify…",
|
||||
"Cancelling…": "Cancelling…",
|
||||
"They match": "They match",
|
||||
|
@ -1027,6 +1028,8 @@
|
|||
"Upload": "Upload",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"No results": "No results",
|
||||
"Search %(spaceName)s": "Search %(spaceName)s",
|
||||
"Please enter a name for the space": "Please enter a name for the space",
|
||||
"Spaces are a new feature.": "Spaces are a new feature.",
|
||||
"Spaces feedback": "Spaces feedback",
|
||||
|
@ -1166,10 +1169,14 @@
|
|||
"Anyone in <spaceName/> can find and join. You can select other spaces too.": "Anyone in <spaceName/> can find and join. You can select other spaces too.",
|
||||
"Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
|
||||
"Space members": "Space members",
|
||||
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
|
||||
"Before you upgrade": "Before you upgrade",
|
||||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.",
|
||||
"Upgrade anyway": "Upgrade anyway",
|
||||
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
|
||||
"Upgrading room": "Upgrading room",
|
||||
"Loading new room": "Loading new room",
|
||||
"Sending invites... (%(progress)s out of %(count)s)|other": "Sending invites... (%(progress)s out of %(count)s)",
|
||||
"Sending invites... (%(progress)s out of %(count)s)|one": "Sending invite...",
|
||||
"Updating spaces... (%(progress)s out of %(count)s)|other": "Updating spaces... (%(progress)s out of %(count)s)",
|
||||
"Updating spaces... (%(progress)s out of %(count)s)|one": "Updating space...",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
|
@ -1575,6 +1582,7 @@
|
|||
"Emoji picker": "Emoji picker",
|
||||
"Add emoji": "Add emoji",
|
||||
"Upload file": "Upload file",
|
||||
"Create poll": "Create poll",
|
||||
"Reply to encrypted thread…": "Reply to encrypted thread…",
|
||||
"Reply to thread…": "Reply to thread…",
|
||||
"Send an encrypted reply…": "Send an encrypted reply…",
|
||||
|
@ -1803,7 +1811,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.",
|
||||
"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",
|
||||
"Accept on your other login…": "Accept on your other login…",
|
||||
"To proceed, please accept the verification request on your other login.": "To proceed, please accept the verification request on your other login.",
|
||||
"Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…",
|
||||
"Accepting…": "Accepting…",
|
||||
"Start Verification": "Start Verification",
|
||||
|
@ -1823,6 +1831,7 @@
|
|||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
"Threads": "Threads",
|
||||
"Room Info": "Room Info",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
||||
|
@ -1858,8 +1867,11 @@
|
|||
"Demote": "Demote",
|
||||
"Disinvite": "Disinvite",
|
||||
"Kick": "Kick",
|
||||
"Disinvite this user?": "Disinvite this user?",
|
||||
"Kick this user?": "Kick this user?",
|
||||
"Disinvite from %(roomName)s": "Disinvite from %(roomName)s",
|
||||
"Kick from %(roomName)s": "Kick from %(roomName)s",
|
||||
"Kick them from everything I'm able to": "Kick them from everything I'm able to",
|
||||
"Kick them from specific things I'm able to": "Kick them from specific things I'm able to",
|
||||
"They'll still be able to access whatever you're not an admin of.": "They'll still be able to access whatever you're not an admin of.",
|
||||
"Failed to kick": "Failed to kick",
|
||||
"No recent messages by %(user)s found": "No recent messages by %(user)s found",
|
||||
"Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.",
|
||||
|
@ -1871,8 +1883,13 @@
|
|||
"Remove %(count)s messages|one": "Remove 1 message",
|
||||
"Remove recent messages": "Remove recent messages",
|
||||
"Ban": "Ban",
|
||||
"Unban this user?": "Unban this user?",
|
||||
"Ban this user?": "Ban this user?",
|
||||
"Unban from %(roomName)s": "Unban from %(roomName)s",
|
||||
"Ban from %(roomName)s": "Ban from %(roomName)s",
|
||||
"Unban them from everything I'm able to": "Unban them from everything I'm able to",
|
||||
"Ban them from everything I'm able to": "Ban them from everything I'm able to",
|
||||
"Unban them from specific things I'm able to": "Unban them from specific things I'm able to",
|
||||
"Ban them from specific things I'm able to": "Ban them from specific things I'm able to",
|
||||
"They won't be able to access whatever you're not an admin of.": "They won't be able to access whatever you're not an admin of.",
|
||||
"Failed to ban user": "Failed to ban user",
|
||||
"Failed to mute user": "Failed to mute user",
|
||||
"Unmute": "Unmute",
|
||||
|
@ -1942,6 +1959,7 @@
|
|||
"Saturday": "Saturday",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Downloading": "Downloading",
|
||||
"Decrypting": "Decrypting",
|
||||
"Download": "Download",
|
||||
"View Source": "View Source",
|
||||
|
@ -2063,7 +2081,6 @@
|
|||
"Application window": "Application window",
|
||||
"Share content": "Share content",
|
||||
"Join": "Join",
|
||||
"No results": "No results",
|
||||
"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",
|
||||
"expand": "expand",
|
||||
|
@ -2280,6 +2297,8 @@
|
|||
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
|
||||
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
|
||||
"Failed to migrate community": "Failed to migrate community",
|
||||
"Fetching data...": "Fetching data...",
|
||||
"Creating Space...": "Creating Space...",
|
||||
"Create Space from community": "Create Space from community",
|
||||
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
|
||||
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
||||
|
@ -2462,16 +2481,15 @@
|
|||
"Clear cache and resync": "Clear cache and resync",
|
||||
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
|
||||
"Updating %(brand)s": "Updating %(brand)s",
|
||||
"Don't leave any rooms": "Don't leave any rooms",
|
||||
"Leave all rooms": "Leave all rooms",
|
||||
"Leave some rooms": "Leave some rooms",
|
||||
"Search %(spaceName)s": "Search %(spaceName)s",
|
||||
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
|
||||
"You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
|
||||
"You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
|
||||
"Leave %(spaceName)s": "Leave %(spaceName)s",
|
||||
"You are about to leave <spaceName/>.": "You are about to leave <spaceName/>.",
|
||||
"Would you like to leave the rooms in this space?": "Would you like to leave the rooms in this space?",
|
||||
"Don't leave any rooms": "Don't leave any rooms",
|
||||
"Leave all rooms": "Leave all rooms",
|
||||
"Leave some rooms": "Leave some rooms",
|
||||
"Leave space": "Leave space",
|
||||
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
|
||||
"Start using Key Backup": "Start using Key Backup",
|
||||
|
@ -2958,6 +2976,11 @@
|
|||
"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?",
|
||||
"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.",
|
||||
"My threads": "My threads",
|
||||
"Shows all threads you’ve participated in": "Shows all threads you’ve participated in",
|
||||
"All threads": "All threads",
|
||||
"Shows all threads from current room": "Shows all threads from current room",
|
||||
"Show:": "Show:",
|
||||
"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 was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
|
@ -2982,8 +3005,11 @@
|
|||
"Could not load user profile": "Could not load user profile",
|
||||
"Decrypted event source": "Decrypted event source",
|
||||
"Original event source": "Original event source",
|
||||
"Unable to verify this login": "Unable to verify this login",
|
||||
"Verify this login": "Verify this login",
|
||||
"Session verified": "Session verified",
|
||||
"Really reset verification keys?": "Really reset verification keys?",
|
||||
"Skip verification for now": "Skip verification for now",
|
||||
"Failed to send email": "Failed to send email",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
"A new password must be entered.": "A new password must be entered.",
|
||||
|
@ -3037,13 +3063,18 @@
|
|||
"Create account": "Create account",
|
||||
"Host account on": "Host account on",
|
||||
"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": "Use Security Key",
|
||||
"Use another login": "Use another login",
|
||||
"It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.": "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
|
||||
"Proceed with reset": "Proceed with reset",
|
||||
"Verify with Security Key or Phrase": "Verify with Security Key or Phrase",
|
||||
"Verify with Security Key": "Verify with Security Key",
|
||||
"Verify with another login": "Verify with another login",
|
||||
"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. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
|
||||
"Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.",
|
||||
"I'll verify later": "I'll verify later",
|
||||
"Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.",
|
||||
"Please only proceed if you're sure you've lost all of your other devices and your security key.": "Please only proceed if you're sure you've lost all of your other devices and your security key.",
|
||||
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
|
||||
"Incorrect password": "Incorrect password",
|
||||
"Failed to re-authenticate": "Failed to re-authenticate",
|
||||
|
|
|
@ -3099,5 +3099,62 @@
|
|||
"What kind of Space do you want to create?": "Kian aron volas vi krei?",
|
||||
"All rooms you're in will appear in Home.": "Ĉiuj ĉambroj, kie vi estas, aperos en la ĉefpaĝo.",
|
||||
"Show all rooms in Home": "Montri ĉiujn ĉambrojn en ĉefpaĝo",
|
||||
"%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksis <a>mesaĝon</a> al ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>."
|
||||
"%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksis <a>mesaĝon</a> al ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>.",
|
||||
"To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.": "Por eviti tiujn problemojn, kreu <a>novan ĉifritan ĉambron</a> por la planata interparolo.",
|
||||
"<b>It's not recommended to add encryption to public rooms.</b>Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Ne rekomendate estas aldoni ĉifradon al publikaj ĉambroj.</b> Ĉiu ajn povas trovi publikajn ĉambrojn kaj aliĝi, do ĉiu ajn povas legi ties mesaĝojn. Vi havos neniujn avantaĝojn de ĉifrado, kaj vi ne povos ĝin malŝalti pli poste. Ĉifrado en publikaj ĉambroj malrapidigos ricevadon kaj sendadon de mesaĝoj.",
|
||||
"Are you sure you want to add encryption to this public room?": "Ĉu vi certas, ke vi volas aldoni ĉifradon al ĉi tiu publika ĉambro?",
|
||||
"Select the roles required to change various parts of the space": "Elekti rolojn bezonatajn por ŝanĝado de diversaj partoj de la aro",
|
||||
"Change description": "Ŝanĝi priskribon",
|
||||
"Change main address for the space": "Ŝanĝi ĉefadreson de aro",
|
||||
"Change space name": "Ŝanĝi nomon de aro",
|
||||
"Change space avatar": "Ŝanĝi bildon de aro",
|
||||
"Upgrade anyway": "Tamen gradaltigi",
|
||||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Ĉi tiu ĉambro estas en iuj aroj, kiujn vi ne administras. En tiuj aroj, la malnova ĉambro aperos, sed tie oni ricevos avizon aliĝi al la nova.",
|
||||
"Before you upgrade": "Antaŭ ol vi gradaltigos",
|
||||
"Anyone in <spaceName/> can find and join. You can select other spaces too.": "Ĉiu en <spaceName/> povas trovi kaj aliĝi. Vi povas elekti ankaŭ aliajn arojn.",
|
||||
"Currently, %(count)s spaces have access|one": "Nun, aro povas aliri",
|
||||
"& %(count)s more|one": "kaj %(count)s pli",
|
||||
"To join a space you'll need an invite.": "Por aliĝi al aro, vi bezonas inviton.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Vi ankaŭ povas krei Arojn el <a>komunumoj</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Provizore montri komunumojn anstataŭ arojn por tiu ĉi salutaĵo. Subteno de tio ĉi baldaŭ malaperos. Ĉi tio re-enlegos Elementon.",
|
||||
"Display Communities instead of Spaces": "Montri komunumojn anstataŭ arojn",
|
||||
"Autoplay videos": "Memage ludi filmojn",
|
||||
"Autoplay GIFs": "Memage ludi GIF-ojn",
|
||||
"Multiple integration managers (requires manual setup)": "Pluraj kunigiloj (bezonas permanan agordon)",
|
||||
"Threaded messaging": "Mesaĝaj fadenoj",
|
||||
"%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s malfiksis mesaĝon de ĉi tiu ĉambro. Vidu ĉiujn fiksitajn mesaĝojn.",
|
||||
"%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s malfiksis <a>mesaĝon</a> de ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>.",
|
||||
"%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fiksis mesaĝon al ĉi tiu ĉambro. Vidu ĉiujn fiksitajn mesaĝojn.",
|
||||
"To join this Space, hide communities in your <a>preferences</a>": "Por aliĝi al ĉi tiu aro, kaŝu komunumojn per viaj <a>agordoj</a>",
|
||||
"To view this Space, hide communities in your <a>preferences</a>": "Por vidi ĉi tiun aron, kaŝu komunumojn per viaj <a>agordoj</a>",
|
||||
"Rooms and spaces": "Ĉambroj kaj aroj",
|
||||
"Results": "Rezultoj",
|
||||
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "Por aliĝi al %(communityName)s, ŝaltu komunumojn en viaj <a>agordoj</a>",
|
||||
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "Por vidi komunumon %(communityName)s, ŝaltu komunumojn en viaj <a>agordoj</a>",
|
||||
"Private community": "Privata komunumo",
|
||||
"Public community": "Publika komunumo",
|
||||
"Forward": "Plusendi",
|
||||
"Would you like to leave the rooms in this space?": "Ĉu vi volus foriri de la ĉambroj en ĉi tiu aro?",
|
||||
"You are about to leave <spaceName/>.": "Vi foriros de <spaceName/>.",
|
||||
"Leave some rooms": "Foriri de iuj ĉambroj",
|
||||
"Leave all rooms": "Foriri de ĉiuj ĉambroj",
|
||||
"Don't leave any rooms": "Foriru de neniuj ĉambroj",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagis per %(content)s",
|
||||
"Thread": "Fadeno",
|
||||
"Some encryption parameters have been changed.": "Ŝanĝiĝis iuj parametroj de ĉifrado.",
|
||||
"Role in <RoomName/>": "Rolo en <RoomName/>",
|
||||
"Message": "Mesaĝo",
|
||||
"Show threads": "Montri fadenojn",
|
||||
"Joining space …": "Aliĝante al aro…",
|
||||
"Explore %(spaceName)s": "Esplori aron %(spaceName)s",
|
||||
"Message didn't send. Click for info.": "Mesaĝo ne sendiĝis. Klaku por akiri informojn.",
|
||||
"Send a sticker": "Sendi glumarkon",
|
||||
"Reply to thread…": "Respondi al fadeno…",
|
||||
"Reply to encrypted thread…": "Respondi al ĉifrita fadeno…",
|
||||
"Add emoji": "Aldoni bildosignon",
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "Por eviti ĉi tiujn problemojn, kreu <a>novan publikan ĉambron</a> por la dezirata interparolo.",
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Publikigo de ĉifrataj ĉambroj estas malrekomendata.</b> Ĝi implicas, ke ĉiu povos trovi la ĉambron kaj aliĝi al ĝi, kaj ĉiu do povos legi mesaĝojn. Vi havos neniujn avantaĝojn de ĉifrado. Ĉifrado de mesaĝoj en publika ĉambro malrapidigos iliajn ricevadon kaj sendadon.",
|
||||
"Are you sure you want to make this encrypted room public?": "Ĉu vi certas, ke vi volas publikigi ĉi tiun ĉifratan ĉambron?",
|
||||
"Unknown failure": "Nekonata malsukceso",
|
||||
"Failed to update the join rules": "Malsukcesis ĝisdatigi regulojn pri aliĝo"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2705,7 +2705,7 @@
|
|||
"Invite by username": "Kutsu kasutajanime alusel",
|
||||
"What projects are you working on?": "Mis ettevõtmistega sa tegeled?",
|
||||
"Decrypted event source": "Sündmuse dekrüptitud lähtekood",
|
||||
"Original event source": "Algse sündmuse lähtekood",
|
||||
"Original event source": "Sündmuse töötlemata lähtekood",
|
||||
"Failed to remove some rooms. Try again later": "Mõnede jututubade eemaldamine ei õnnestunud. Proovi hiljem uuesti",
|
||||
"Removing...": "Eemaldan...",
|
||||
"Mark as not suggested": "Eemalda soovitus",
|
||||
|
@ -3158,5 +3158,47 @@
|
|||
"Before you upgrade": "Enne uuendamist",
|
||||
"To join a space you'll need an invite.": "Kogukonnakeskusega liitumiseks vajad kutset.",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s kasutajat reageeris järgnevalt: %(content)s",
|
||||
"Joining space …": "Liitun kohukonnakeskusega…"
|
||||
"Joining space …": "Liitun kohukonnakeskusega…",
|
||||
"Would you like to leave the rooms in this space?": "Kas sa soovid lahkuda ka selle kogukonna jututubadest?",
|
||||
"You are about to leave <spaceName/>.": "Sa oled lahkumas <spaceName/> kogukonnast.",
|
||||
"Leave some rooms": "Lahku mõnedest jututubadest",
|
||||
"Leave all rooms": "Lahku kõikidest jututubadest",
|
||||
"Don't leave any rooms": "Ära lahku ühestki jututoast",
|
||||
"Expand quotes │ ⇧+click": "Näita tsitaate │ ⇧+click",
|
||||
"Collapse quotes │ ⇧+click": "Ahenda tsitaadid │ ⇧+click",
|
||||
"Media omitted": "Osa meediat jäi eksportimata",
|
||||
"Media omitted - file size limit exceeded": "Osa meediat jäi vahele failisuuruse piirangu tõttu",
|
||||
"Include Attachments": "Kaasa manused",
|
||||
"Size Limit": "Andmemahu piir",
|
||||
"Format": "Vorming",
|
||||
"Select from the options below to export chats from your timeline": "Kui soovid oma ajajoonelt mõnda vestlust eksportida, siis vali tingimused alljärgnevalt",
|
||||
"Export Chat": "Ekspordi vestlus",
|
||||
"Exporting your data": "Ekspordin sinu andmeid",
|
||||
"Stop": "Peata",
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Kas sa oled kindel, et soovid oma andmete eksporti katkestada? Kui nii toimid, siis pead hiljem uuesti alustama.",
|
||||
"Your export was successful. Find it in your Downloads folder.": "Sinu andmete eksport õnnestus. Faili leiad tavapärasest allalaadimiste kaustast.",
|
||||
"The export was cancelled successfully": "Ekspordi tühistamine õnnestus",
|
||||
"Export Successful": "Eksport õnnestus",
|
||||
"MB": "MB",
|
||||
"Number of messages": "Sõnumite arv",
|
||||
"Number of messages can only be a number between %(min)s and %(max)s": "Sõnumite arv saab olla ainult number%(min)s ja %(max)s vahemikust",
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB": "Suurus saab olla number %(min)s MB ja %(max)s MB vahemikust",
|
||||
"Enter a number between %(min)s and %(max)s": "Sisesta number %(min)s ja %(max)s vahemikust",
|
||||
"In reply to <a>this message</a>": "Vastuseks <a>sellele sõnumile</a>",
|
||||
"Export chat": "Ekspordi vestlus",
|
||||
"File Attached": "Fail on manustatud",
|
||||
"Error fetching file": "Viga faili laadimisel",
|
||||
"Topic: %(topic)s": "Teema: %(topic)s",
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "See on <roomName/> jututoast eksporditud andmekogu. Viited: <exporterDetails/>, %(exportDate)s.",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s lõi selle jututoa.",
|
||||
"Current Timeline": "Praegune ajajoon",
|
||||
"Specify a number of messages": "Määra sõnumite arv",
|
||||
"From the beginning": "Algusest alates",
|
||||
"Plain Text": "Vormindamata tekst",
|
||||
"JSON": "JSON",
|
||||
"HTML": "HTML",
|
||||
"Are you sure you want to exit during this export?": "Kas sa oled kindel, et soovid lõpetada tegevuse selle ekspordi ajal?",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s saatis kleepsu.",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s muutis jututoa tunnuspilti.",
|
||||
"%(date)s at %(time)s": "%(date)s %(time)s"
|
||||
}
|
||||
|
|
|
@ -3162,5 +3162,12 @@
|
|||
"To join a space you'll need an invite.": "Vous avez besoin d’une invitation pour rejoindre un espace.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Vous pouvez également créer des espaces à partir de <a>communautés</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Montre temporairement les communautés au lieu des espaces pour cette session. Il ne sera plus possible de le faire dans un futur proche. Cela va recharger Element.",
|
||||
"Display Communities instead of Spaces": "Afficher les communautés au lieu des espaces"
|
||||
"Display Communities instead of Spaces": "Afficher les communautés au lieu des espaces",
|
||||
"Would you like to leave the rooms in this space?": "Voulez-vous quitter les salons de cet espace ?",
|
||||
"You are about to leave <spaceName/>.": "Vous êtes sur le point de quitter <spaceName/>.",
|
||||
"Leave some rooms": "Quitter certains salons",
|
||||
"Leave all rooms": "Quitter tous les salons",
|
||||
"Don't leave any rooms": "Ne quitter aucun salon",
|
||||
"Expand quotes │ ⇧+click": "Développer les citations │ ⇧+clic",
|
||||
"Collapse quotes │ ⇧+click": "Réduire les citations │ ⇧+clic"
|
||||
}
|
||||
|
|
|
@ -3157,5 +3157,14 @@
|
|||
"To join a space you'll need an invite.": "Para unirte a un espazo precisas un convite.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Tamén podes crear Espazos a partir de <a>comunidades</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "De xeito temporal, mostrar comunidades no lugar de Espazos durante esta sesión. Esta función vai ser eliminada en próximas versións. Reiniciará Element.",
|
||||
"Display Communities instead of Spaces": "Mostrar Comunidades no lugar de Espazos"
|
||||
"Display Communities instead of Spaces": "Mostrar Comunidades no lugar de Espazos",
|
||||
"Would you like to leave the rooms in this space?": "Queres sair destas salas neste espazo?",
|
||||
"You are about to leave <spaceName/>.": "Vas saír de <spaceName/>.",
|
||||
"Leave some rooms": "Sair de algunhas salas",
|
||||
"Leave all rooms": "Sair de tódalas salas",
|
||||
"Don't leave any rooms": "Non saír de ningunha sala",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reaccionou con %(content)s",
|
||||
"Joining space …": "Uníndote ao espazo…",
|
||||
"Expand quotes │ ⇧+click": "Despregar citas | ⇧+click",
|
||||
"Collapse quotes │ ⇧+click": "Pechar citas | ⇧+click"
|
||||
}
|
||||
|
|
|
@ -3164,5 +3164,35 @@
|
|||
"You are about to leave <spaceName/>.": "Éppen el akarja hagyni <spaceName/> teret.",
|
||||
"Leave some rooms": "Kilépés néhány szobából",
|
||||
"Leave all rooms": "Kilépés minden szobából",
|
||||
"Don't leave any rooms": "Ne lépjen ki egy szobából sem"
|
||||
"Don't leave any rooms": "Ne lépjen ki egy szobából sem",
|
||||
"Expand quotes │ ⇧+click": "Idézetek megnyitása │ ⇧+kattintás",
|
||||
"Collapse quotes │ ⇧+click": "Idézetek bezárása│ ⇧+kattintás",
|
||||
"Include Attachments": "Csatolmányokkal együtt",
|
||||
"Size Limit": "Méret korlát",
|
||||
"Format": "Formátum",
|
||||
"Export Chat": "Beszélgetés kimentése",
|
||||
"Exporting your data": "Adatai kimentése",
|
||||
"Stop": "Állj",
|
||||
"The export was cancelled successfully": "Az exportálás sikeresen félbeszakítva",
|
||||
"Export Successful": "Exportálás sikeres",
|
||||
"MB": "MB",
|
||||
"Number of messages": "Üzenetek száma",
|
||||
"In reply to <a>this message</a>": "Válasz erre az <a>üzenetre</a>",
|
||||
"Export chat": "Beszélgetés kimentése",
|
||||
"File Attached": "Fájl csatolva",
|
||||
"Error fetching file": "Fájl letöltés hiba",
|
||||
"Topic: %(topic)s": "Téma: %(topic)s",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s hozta létre ezt a szobát.",
|
||||
"Media omitted - file size limit exceeded": "Média fájl kihagyva - fájl méret korlát túllépés",
|
||||
"Media omitted": "Média nélkül",
|
||||
"Current Timeline": "Aktuális idővonal",
|
||||
"Specify a number of messages": "Üzenetek számának megadása",
|
||||
"From the beginning": "Az elejétől",
|
||||
"Plain Text": "Sima szöveg",
|
||||
"JSON": "JSON",
|
||||
"HTML": "HTML",
|
||||
"Are you sure you want to exit during this export?": "Biztos, hogy kilép az exportálás közben?",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s matricát küldött.",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s megváltoztatta a szoba avatar képét.",
|
||||
"%(date)s at %(time)s": "%(date)s %(time)s"
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
"The version of %(brand)s": "Versi %(brand)s",
|
||||
"Your language of choice": "Pilihan bahasamu",
|
||||
"Your homeserver's URL": "URL Homeserver Anda",
|
||||
"e.g. %(exampleValue)s": "",
|
||||
"e.g. %(exampleValue)s": "mis. %(exampleValue)s",
|
||||
"Every page you use in the app": "Setiap halaman yang digunakan di app",
|
||||
"e.g. <CurrentPageURL>": "e.g. <URLHalamanSaatIni>",
|
||||
"Your device resolution": "Resolusi perangkat Anda",
|
||||
|
@ -214,5 +214,5 @@
|
|||
"Explore rooms": "Jelajahi ruang",
|
||||
"Sign In": "Masuk",
|
||||
"Create Account": "Buat Akun",
|
||||
"Identity server": "Server Identitas"
|
||||
"Identity server": "Server identitas"
|
||||
}
|
||||
|
|
|
@ -1218,14 +1218,14 @@
|
|||
"Do not use an identity server": "Non usare un server di identità",
|
||||
"You do not have the required permissions to use this command.": "Non hai l'autorizzazione necessaria per usare questo comando.",
|
||||
"Use an identity server": "Usa un server di identità",
|
||||
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Usa un server di identità per invitare via email. Clicca \"Continua\" per usare quello predefinito (%(defaultIdentityServerName)s) o gestiscilo nelle impostazioni.",
|
||||
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Usa un server d'identità per invitare via email. Clicca \"Continua\" per usare quello predefinito (%(defaultIdentityServerName)s) o gestiscilo nelle impostazioni.",
|
||||
"Use an identity server to invite by email. Manage in Settings.": "Usa un server di identità per invitare via email. Gestisci nelle impostazioni.",
|
||||
"Upgrade the room": "Aggiorna la stanza",
|
||||
"Enable room encryption": "Attiva la crittografia della stanza",
|
||||
"Deactivate user?": "Disattivare l'utente?",
|
||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Disattivare questo utente lo disconnetterà e ne impedirà nuovi accessi. In aggiunta, abbandonerà tutte le stanze in cui è presente. Questa azione non può essere annullata. Sei sicuro di volere disattivare questo utente?",
|
||||
"Deactivate user": "Disattiva utente",
|
||||
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Usa un server di identità per invitare via email. <default>Usa quello predefinito (%(defaultIdentityServerName)s)</default> o gestiscilo nelle <settings>impostazioni</settings>.",
|
||||
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Usa un server d'identità per invitare via email. <default>Usa quello predefinito (%(defaultIdentityServerName)s)</default> o gestiscilo nelle <settings>impostazioni</settings>.",
|
||||
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Usa un server di identità per invitare via email. Gestisci nelle <settings>impostazioni</settings>.",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Invia un messaggio in testo semplice, senza interpretarlo come markdown",
|
||||
"Error changing power level": "Errore cambiando il livello di poteri",
|
||||
|
@ -1236,11 +1236,11 @@
|
|||
"This invite to %(roomName)s was sent to %(email)s": "Questo invito per %(roomName)s è stato inviato a %(email)s",
|
||||
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Usa un server di identià nelle impostazioni per ricevere inviti direttamente in %(brand)s.",
|
||||
"Share this email in Settings to receive invites directly in %(brand)s.": "Condividi questa email nelle impostazioni per ricevere inviti direttamente in %(brand)s.",
|
||||
"Change identity server": "Cambia Identity Server",
|
||||
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Disconnettersi dall'Identity Server <current /> e connettesi invece a <new />?",
|
||||
"Disconnect identity server": "Disconnetti dall'Identity Server",
|
||||
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Stai ancora <b> fornendo le tue informazioni personali </b> sull'Identity Server <idserver />.",
|
||||
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Ti suggeriamo di rimuovere il tuo indirizzo email e numero di telefono dall'Identity Server prima di disconnetterti.",
|
||||
"Change identity server": "Cambia server d'identità",
|
||||
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Disconnettersi dal server d'identità <current /> e connettesi invece a <new />?",
|
||||
"Disconnect identity server": "Disconnetti dal server d'identità",
|
||||
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Stai ancora <b> fornendo le tue informazioni personali </b> sul server d'identità <idserver />.",
|
||||
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Ti suggeriamo di rimuovere il tuo indirizzo email e numero di telefono dal server d'identità prima di disconnetterti.",
|
||||
"Disconnect anyway": "Disconnetti comunque",
|
||||
"Error changing power level requirement": "Errore nella modifica del livello dei permessi",
|
||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "C'é stato un errore nel cambio di libelli dei permessi. Assicurati di avere i permessi necessari e riprova.",
|
||||
|
@ -3161,5 +3161,12 @@
|
|||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Mostra temporaneamente le comunità invece degli spazi per questa sessione. Il supporto per questa azione verrà rimosso nel breve termine. Element verrà ricaricato.",
|
||||
"Display Communities instead of Spaces": "Mostra le comunità invece degli spazi",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s ha reagito con %(content)s",
|
||||
"Joining space …": "Ingresso nello spazio …"
|
||||
"Joining space …": "Ingresso nello spazio …",
|
||||
"Would you like to leave the rooms in this space?": "Vuoi uscire dalle stanze di questo spazio?",
|
||||
"You are about to leave <spaceName/>.": "Stai per uscire da <spaceName/>.",
|
||||
"Leave some rooms": "Esci da alcune stanze",
|
||||
"Leave all rooms": "Esci da tutte le stanze",
|
||||
"Don't leave any rooms": "Non uscire da alcuna stanza",
|
||||
"Expand quotes │ ⇧+click": "Espandi le menzioni │ ⇧+clic",
|
||||
"Collapse quotes │ ⇧+click": "Riduci le menzioni │ ⇧+clic"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue