mirror of https://github.com/vector-im/riot-web
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/19010
Conflicts: src/components/structures/SpaceRoomView.tsx src/components/views/rooms/MemberList.tsx src/components/views/rooms/RoomList.tsxpull/21833/head
commit
020eca6922
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)
|
||||
===================================================================================================
|
||||
|
||||
|
|
16
package.json
16
package.json
|
@ -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": {
|
||||
|
@ -25,9 +25,9 @@
|
|||
"bin": {
|
||||
"reskindex": "scripts/reskindex.js"
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"matrix_src_main": "./src/index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
|
@ -79,6 +79,7 @@
|
|||
"highlight.js": "^10.5.0",
|
||||
"html-entities": "^1.4.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.12.0",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
|
@ -133,6 +134,8 @@
|
|||
"@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",
|
||||
"@types/linkifyjs": "^2.1.3",
|
||||
|
@ -156,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",
|
||||
|
@ -166,9 +170,11 @@
|
|||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.3",
|
||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rrweb-snapshot": "1.1.7",
|
||||
|
@ -182,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)"
|
||||
|
@ -199,6 +206,7 @@
|
|||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
|
|
|
@ -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";
|
||||
|
@ -82,6 +84,7 @@
|
|||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_ExportDialog.scss";
|
||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||
@import "./views/dialogs/_ForwardDialog.scss";
|
||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
|
||||
|
@ -268,6 +271,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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ limitations under the License.
|
|||
float: right;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-22px;
|
||||
color: $muted-fg-color;
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
.mx_SetupEncryptionBody_reset {
|
||||
color: $light-fg-color;
|
||||
margin-top: $font-14px;
|
||||
|
||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||
MatrixClientContext.displayName = "MatrixClientContext";
|
||||
export default MatrixClientContext;
|
||||
a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
|||
&.mx_Waveform_bar_100pct {
|
||||
// Small animation to remove the mechanical feel of progress
|
||||
transition: background-color 250ms ease;
|
||||
background-color: $message-body-panel-fg-color;
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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_ExportDialog {
|
||||
.mx_ExportDialog_subheading {
|
||||
font-size: $font-16px;
|
||||
display: block;
|
||||
font-family: $font-family;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-content;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&.mx_ExportDialog_Exporting {
|
||||
.mx_ExportDialog_options {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_Field_select::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_RadioButton input[type="radio"]:checked + div > div {
|
||||
background: $greyed-fg-color;
|
||||
}
|
||||
|
||||
.mx_RadioButton input[type=radio]:checked + div {
|
||||
border-color: unset;
|
||||
}
|
||||
|
||||
.mx_Field_valid.mx_Field label,
|
||||
.mx_Field_valid.mx_Field:focus-within label {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.mx_Field_valid.mx_Field, .mx_Field_valid.mx_Field:focus-within {
|
||||
border-color: $input-border-color;
|
||||
}
|
||||
|
||||
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
||||
background: $greyed-fg-color;
|
||||
border-color: $greyed-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ExportDialog_progress {
|
||||
.mx_Dialog_buttons {
|
||||
margin-top: unset;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.mx_Spinner {
|
||||
width: unset;
|
||||
height: unset;
|
||||
flex: unset;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_RadioButton > .mx_RadioButton_content {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.mx_Field_postfix {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
.mx_InviteDialog_editor {
|
||||
flex: 1;
|
||||
width: 100%; // Needed to make the Field inside grow
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
min-height: 25px;
|
||||
padding-left: 8px;
|
||||
|
@ -167,7 +167,7 @@ limitations under the License.
|
|||
padding: 5px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
@ -395,7 +395,7 @@ limitations under the License.
|
|||
left: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-top: 1px solid $message-body-panel-bg-color;
|
||||
border-top: 1px solid $quinary-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -38,7 +38,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
& + .mx_SettingsTab_subheading {
|
||||
border-top: 1px solid $message-body-panel-bg-color;
|
||||
border-top: 1px solid $quinary-content;
|
||||
margin-top: 0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -63,7 +63,7 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
|
||||
.mx_MFileBody_info_icon {
|
||||
background-color: $message-body-panel-icon-bg-color;
|
||||
background-color: $system;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
|
@ -78,7 +78,7 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: cover;
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||
background-color: $message-body-panel-icon-fg-color;
|
||||
background-color: $secondary-content;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
||||
|
|
|
@ -18,11 +18,11 @@ limitations under the License.
|
|||
// have unique styles).
|
||||
|
||||
.mx_MediaBody {
|
||||
background-color: $message-body-panel-bg-color;
|
||||
background-color: $quinary-content;
|
||||
border-radius: 12px;
|
||||
max-width: 243px; // use max-width instead of width so it fits within right panels
|
||||
|
||||
color: $message-body-panel-fg-color;
|
||||
color: $secondary-content;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
|
||||
|
|
|
@ -243,3 +243,7 @@ limitations under the License.
|
|||
.mx_RoomSummaryCard_icon_settings::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_export::before {
|
||||
mask-image: url('$(res)/img/element-icons/export.svg');
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_VerificationPanel_QRPhase_startOption {
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,14 @@
|
|||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47716 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12.7071 17.7071C12.6112 17.803 12.5007 17.8753 12.3828 17.9241L11.2929 17.7071L11.2925 17.7067L7.2929 13.7071C6.90237 13.3166 6.90237 12.6834 7.2929 12.2929C7.68342 11.9024 8.31658 11.9024 8.70711 12.2929L11 14.5858L11 7C11 6.44771 11.4477 6 12 6C12.5523 6 13 6.44771 13 7L13 14.5858L15.2929 12.2929C15.6834 11.9024 16.3166 11.9024 16.7071 12.2929C17.0976 12.6834 17.0976 13.3166 16.7071 13.7071L12.7071 17.7071ZM12.3828 17.9241L11.295 17.7092C11.4758 17.8889 11.7249 18 12 18C12.1356 18 12.2649 17.973 12.3828 17.9241Z"
|
||||
fill="#C1C6CD"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 821 B |
|
@ -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 |
|
@ -206,23 +206,13 @@ $kbd-border-color: #000000;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: $primary-content;
|
||||
|
||||
$interactive-tooltip-bg-color: $background;
|
||||
$interactive-tooltip-fg-color: $primary-content;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-content;
|
||||
$message-body-panel-bg-color: $quinary-content;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
$message-body-panel-icon-fg-color: $secondary-content;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-content;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||
$voice-record-icon-color: $quaternary-content;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// Appearance tab colors
|
||||
$appearance-tab-border-color: $room-highlight-color;
|
||||
|
|
|
@ -202,18 +202,8 @@ $kbd-border-color: #000000;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: #21262C;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
|
|
|
@ -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;
|
||||
|
@ -32,12 +29,22 @@ $primary-bg-color: #ffffff;
|
|||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||
|
||||
// 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: #C1C6CD;
|
||||
$quinary-content: #e3e8f0;
|
||||
$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: rgba($tertiary-content, 0.2);
|
||||
$space-nav: rgba($tertiary-content, 0.15);
|
||||
|
||||
// used for dialog box text
|
||||
$light-fg-color: #747474;
|
||||
|
@ -326,26 +333,16 @@ $kbd-border-color: $reaction-row-button-border-color;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -16,6 +16,25 @@ limitations under the License.
|
|||
|
||||
$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, $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, $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);
|
||||
$space-nav: rgba($panel-base, 0.1);
|
||||
|
||||
//
|
||||
// --accent-color
|
||||
$accent-color: var(--accent-color);
|
||||
|
@ -48,7 +67,6 @@ $roomheader-bg-color: var(--timeline-background-color);
|
|||
$roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
||||
//
|
||||
// --sidebar-color
|
||||
$interactive-tooltip-bg-color: var(--sidebar-color);
|
||||
$groupFilterPanel-bg-color: var(--sidebar-color);
|
||||
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||
|
|
|
@ -326,18 +326,8 @@ $inverted-bg-color: #27303a;
|
|||
$tooltip-timeline-bg-color: $inverted-bg-color;
|
||||
$tooltip-timeline-fg-color: $background;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
$interactive-tooltip-fg-color: $background;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-content;
|
||||
$message-body-panel-bg-color: $quinary-content;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
$message-body-panel-icon-fg-color: $secondary-content;
|
||||
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
|
@ -346,8 +336,8 @@ $voice-record-live-circle-color: #ff4b55;
|
|||
$voice-record-stop-border-color: $quinary-content;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||
$voice-record-icon-color: $tertiary-content;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -51,6 +51,7 @@ import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
|||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import { Skinner } from "../Skinner";
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
|
@ -95,6 +96,7 @@ declare global {
|
|||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||
mxActiveWidgetStore?: ActiveWidgetStore;
|
||||
mxSkinner?: Skinner;
|
||||
mxOnRecaptchaLoaded?: () => void;
|
||||
electron?: Electron;
|
||||
}
|
||||
|
@ -157,6 +159,10 @@ declare global {
|
|||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface HTMLStyleElement {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Add Chrome-specific `instant` ScrollBehaviour
|
||||
type _ScrollBehavior = ScrollBehavior | "instant";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -14,11 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ALL_MESSAGES,
|
||||
ALL_MESSAGES_LOUD,
|
||||
MENTIONS_ONLY,
|
||||
MUTE,
|
||||
} from "./RoomNotifs";
|
||||
|
||||
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
|
||||
declare module '!!raw-loader!*' {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import * as sdk from './index';
|
||||
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/src";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
function getIdServerDomain() {
|
||||
function getIdServerDomain(): string {
|
||||
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
}
|
||||
|
||||
|
@ -40,10 +41,13 @@ function getIdServerDomain() {
|
|||
* https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
|
||||
*/
|
||||
export default class AddThreepid {
|
||||
private sessionId: string;
|
||||
private submitUrl: string;
|
||||
private clientSecret: string;
|
||||
private bind: boolean;
|
||||
|
||||
constructor() {
|
||||
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
||||
this.sessionId = null;
|
||||
this.submitUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +56,7 @@ export default class AddThreepid {
|
|||
* @param {string} emailAddress The email address to add
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
addEmailAddress(emailAddress) {
|
||||
public addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
|
@ -72,7 +76,7 @@ export default class AddThreepid {
|
|||
* @param {string} emailAddress The email address to add
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
async bindEmailAddress(emailAddress) {
|
||||
public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
|
@ -105,7 +109,7 @@ export default class AddThreepid {
|
|||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
addMsisdn(phoneCountry, phoneNumber) {
|
||||
public addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||
).then((res) => {
|
||||
|
@ -129,7 +133,7 @@ export default class AddThreepid {
|
|||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
async bindMsisdn(phoneCountry, phoneNumber) {
|
||||
public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
|
@ -161,7 +165,7 @@ export default class AddThreepid {
|
|||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
async checkEmailLinkClicked() {
|
||||
public async checkEmailLinkClicked(): Promise<any[]> {
|
||||
try {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (this.bind) {
|
||||
|
@ -175,7 +179,7 @@ export default class AddThreepid {
|
|||
});
|
||||
} else {
|
||||
try {
|
||||
await this._makeAddThreepidOnlyRequest();
|
||||
await this.makeAddThreepidOnlyRequest();
|
||||
|
||||
// The spec has always required this to use UI auth but synapse briefly
|
||||
// implemented it without, so this may just succeed and that's OK.
|
||||
|
@ -186,9 +190,6 @@ export default class AddThreepid {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -208,7 +209,7 @@ export default class AddThreepid {
|
|||
title: _t("Add Email Address"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: e.data,
|
||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
|
@ -235,16 +236,16 @@ export default class AddThreepid {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Object} auth UI auth object
|
||||
* @param {{type: string, session?: string}} auth UI auth object
|
||||
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
|
||||
*/
|
||||
_makeAddThreepidOnlyRequest = (auth) => {
|
||||
private makeAddThreepidOnlyRequest = (auth?: {type: string, session?: string}): Promise<{}> => {
|
||||
return MatrixClientPeg.get().addThreePidOnly({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
auth,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a phone number verification code as entered by the user and validates
|
||||
|
@ -254,7 +255,7 @@ export default class AddThreepid {
|
|||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
async haveMsisdnToken(msisdnToken) {
|
||||
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const supportsSeparateAddAndBind =
|
||||
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
||||
|
@ -291,7 +292,7 @@ export default class AddThreepid {
|
|||
});
|
||||
} else {
|
||||
try {
|
||||
await this._makeAddThreepidOnlyRequest();
|
||||
await this.makeAddThreepidOnlyRequest();
|
||||
|
||||
// The spec has always required this to use UI auth but synapse briefly
|
||||
// implemented it without, so this may just succeed and that's OK.
|
||||
|
@ -302,9 +303,6 @@ export default class AddThreepid {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -324,7 +322,7 @@ export default class AddThreepid {
|
|||
title: _t("Add Phone Number"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: e.data,
|
||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -161,3 +161,20 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
|
|||
// Compare weekdays
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
}
|
||||
|
||||
export function formatFullDateNoDay(date: Date) {
|
||||
return _t("%(date)s at %(time)s", {
|
||||
date: date.toLocaleDateString().replace(/\//g, '-'),
|
||||
time: date.toLocaleTimeString().replace(/:/g, '-'),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFullDateNoDayNoTime(date: Date) {
|
||||
return (
|
||||
date.getFullYear() +
|
||||
"/" +
|
||||
pad(date.getMonth() + 1) +
|
||||
"/" +
|
||||
pad(date.getDate())
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { createClient, MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
||||
import {
|
||||
|
@ -27,23 +27,25 @@ import {
|
|||
doesIdentityServerHaveTerms,
|
||||
useDefaultIdentityServer,
|
||||
} from './utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from './utils/UrlUtils';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import { abbreviateUrl } from "./utils/UrlUtils";
|
||||
|
||||
export class AbortedIdentityActionError extends Error {}
|
||||
|
||||
export default class IdentityAuthClient {
|
||||
private accessToken: string;
|
||||
private tempClient: MatrixClient;
|
||||
private authEnabled = true;
|
||||
|
||||
/**
|
||||
* Creates a new identity auth client
|
||||
* @param {string} identityUrl The URL to contact the identity server with.
|
||||
* When provided, this class will operate solely within memory, refusing to
|
||||
* persist any information such as tokens. Default null (not provided).
|
||||
*/
|
||||
constructor(identityUrl = null) {
|
||||
this.accessToken = null;
|
||||
this.authEnabled = true;
|
||||
|
||||
constructor(identityUrl?: string) {
|
||||
if (identityUrl) {
|
||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
||||
// do identity server auth. The functions don't take an identity URL
|
||||
|
@ -54,32 +56,29 @@ export default class IdentityAuthClient {
|
|||
baseUrl: "", // invalid by design
|
||||
idBaseUrl: identityUrl,
|
||||
});
|
||||
} else {
|
||||
// Indicates that we're using the real client, not some workaround.
|
||||
this.tempClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
get _matrixClient() {
|
||||
private get matrixClient(): MatrixClient {
|
||||
return this.tempClient ? this.tempClient : MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
_writeToken() {
|
||||
private writeToken(): void {
|
||||
if (this.tempClient) return; // temporary client: ignore
|
||||
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
||||
}
|
||||
|
||||
_readToken() {
|
||||
private readToken(): string {
|
||||
if (this.tempClient) return null; // temporary client: ignore
|
||||
return window.localStorage.getItem("mx_is_access_token");
|
||||
}
|
||||
|
||||
hasCredentials() {
|
||||
return this.accessToken != null; // undef or null
|
||||
public hasCredentials(): boolean {
|
||||
return Boolean(this.accessToken);
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to the access_token string from the IS
|
||||
async getAccessToken({ check = true } = {}) {
|
||||
public async getAccessToken({ check = true } = {}): Promise<string> {
|
||||
if (!this.authEnabled) {
|
||||
// The current IS doesn't support authentication
|
||||
return null;
|
||||
|
@ -87,21 +86,21 @@ export default class IdentityAuthClient {
|
|||
|
||||
let token = this.accessToken;
|
||||
if (!token) {
|
||||
token = this._readToken();
|
||||
token = this.readToken();
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
token = await this.registerForToken(check);
|
||||
if (token) {
|
||||
this.accessToken = token;
|
||||
this._writeToken();
|
||||
this.writeToken();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
if (check) {
|
||||
try {
|
||||
await this._checkToken(token);
|
||||
await this.checkToken(token);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof TermsNotSignedError ||
|
||||
|
@ -114,7 +113,7 @@ export default class IdentityAuthClient {
|
|||
token = await this.registerForToken();
|
||||
if (token) {
|
||||
this.accessToken = token;
|
||||
this._writeToken();
|
||||
this.writeToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,11 +121,11 @@ export default class IdentityAuthClient {
|
|||
return token;
|
||||
}
|
||||
|
||||
async _checkToken(token) {
|
||||
const identityServerUrl = this._matrixClient.getIdentityServerUrl();
|
||||
private async checkToken(token: string): Promise<void> {
|
||||
const identityServerUrl = this.matrixClient.getIdentityServerUrl();
|
||||
|
||||
try {
|
||||
await this._matrixClient.getIdentityAccount(token);
|
||||
await this.matrixClient.getIdentityAccount(token);
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
||||
logger.log("Identity server requires new terms to be agreed to");
|
||||
|
@ -145,8 +144,8 @@ export default class IdentityAuthClient {
|
|||
!doesAccountDataHaveIdentityServer() &&
|
||||
!(await doesIdentityServerHaveTerms(identityServerUrl))
|
||||
) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Default identity server terms warning', '',
|
||||
QuestionDialog, {
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
|
@ -184,13 +183,13 @@ export default class IdentityAuthClient {
|
|||
// See also https://github.com/vector-im/element-web/issues/10455.
|
||||
}
|
||||
|
||||
async registerForToken(check=true) {
|
||||
public async registerForToken(check = true): Promise<string> {
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
if (check) await this.checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,21 @@
|
|||
import React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
interface IChildProps {
|
||||
style: React.CSSProperties;
|
||||
ref: (node: React.ReactInstance) => void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// either a list of child nodes, or a single child.
|
||||
children: React.ReactNode;
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition?: object;
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: React.CSSProperties[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The NodeAnimator contains components and animates transitions.
|
||||
|
@ -9,55 +24,45 @@ import PropTypes from 'prop-types';
|
|||
* from DOM order. This makes it a lot simpler and lighter: if you need fully
|
||||
* automatic positional animation, look at react-shuffle or similar libraries.
|
||||
*/
|
||||
export default class NodeAnimator extends React.Component {
|
||||
static propTypes = {
|
||||
// either a list of child nodes, or a single child.
|
||||
children: PropTypes.any,
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition: PropTypes.object,
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class NodeAnimator extends React.Component<IProps> {
|
||||
private nodes = {};
|
||||
private children: { [key: string]: React.DetailedReactHTMLElement<any, HTMLElement> };
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
startStyles: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.nodes = {};
|
||||
this._updateChildren(this.props.children);
|
||||
this.updateChildren(this.props.children);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._updateChildren(this.props.children);
|
||||
public componentDidUpdate(): void {
|
||||
this.updateChildren(this.props.children);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} node element to apply styles to
|
||||
* @param {object} styles a key/value pair of CSS properties
|
||||
* @param {React.CSSProperties} styles a key/value pair of CSS properties
|
||||
* @returns {void}
|
||||
*/
|
||||
_applyStyles(node, styles) {
|
||||
private applyStyles(node: HTMLElement, styles: React.CSSProperties): void {
|
||||
Object.entries(styles).forEach(([property, value]) => {
|
||||
node.style[property] = value;
|
||||
});
|
||||
}
|
||||
|
||||
_updateChildren(newChildren) {
|
||||
private updateChildren(newChildren: React.ReactNode): void {
|
||||
const oldChildren = this.children || {};
|
||||
this.children = {};
|
||||
React.Children.toArray(newChildren).forEach((c) => {
|
||||
React.Children.toArray(newChildren).forEach((c: any) => {
|
||||
if (oldChildren[c.key]) {
|
||||
const old = oldChildren[c.key];
|
||||
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
||||
|
||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
||||
this._applyStyles(oldNode, { left: c.props.style.left });
|
||||
if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) {
|
||||
this.applyStyles(oldNode as HTMLElement, { left: c.props.style.left });
|
||||
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
// clone the old element with the props (and children) of the new element
|
||||
|
@ -66,7 +71,7 @@ export default class NodeAnimator extends React.Component {
|
|||
} else {
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
// the enter animations
|
||||
const newProps = {};
|
||||
const newProps: Partial<IChildProps> = {};
|
||||
const restingStyle = c.props.style;
|
||||
|
||||
const startStyles = this.props.startStyles;
|
||||
|
@ -76,7 +81,7 @@ export default class NodeAnimator extends React.Component {
|
|||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||
}
|
||||
|
||||
newProps.ref = ((n) => this._collectNode(
|
||||
newProps.ref = ((n) => this.collectNode(
|
||||
c.key, n, restingStyle,
|
||||
));
|
||||
|
||||
|
@ -85,7 +90,7 @@ export default class NodeAnimator extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_collectNode(k, node, restingStyle) {
|
||||
private collectNode(k: string, node: React.ReactInstance, restingStyle: React.CSSProperties): void {
|
||||
if (
|
||||
node &&
|
||||
this.nodes[k] === undefined &&
|
||||
|
@ -96,7 +101,7 @@ export default class NodeAnimator extends React.Component {
|
|||
// start from startStyle 1: 0 is the one we gave it
|
||||
// to start with, so now we animate 1 etc.
|
||||
for (let i = 1; i < startStyles.length; ++i) {
|
||||
this._applyStyles(domNode, startStyles[i]);
|
||||
this.applyStyles(domNode as HTMLElement, startStyles[i]);
|
||||
// console.log("start:"
|
||||
// JSON.stringify(startStyles[i]),
|
||||
// );
|
||||
|
@ -104,7 +109,7 @@ export default class NodeAnimator extends React.Component {
|
|||
|
||||
// and then we animate to the resting state
|
||||
setTimeout(() => {
|
||||
this._applyStyles(domNode, restingStyle);
|
||||
this.applyStyles(domNode as HTMLElement, restingStyle);
|
||||
}, 0);
|
||||
|
||||
// console.log("enter:",
|
||||
|
@ -113,7 +118,7 @@ export default class NodeAnimator extends React.Component {
|
|||
this.nodes[k] = node;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>{ Object.values(this.children) }</>
|
||||
);
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/** The types of page which can be shown by the LoggedInView */
|
||||
export default {
|
||||
HomePage: "home_page",
|
||||
RoomView: "room_view",
|
||||
RoomDirectory: "room_directory",
|
||||
UserView: "user_view",
|
||||
GroupView: "group_view",
|
||||
MyGroups: "my_groups",
|
||||
};
|
||||
enum PageType {
|
||||
HomePage = "home_page",
|
||||
RoomView = "room_view",
|
||||
RoomDirectory = "room_directory",
|
||||
UserView = "user_view",
|
||||
GroupView = "group_view",
|
||||
MyGroups = "my_groups",
|
||||
}
|
||||
|
||||
export default PageType;
|
|
@ -20,10 +20,11 @@ limitations under the License.
|
|||
* registration code.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
|
||||
// Regex for what a "safe" or "Matrix-looking" localpart would be.
|
||||
// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
|
||||
|
@ -41,9 +42,11 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
|||
* @param {bool} options.screen_after
|
||||
* If present the screen to redirect to after a successful login or register.
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
export async function startAnyRegistrationFlow(
|
||||
// eslint-disable-next-line camelcase
|
||||
options: { go_home_on_cancel?: boolean, go_welcome_on_cancel?: boolean, screen_after?: boolean},
|
||||
): Promise<void> {
|
||||
if (options === undefined) options = {};
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
|
@ -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) => {
|
||||
|
|
|
@ -17,27 +17,31 @@ limitations under the License.
|
|||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IAnnotatedPushRule, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||
|
||||
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
|
||||
export const ALL_MESSAGES = 'all_messages';
|
||||
export const MENTIONS_ONLY = 'mentions_only';
|
||||
export const MUTE = 'mute';
|
||||
export enum RoomNotifState {
|
||||
AllMessagesLoud = 'all_messages_loud',
|
||||
AllMessages = 'all_messages',
|
||||
MentionsOnly = 'mentions_only',
|
||||
Mute = 'mute',
|
||||
}
|
||||
|
||||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
||||
export const BADGE_STATES = [RoomNotifState.AllMessages, RoomNotifState.AllMessagesLoud];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, RoomNotifState.MentionsOnly];
|
||||
|
||||
export function shouldShowNotifBadge(roomNotifState) {
|
||||
export function shouldShowNotifBadge(roomNotifState: RoomNotifState): boolean {
|
||||
return BADGE_STATES.includes(roomNotifState);
|
||||
}
|
||||
|
||||
export function shouldShowMentionBadge(roomNotifState) {
|
||||
export function shouldShowMentionBadge(roomNotifState: RoomNotifState): boolean {
|
||||
return MENTION_BADGE_STATES.includes(roomNotifState);
|
||||
}
|
||||
|
||||
export function aggregateNotificationCount(rooms) {
|
||||
return rooms.reduce((result, room) => {
|
||||
export function aggregateNotificationCount(rooms: Room[]): {count: number, highlight: boolean} {
|
||||
return rooms.reduce<{count: number, highlight: boolean}>((result, room) => {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
|
||||
// use helper method to include highlights in the previous version of the room
|
||||
const notificationCount = getUnreadNotificationCount(room);
|
||||
|
||||
|
@ -55,9 +59,9 @@ export function aggregateNotificationCount(rooms) {
|
|||
}, { count: 0, highlight: false });
|
||||
}
|
||||
|
||||
export function getRoomHasBadge(room) {
|
||||
export function getRoomHasBadge(room: Room): boolean {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||
|
@ -66,14 +70,14 @@ export function getRoomHasBadge(room) {
|
|||
return notifBadges || mentionBadges;
|
||||
}
|
||||
|
||||
export function getRoomNotifsState(roomId) {
|
||||
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
|
||||
export function getRoomNotifsState(roomId: string): RoomNotifState {
|
||||
if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages;
|
||||
|
||||
// look through the override rules for a rule affecting this room:
|
||||
// if one exists, it will take precedence.
|
||||
const muteRule = findOverrideMuteRule(roomId);
|
||||
if (muteRule) {
|
||||
return MUTE;
|
||||
return RoomNotifState.Mute;
|
||||
}
|
||||
|
||||
// for everything else, look at the room rule.
|
||||
|
@ -89,27 +93,27 @@ export function getRoomNotifsState(roomId) {
|
|||
// XXX: We have to assume the default is to notify for all messages
|
||||
// (in particular this will be 'wrong' for one to one rooms because
|
||||
// they will notify loudly for all messages)
|
||||
if (!roomRule || !roomRule.enabled) return ALL_MESSAGES;
|
||||
if (!roomRule || !roomRule.enabled) return RoomNotifState.AllMessages;
|
||||
|
||||
// a mute at the room level will still allow mentions
|
||||
// to notify
|
||||
if (isMuteRule(roomRule)) return MENTIONS_ONLY;
|
||||
if (isMuteRule(roomRule)) return RoomNotifState.MentionsOnly;
|
||||
|
||||
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
|
||||
if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD;
|
||||
if (actionsObject.tweaks.sound) return RoomNotifState.AllMessagesLoud;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setRoomNotifsState(roomId, newState) {
|
||||
if (newState === MUTE) {
|
||||
export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Promise<void> {
|
||||
if (newState === RoomNotifState.Mute) {
|
||||
return setRoomNotifsStateMuted(roomId);
|
||||
} else {
|
||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||
}
|
||||
}
|
||||
|
||||
export function getUnreadNotificationCount(room, type=null) {
|
||||
export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number {
|
||||
let notificationCount = room.getUnreadNotificationCount(type);
|
||||
|
||||
// Check notification counts in the old room just in case there's some lost
|
||||
|
@ -124,21 +128,21 @@ export function getUnreadNotificationCount(room, type=null) {
|
|||
// notifying the user for unread messages because they would have extreme
|
||||
// difficulty changing their notification preferences away from "All Messages"
|
||||
// and "Noisy".
|
||||
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
|
||||
notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
function setRoomNotifsStateMuted(roomId) {
|
||||
function setRoomNotifsStateMuted(roomId: string): Promise<any> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const promises = [];
|
||||
|
||||
// delete the room rule
|
||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||
if (roomRule) {
|
||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
|
||||
}
|
||||
|
||||
// add/replace an override rule to squelch everything in this room
|
||||
|
@ -146,7 +150,7 @@ function setRoomNotifsStateMuted(roomId) {
|
|||
// is an override rule, not a room rule: it still pertains to this room
|
||||
// though, so using the room ID as the rule ID is logical and prevents
|
||||
// duplicate copies of the rule.
|
||||
promises.push(cli.addPushRule('global', 'override', roomId, {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
|
||||
conditions: [
|
||||
{
|
||||
kind: 'event_match',
|
||||
|
@ -162,30 +166,30 @@ function setRoomNotifsStateMuted(roomId) {
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise<any> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const promises = [];
|
||||
|
||||
const overrideMuteRule = findOverrideMuteRule(roomId);
|
||||
if (overrideMuteRule) {
|
||||
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.Override, overrideMuteRule.rule_id));
|
||||
}
|
||||
|
||||
if (newState === 'all_messages') {
|
||||
if (newState === RoomNotifState.AllMessages) {
|
||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||
if (roomRule) {
|
||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
|
||||
}
|
||||
} else if (newState === 'mentions_only') {
|
||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||
} else if (newState === RoomNotifState.MentionsOnly) {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||
actions: [
|
||||
'dont_notify',
|
||||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
} else if ('all_messages_loud') {
|
||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
|
||||
} else if (newState === RoomNotifState.AllMessagesLoud) {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||
actions: [
|
||||
'notify',
|
||||
{
|
||||
|
@ -195,13 +199,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
|||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function findOverrideMuteRule(roomId) {
|
||||
function findOverrideMuteRule(roomId: string): IAnnotatedPushRule {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.pushRules ||
|
||||
!cli.pushRules['global'] ||
|
||||
|
@ -218,7 +222,7 @@ function findOverrideMuteRule(roomId) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function isRuleForRoom(roomId, rule) {
|
||||
function isRuleForRoom(roomId: string, rule: IAnnotatedPushRule): boolean {
|
||||
if (rule.conditions.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
@ -226,6 +230,6 @@ function isRuleForRoom(roomId, rule) {
|
|||
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
||||
}
|
||||
|
||||
function isMuteRule(rule) {
|
||||
function isMuteRule(rule: IAnnotatedPushRule): boolean {
|
||||
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||
}
|
|
@ -247,13 +247,31 @@ import { objectClone } from "./utils/objects";
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
function sendResponse(event, res) {
|
||||
enum Action {
|
||||
CloseScalar = "close_scalar",
|
||||
GetWidgets = "get_widgets",
|
||||
SetWidgets = "set_widgets",
|
||||
SetWidget = "set_widget",
|
||||
JoinRulesState = "join_rules_state",
|
||||
SetPlumbingState = "set_plumbing_state",
|
||||
GetMembershipCount = "get_membership_count",
|
||||
GetRoomEncryptionState = "get_room_enc_state",
|
||||
CanSendEvent = "can_send_event",
|
||||
MembershipState = "membership_state",
|
||||
invite = "invite",
|
||||
BotOptions = "bot_options",
|
||||
SetBotOptions = "set_bot_options",
|
||||
SetBotPower = "set_bot_power",
|
||||
}
|
||||
|
||||
function sendResponse(event: MessageEvent<any>, res: any): void {
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
// @ts-ignore
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
function sendError(event, msg, nestedError) {
|
||||
function sendError(event: MessageEvent<any>, msg: string, nestedError?: Error): void {
|
||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
|
@ -264,10 +282,11 @@ function sendError(event, msg, nestedError) {
|
|||
if (nestedError) {
|
||||
data.response.error._error = nestedError;
|
||||
}
|
||||
// @ts-ignore
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
function inviteUser(event, roomId, userId) {
|
||||
function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`Received request to invite ${userId} into room ${roomId}`);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
|
@ -295,7 +314,7 @@ function inviteUser(event, roomId, userId) {
|
|||
});
|
||||
}
|
||||
|
||||
function setWidget(event, roomId) {
|
||||
function setWidget(event: MessageEvent<any>, roomId: string): void {
|
||||
const widgetId = event.data.widget_id;
|
||||
let widgetType = event.data.type;
|
||||
const widgetUrl = event.data.url;
|
||||
|
@ -356,7 +375,7 @@ function setWidget(event, roomId) {
|
|||
}
|
||||
}
|
||||
|
||||
function getWidgets(event, roomId) {
|
||||
function getWidgets(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -382,7 +401,7 @@ function getWidgets(event, roomId) {
|
|||
sendResponse(event, widgetStateEvents);
|
||||
}
|
||||
|
||||
function getRoomEncState(event, roomId) {
|
||||
function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -398,7 +417,7 @@ function getRoomEncState(event, roomId) {
|
|||
sendResponse(event, roomIsEncrypted);
|
||||
}
|
||||
|
||||
function setPlumbingState(event, roomId, status) {
|
||||
function setPlumbingState(event: MessageEvent<any>, roomId: string, status: string): void {
|
||||
if (typeof status !== 'string') {
|
||||
throw new Error('Plumbing state status should be a string');
|
||||
}
|
||||
|
@ -417,7 +436,7 @@ function setPlumbingState(event, roomId, status) {
|
|||
});
|
||||
}
|
||||
|
||||
function setBotOptions(event, roomId, userId) {
|
||||
function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
|
@ -433,7 +452,9 @@ function setBotOptions(event, roomId, userId) {
|
|||
});
|
||||
}
|
||||
|
||||
function setBotPower(event, roomId, userId, level) {
|
||||
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;
|
||||
|
@ -446,40 +467,52 @@ function setBotPower(event, roomId, userId, level) {
|
|||
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, roomId, userId) {
|
||||
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.member", userId);
|
||||
}
|
||||
|
||||
function getJoinRules(event, roomId) {
|
||||
function getJoinRules(event: MessageEvent<any>, roomId: string): void {
|
||||
logger.log(`join_rules of ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.join_rules", "");
|
||||
}
|
||||
|
||||
function botOptions(event, roomId, userId) {
|
||||
function botOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||
}
|
||||
|
||||
function getMembershipCount(event, roomId) {
|
||||
function getMembershipCount(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -494,7 +527,7 @@ function getMembershipCount(event, roomId) {
|
|||
sendResponse(event, count);
|
||||
}
|
||||
|
||||
function canSendEvent(event, roomId) {
|
||||
function canSendEvent(event: MessageEvent<any>, roomId: string): void {
|
||||
const evType = "" + event.data.event_type; // force stringify
|
||||
const isState = Boolean(event.data.is_state);
|
||||
const client = MatrixClientPeg.get();
|
||||
|
@ -528,7 +561,7 @@ function canSendEvent(event, roomId) {
|
|||
sendResponse(event, true);
|
||||
}
|
||||
|
||||
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||
function returnStateEvent(event: MessageEvent<any>, roomId: string, eventType: string, stateKey: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -547,8 +580,9 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
|||
sendResponse(event, stateEvent.getContent());
|
||||
}
|
||||
|
||||
const onMessage = function(event) {
|
||||
const onMessage = function(event: MessageEvent<any>): void {
|
||||
if (!event.origin) { // stupid chrome
|
||||
// @ts-ignore
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
|
@ -582,8 +616,8 @@ const onMessage = function(event) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.data.action === "close_scalar") {
|
||||
dis.dispatch({ action: "close_scalar" });
|
||||
if (event.data.action === Action.CloseScalar) {
|
||||
dis.dispatch({ action: Action.CloseScalar });
|
||||
sendResponse(event, null);
|
||||
return;
|
||||
}
|
||||
|
@ -596,10 +630,10 @@ const onMessage = function(event) {
|
|||
// Get and set user widgets (not associated with a specific room)
|
||||
// If roomId is specified, it must be validated, so room-based widgets agreed
|
||||
// handled further down.
|
||||
if (event.data.action === "get_widgets") {
|
||||
if (event.data.action === Action.GetWidgets) {
|
||||
getWidgets(event, null);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
} else if (event.data.action === Action.SetWidgets) {
|
||||
setWidget(event, null);
|
||||
return;
|
||||
} else {
|
||||
|
@ -614,28 +648,28 @@ const onMessage = function(event) {
|
|||
}
|
||||
|
||||
// Get and set room-based widgets
|
||||
if (event.data.action === "get_widgets") {
|
||||
if (event.data.action === Action.GetWidgets) {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
} else if (event.data.action === Action.SetWidget) {
|
||||
setWidget(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
if (event.data.action === Action.JoinRulesState) {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_plumbing_state") {
|
||||
} else if (event.data.action === Action.SetPlumbingState) {
|
||||
setPlumbingState(event, roomId, event.data.status);
|
||||
return;
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
} else if (event.data.action === Action.GetMembershipCount) {
|
||||
getMembershipCount(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_room_enc_state") {
|
||||
} else if (event.data.action === Action.GetRoomEncryptionState) {
|
||||
getRoomEncState(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
} else if (event.data.action === Action.CanSendEvent) {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
@ -645,20 +679,20 @@ const onMessage = function(event) {
|
|||
return;
|
||||
}
|
||||
switch (event.data.action) {
|
||||
case "membership_state":
|
||||
case Action.MembershipState:
|
||||
getMembershipState(event, roomId, userId);
|
||||
break;
|
||||
case "invite":
|
||||
case Action.invite:
|
||||
inviteUser(event, roomId, userId);
|
||||
break;
|
||||
case "bot_options":
|
||||
case Action.BotOptions:
|
||||
botOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_options":
|
||||
case Action.SetBotOptions:
|
||||
setBotOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_power":
|
||||
setBotPower(event, roomId, userId, event.data.level);
|
||||
case Action.SetBotPower:
|
||||
setBotPower(event, roomId, userId, event.data.level, event.data.ignoreIfGreater);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||
|
@ -667,16 +701,16 @@ const onMessage = function(event) {
|
|||
};
|
||||
|
||||
let listenerCount = 0;
|
||||
let openManagerUrl = null;
|
||||
let openManagerUrl: string = null;
|
||||
|
||||
export function startListening() {
|
||||
export function startListening(): void {
|
||||
if (listenerCount === 0) {
|
||||
window.addEventListener("message", onMessage, false);
|
||||
}
|
||||
listenerCount += 1;
|
||||
}
|
||||
|
||||
export function stopListening() {
|
||||
export function stopListening(): void {
|
||||
listenerCount -= 1;
|
||||
if (listenerCount === 0) {
|
||||
window.removeEventListener("message", onMessage);
|
||||
|
@ -691,6 +725,6 @@ export function stopListening() {
|
|||
}
|
||||
}
|
||||
|
||||
export function setOpenManagerUrl(url) {
|
||||
export function setOpenManagerUrl(url: string): void {
|
||||
openManagerUrl = url;
|
||||
}
|
|
@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
class Skinner {
|
||||
constructor() {
|
||||
this.components = null;
|
||||
}
|
||||
import React from "react";
|
||||
|
||||
getComponent(name) {
|
||||
export interface IComponents {
|
||||
[key: string]: React.Component;
|
||||
}
|
||||
|
||||
export interface ISkinObject {
|
||||
components: IComponents;
|
||||
}
|
||||
|
||||
export class Skinner {
|
||||
public components: IComponents = null;
|
||||
|
||||
public getComponent(name: string): React.Component {
|
||||
if (!name) throw new Error(`Invalid component name: ${name}`);
|
||||
if (this.components === null) {
|
||||
throw new Error(
|
||||
|
@ -30,7 +38,7 @@ class Skinner {
|
|||
);
|
||||
}
|
||||
|
||||
const doLookup = (components) => {
|
||||
const doLookup = (components: IComponents): React.Component => {
|
||||
if (!components) return null;
|
||||
let comp = components[name];
|
||||
// XXX: Temporarily also try 'views.' as we're currently
|
||||
|
@ -58,7 +66,7 @@ class Skinner {
|
|||
return comp;
|
||||
}
|
||||
|
||||
load(skinObject) {
|
||||
public load(skinObject: ISkinObject): void {
|
||||
if (this.components !== null) {
|
||||
throw new Error(
|
||||
"Attempted to load a skin while a skin is already loaded"+
|
||||
|
@ -72,6 +80,7 @@ class Skinner {
|
|||
}
|
||||
|
||||
// Now that we have a skin, load our components too
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const idx = require("./component-index");
|
||||
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
|
||||
for (const c in idx.components) {
|
||||
|
@ -79,7 +88,7 @@ class Skinner {
|
|||
}
|
||||
}
|
||||
|
||||
addComponent(name, comp) {
|
||||
public addComponent(name: string, comp: any) {
|
||||
let slot = name;
|
||||
if (comp.replaces !== undefined) {
|
||||
if (comp.replaces.indexOf('.') > -1) {
|
||||
|
@ -91,7 +100,7 @@ class Skinner {
|
|||
this.components[slot] = comp;
|
||||
}
|
||||
|
||||
reset() {
|
||||
public reset(): void {
|
||||
this.components = null;
|
||||
}
|
||||
}
|
||||
|
@ -105,8 +114,8 @@ class Skinner {
|
|||
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||
// ("Modules are cached based on their resolved filename")
|
||||
if (global.mxSkinner === undefined) {
|
||||
global.mxSkinner = new Skinner();
|
||||
if (window.mxSkinner === undefined) {
|
||||
window.mxSkinner = new Skinner();
|
||||
}
|
||||
export default global.mxSkinner;
|
||||
export default window.mxSkinner;
|
||||
|
|
@ -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+(.+)/);
|
||||
|
|
|
@ -166,6 +166,11 @@ function textForTopicEvent(ev: MatrixEvent): () => string | null {
|
|||
});
|
||||
}
|
||||
|
||||
function textForRoomAvatarEvent(ev: MatrixEvent): () => string | null {
|
||||
const senderDisplayName = ev?.sender?.name || ev.getSender();
|
||||
return () => _t('%(senderDisplayName)s changed the room avatar.', { senderDisplayName });
|
||||
}
|
||||
|
||||
function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
|
@ -289,11 +294,27 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null {
|
|||
function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
||||
return () => {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
let message = ev.getContent().body;
|
||||
if (ev.isRedacted()) {
|
||||
message = _t("Message deleted");
|
||||
const unsigned = ev.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned?.redacted_because?.sender;
|
||||
if (redactedBecauseUserId && redactedBecauseUserId !== ev.getSender()) {
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const sender = room?.getMember(redactedBecauseUserId);
|
||||
message = _t("Message deleted by %(name)s", { name: sender?.name
|
||||
|| redactedBecauseUserId });
|
||||
}
|
||||
}
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
||||
} else if (ev.getType() == "m.sticker") {
|
||||
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
|
||||
} else {
|
||||
// in this case, parse it as a plain text message
|
||||
message = senderDisplayName + ': ' + message;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
@ -669,6 +690,7 @@ interface IHandlers {
|
|||
|
||||
const handlers: IHandlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.sticker': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
};
|
||||
|
||||
|
@ -677,6 +699,7 @@ const stateHandlers: IHandlers = {
|
|||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
"m.room.avatar": textForRoomAvatarEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
|
|
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
|
||||
export async function getThreepidsWithBindStatus(client, filterMedium) {
|
||||
export async function getThreepidsWithBindStatus(
|
||||
client: MatrixClient, filterMedium?: ThreepidMedium,
|
||||
): Promise<IThreepid[]> {
|
||||
const userId = client.getUserId();
|
||||
|
||||
let { threepids } = await client.getThreePids();
|
||||
|
@ -31,7 +35,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) {
|
|||
const identityAccessToken = await authClient.getAccessToken({ check: false });
|
||||
|
||||
// Restructure for lookup query
|
||||
const query = threepids.map(({ medium, address }) => [medium, address]);
|
||||
const query = threepids.map(({ medium, address }): [string, string] => [medium, address]);
|
||||
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
|
||||
|
||||
// Record which are already bound
|
|
@ -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();
|
||||
|
@ -226,6 +229,11 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
// Don't allow clicks to escape the context menu wrapper
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
@ -378,11 +386,23 @@ 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)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
<div
|
||||
|
@ -391,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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import linkifyMatrix from "../../linkify-matrix";
|
|||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import PageType from '../../PageTypes';
|
||||
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||
|
@ -208,7 +208,7 @@ interface IState {
|
|||
view: Views;
|
||||
// What the LoggedInView would be showing if visible
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type?: PageTypes;
|
||||
page_type?: PageType;
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
|
@ -724,7 +724,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_my_groups':
|
||||
this.setPage(PageTypes.MyGroups);
|
||||
this.setPage(PageType.MyGroups);
|
||||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
|
@ -763,7 +763,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
localStorage.setItem("mx_seenSpacesBeta", "1");
|
||||
// We just dispatch the page change rather than have to worry about
|
||||
// what the logic is for each of these branches.
|
||||
if (this.state.page_type === PageTypes.MyGroups) {
|
||||
if (this.state.page_type === PageType.MyGroups) {
|
||||
dis.dispatch({ action: 'view_last_screen' });
|
||||
} else {
|
||||
dis.dispatch({ action: 'view_my_groups' });
|
||||
|
@ -849,7 +849,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private setPage(pageType: string) {
|
||||
private setPage(pageType: PageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
});
|
||||
|
@ -956,7 +956,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
page_type: PageType.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
ready: true,
|
||||
|
@ -984,7 +984,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this.setPage(PageTypes.GroupView);
|
||||
this.setPage(PageType.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
|
||||
|
@ -1027,7 +1027,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
justRegistered,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.setPage(PageTypes.HomePage);
|
||||
this.setPage(PageType.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
ThemeController.isLogin = false;
|
||||
this.themeWatcher.recheck();
|
||||
|
@ -1045,7 +1045,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
this.setState({ currentUserId: userId });
|
||||
this.setPage(PageTypes.UserView);
|
||||
this.setPage(PageType.UserView);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
|||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -60,7 +62,7 @@ const groupedEvents = [
|
|||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(
|
||||
export function shouldFormContinuation(
|
||||
prevEvent: MatrixEvent,
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRoomMembersCount = (): void => {
|
||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return { nextEvent, nextTile };
|
||||
}
|
||||
|
||||
private get roomHasPendingEdit(): string {
|
||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
private get pendingEditItem(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this.roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this.roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -113,7 +114,7 @@ interface IProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
|
@ -187,10 +188,12 @@ export interface IState {
|
|||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
liveTimeline?: EventTimeline;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState: Pick<IState, any> = {
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if we're trying to edit events in wrong rendering context
|
||||
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
};
|
||||
|
||||
private async calculateRecommendedVersion(room: Room) {
|
||||
|
@ -2086,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
|
|
|
@ -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 (!hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
|
||||
setLoading(true);
|
||||
if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
await hierarchy.load(pageSize);
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
}, [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();
|
||||
|
@ -648,8 +662,6 @@ const SpaceHierarchy = ({
|
|||
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => {
|
||||
let content: JSX.Element;
|
||||
let loader: JSX.Element;
|
||||
|
||||
if (loading && !rooms.length) {
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
|
@ -671,19 +683,20 @@ const SpaceHierarchy = ({
|
|||
}}
|
||||
/>
|
||||
</>;
|
||||
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
} else {
|
||||
} else if (!hierarchy.canLoadMore) {
|
||||
results = <div className="mx_SpaceHierarchy_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let loader: JSX.Element;
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceHierarchy_listHeader">
|
||||
<h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4>
|
||||
|
|
|
@ -82,6 +82,8 @@ import GroupAvatar from "../views/avatars/GroupAvatar";
|
|||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -412,7 +414,9 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
const userId = cli.getUserId();
|
||||
|
||||
let inviteButton;
|
||||
if ((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) {
|
||||
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
@ -730,7 +734,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>,
|
||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,10 +49,14 @@ interface IProps {
|
|||
interface IState {
|
||||
replyToEvent?: MatrixEvent;
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.setupThread(payload.event);
|
||||
}
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if it's not a thread context
|
||||
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
|
||||
// Quit early if that's not a thread event
|
||||
if (payload.event && !payload.event.getThread()) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
|
@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
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={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
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={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
/>
|
||||
) }
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||
resizeMethod?: ResizeMethod;
|
||||
// The onClick to give the avatar
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick?: boolean;
|
||||
title?: string;
|
||||
style?: any;
|
||||
|
|
|
@ -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>
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,397 @@
|
|||
/*
|
||||
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, { useRef, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Field from "../elements/Field";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import {
|
||||
ExportFormat,
|
||||
ExportType,
|
||||
textForFormat,
|
||||
textForType,
|
||||
} from "../../../utils/exportUtils/exportUtils";
|
||||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
|
||||
import JSONExporter from "../../../utils/exportUtils/JSONExport";
|
||||
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
|
||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||
import Exporter from "../../../utils/exportUtils/Exporter";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
|
||||
const [exportFormat, setExportFormat] = useState(ExportFormat.Html);
|
||||
const [exportType, setExportType] = useState(ExportType.Timeline);
|
||||
const [includeAttachments, setAttachments] = useState(false);
|
||||
const [isExporting, setExporting] = useState(false);
|
||||
const [numberOfMessages, setNumberOfMessages] = useState<number>(100);
|
||||
const [sizeLimit, setSizeLimit] = useState<number | null>(8);
|
||||
const sizeLimitRef = useRef<Field>();
|
||||
const messageCountRef = useRef<Field>();
|
||||
const [exportProgressText, setExportProgressText] = useState("Processing...");
|
||||
const [displayCancel, setCancelWarning] = useState(false);
|
||||
const [exportCancelled, setExportCancelled] = useState(false);
|
||||
const [exportSuccessful, setExportSuccessful] = useState(false);
|
||||
const [exporter, setExporter] = useStateCallback<Exporter>(
|
||||
null,
|
||||
async (exporter: Exporter) => {
|
||||
await exporter?.export().then(() => {
|
||||
if (!exportCancelled) setExportSuccessful(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const startExport = async () => {
|
||||
const exportOptions = {
|
||||
numberOfMessages,
|
||||
attachmentsIncluded: includeAttachments,
|
||||
maxSize: sizeLimit * 1024 * 1024,
|
||||
};
|
||||
switch (exportFormat) {
|
||||
case ExportFormat.Html:
|
||||
setExporter(
|
||||
new HTMLExporter(
|
||||
room,
|
||||
ExportType[exportType],
|
||||
exportOptions,
|
||||
setExportProgressText,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case ExportFormat.Json:
|
||||
setExporter(
|
||||
new JSONExporter(
|
||||
room,
|
||||
ExportType[exportType],
|
||||
exportOptions,
|
||||
setExportProgressText,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case ExportFormat.PlainText:
|
||||
setExporter(
|
||||
new PlainTextExporter(
|
||||
room,
|
||||
ExportType[exportType],
|
||||
exportOptions,
|
||||
setExportProgressText,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown export format");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onExportClick = async () => {
|
||||
const isValidSize = await sizeLimitRef.current.validate({
|
||||
focused: false,
|
||||
});
|
||||
if (!isValidSize) {
|
||||
sizeLimitRef.current.validate({ focused: true });
|
||||
return;
|
||||
}
|
||||
if (exportType === ExportType.LastNMessages) {
|
||||
const isValidNumberOfMessages =
|
||||
await messageCountRef.current.validate({ focused: false });
|
||||
if (!isValidNumberOfMessages) {
|
||||
messageCountRef.current.validate({ focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
setExporting(true);
|
||||
await startExport();
|
||||
};
|
||||
|
||||
const validateSize = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => {
|
||||
const min = 1;
|
||||
const max = 10 ** 8;
|
||||
return _t("Enter a number between %(min)s and %(max)s", {
|
||||
min,
|
||||
max,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
key: "number",
|
||||
test: ({ value }) => {
|
||||
const parsedSize = parseFloat(value);
|
||||
const min = 1;
|
||||
const max = 2000;
|
||||
return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
|
||||
},
|
||||
invalid: () => {
|
||||
const min = 1;
|
||||
const max = 2000;
|
||||
return _t(
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB",
|
||||
{ min, max },
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const onValidateSize = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await validateSize(fieldState);
|
||||
return result;
|
||||
};
|
||||
|
||||
const validateNumberOfMessages = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => {
|
||||
const min = 1;
|
||||
const max = 10 ** 8;
|
||||
return _t("Enter a number between %(min)s and %(max)s", {
|
||||
min,
|
||||
max,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
key: "number",
|
||||
test: ({ value }) => {
|
||||
const parsedSize = parseFloat(value);
|
||||
const min = 1;
|
||||
const max = 10 ** 8;
|
||||
if (isNaN(parsedSize)) return false;
|
||||
return !(min > parsedSize || parsedSize > max);
|
||||
},
|
||||
invalid: () => {
|
||||
const min = 1;
|
||||
const max = 10 ** 8;
|
||||
return _t(
|
||||
"Number of messages can only be a number between %(min)s and %(max)s",
|
||||
{ min, max },
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const onValidateNumberOfMessages = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await validateNumberOfMessages(fieldState);
|
||||
return result;
|
||||
};
|
||||
|
||||
const onCancel = async () => {
|
||||
if (isExporting) setCancelWarning(true);
|
||||
else onFinished(false);
|
||||
};
|
||||
|
||||
const confirmCanel = async () => {
|
||||
await exporter?.cancelExport();
|
||||
setExportCancelled(true);
|
||||
setExporting(false);
|
||||
setExporter(null);
|
||||
};
|
||||
|
||||
const exportFormatOptions = Object.keys(ExportFormat).map((format) => ({
|
||||
value: ExportFormat[format],
|
||||
label: textForFormat(ExportFormat[format]),
|
||||
}));
|
||||
|
||||
const exportTypeOptions = Object.keys(ExportType).map((type) => {
|
||||
return (
|
||||
<option key={type} value={ExportType[type]}>
|
||||
{ textForType(ExportType[type]) }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
let messageCount = null;
|
||||
if (exportType === ExportType.LastNMessages) {
|
||||
messageCount = (
|
||||
<Field
|
||||
element="input"
|
||||
type="number"
|
||||
value={numberOfMessages.toString()}
|
||||
ref={messageCountRef}
|
||||
onValidate={onValidateNumberOfMessages}
|
||||
label={_t("Number of messages")}
|
||||
onChange={(e) => {
|
||||
setNumberOfMessages(parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sizePostFix = <span>{ _t("MB") }</span>;
|
||||
|
||||
if (exportCancelled) {
|
||||
// Display successful cancellation message
|
||||
return (
|
||||
<InfoDialog
|
||||
title={_t("Export Successful")}
|
||||
description={_t("The export was cancelled successfully")}
|
||||
hasCloseButton={true}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
);
|
||||
} else if (exportSuccessful) {
|
||||
// Display successful export message
|
||||
return (
|
||||
<InfoDialog
|
||||
title={_t("Export Successful")}
|
||||
description={_t(
|
||||
"Your export was successful. Find it in your Downloads folder.",
|
||||
)}
|
||||
hasCloseButton={true}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
);
|
||||
} else if (displayCancel) {
|
||||
// Display cancel warning
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("Warning")}
|
||||
className="mx_ExportDialog"
|
||||
contentId="mx_Dialog_content"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={true}
|
||||
>
|
||||
<p>
|
||||
{ _t(
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.",
|
||||
) }
|
||||
</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Stop")}
|
||||
primaryButtonClass="danger"
|
||||
hasCancel={true}
|
||||
cancelButton={_t("Continue")}
|
||||
onCancel={() => setCancelWarning(false)}
|
||||
onPrimaryButtonClick={confirmCanel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
} else {
|
||||
// Display export settings
|
||||
return (
|
||||
<BaseDialog
|
||||
title={isExporting ? _t("Exporting your data") : _t("Export Chat")}
|
||||
className={`mx_ExportDialog ${isExporting && "mx_ExportDialog_Exporting"}`}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={onFinished}
|
||||
fixedWidth={true}
|
||||
>
|
||||
{ !isExporting ? <p>
|
||||
{ _t(
|
||||
"Select from the options below to export chats from your timeline",
|
||||
) }
|
||||
</p> : null }
|
||||
|
||||
<span className="mx_ExportDialog_subheading">
|
||||
{ _t("Format") }
|
||||
</span>
|
||||
|
||||
<div className="mx_ExportDialog_options">
|
||||
<StyledRadioGroup
|
||||
name="exportFormat"
|
||||
value={exportFormat}
|
||||
onChange={(key) => setExportFormat(ExportFormat[key])}
|
||||
definitions={exportFormatOptions}
|
||||
/>
|
||||
|
||||
<span className="mx_ExportDialog_subheading">
|
||||
{ _t("Messages") }
|
||||
</span>
|
||||
|
||||
<Field
|
||||
element="select"
|
||||
value={exportType}
|
||||
onChange={(e) => {
|
||||
setExportType(ExportType[e.target.value]);
|
||||
}}
|
||||
>
|
||||
{ exportTypeOptions }
|
||||
</Field>
|
||||
{ messageCount }
|
||||
|
||||
<span className="mx_ExportDialog_subheading">
|
||||
{ _t("Size Limit") }
|
||||
</span>
|
||||
|
||||
<Field
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
onValidate={onValidateSize}
|
||||
element="input"
|
||||
ref={sizeLimitRef}
|
||||
value={sizeLimit.toString()}
|
||||
postfixComponent={sizePostFix}
|
||||
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
|
||||
/>
|
||||
|
||||
<StyledCheckbox
|
||||
checked={includeAttachments}
|
||||
onChange={(e) =>
|
||||
setAttachments(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
>
|
||||
{ _t("Include Attachments") }
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
{ isExporting ? (
|
||||
<div className="mx_ExportDialog_progress">
|
||||
<Spinner w={24} h={24} />
|
||||
<p>
|
||||
{ exportProgressText }
|
||||
</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Cancel")}
|
||||
primaryButtonClass="danger"
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={onCancel}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DialogButtons
|
||||
primaryButton={_t("Export")}
|
||||
onPrimaryButtonClick={onExportClick}
|
||||
onCancel={() => onFinished(false)}
|
||||
/>
|
||||
) }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ExportDialog;
|
|
@ -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}>
|
||||
|
|
|
@ -178,6 +178,14 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.ignoreEvent = ev;
|
||||
};
|
||||
|
||||
private onChevronClick = (ev: React.MouseEvent) => {
|
||||
if (this.state.expanded) {
|
||||
this.setState({ expanded: false });
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
|
@ -375,7 +383,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
<span onClick={this.onChevronClick} className="mx_Dropdown_arrow" />
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -53,6 +53,7 @@ interface IProps {
|
|||
layout?: Layout;
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps?: boolean;
|
||||
forExport?: boolean;
|
||||
isQuoteExpanded?: boolean;
|
||||
setQuoteExpanded: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
@ -381,6 +382,17 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
})
|
||||
}
|
||||
</blockquote>;
|
||||
} else if (this.props.forExport) {
|
||||
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
|
||||
header = <p className="mx_ReplyThread_Export">
|
||||
{ _t("In reply to <a>this message</a>",
|
||||
{},
|
||||
{ a: (sub) => (
|
||||
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
|
||||
),
|
||||
})
|
||||
}
|
||||
</p>;
|
||||
} else if (this.state.loading) {
|
||||
header = <Spinner w={16} h={16} />;
|
||||
}
|
||||
|
|
|
@ -35,12 +35,17 @@ function getDaysArray(): string[] {
|
|||
|
||||
interface IProps {
|
||||
ts: number;
|
||||
forExport?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DateSeparator")
|
||||
export default class DateSeparator extends React.Component<IProps> {
|
||||
private getLabel() {
|
||||
const date = new Date(this.props.ts);
|
||||
|
||||
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
|
||||
if (this.props.forExport) return formatFullDateNoTime(date);
|
||||
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const days = getDaysArray();
|
||||
|
|
|
@ -33,6 +33,7 @@ export interface IBodyProps {
|
|||
onHeightChanged: () => void;
|
||||
|
||||
showUrlPreview?: boolean;
|
||||
forExport?: boolean;
|
||||
tileShape: TileShape;
|
||||
maxImageHeight?: number;
|
||||
replacingEventId?: string;
|
||||
|
|
|
@ -90,6 +90,17 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.forExport) {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
const contentUrl = content.file?.url || content.url;
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<audio src={contentUrl} controls />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.playback) {
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
|
|
|
@ -123,6 +123,11 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
private getContentUrl(): string | null {
|
||||
if (this.props.forExport) return null;
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
private get content(): IMediaEventContent {
|
||||
return this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
}
|
||||
|
@ -149,11 +154,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private getContentUrl(): string {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
|
||||
this.props.onHeightChanged();
|
||||
|
@ -213,6 +213,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.forExport) {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
return <span className="mx_MFileBody">
|
||||
<a href={content.file?.url || content.url}>
|
||||
{ placeholder }
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
const showDownloadLink = this.props.tileShape || !this.props.showGenericPlaceholder;
|
||||
|
||||
if (isEncrypted) {
|
||||
|
|
|
@ -179,6 +179,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
};
|
||||
|
||||
protected getContentUrl(): string {
|
||||
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
if (this.props.forExport) return content.url || content.file?.url;
|
||||
if (this.media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
|
@ -372,7 +375,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
let placeholder = null;
|
||||
let gifLabel = null;
|
||||
|
||||
if (!this.state.imgLoaded) {
|
||||
if (!this.props.forExport && !this.state.imgLoaded) {
|
||||
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
|
@ -462,7 +465,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
// Overidden by MStickerBody
|
||||
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
return <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
|
||||
{ children }
|
||||
</a>;
|
||||
}
|
||||
|
@ -490,6 +493,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
// Overidden by MStickerBody
|
||||
protected getFileBody(): string | JSX.Element {
|
||||
if (this.props.forExport) return null;
|
||||
// We only ever need the download bar if we're appearing outside of the timeline
|
||||
if (this.props.tileShape) {
|
||||
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||
|
@ -510,7 +514,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
const contentUrl = this.getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this.isGif() && SettingsStore.getValue("autoplayGifs")) {
|
||||
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this.getThumbUrl();
|
||||
|
|
|
@ -79,7 +79,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
}
|
||||
|
||||
private getContentUrl(): string|null {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
if (this.props.forExport) return content.file?.url || content.url;
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
|
@ -93,6 +96,9 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
}
|
||||
|
||||
private getThumbUrl(): string|null {
|
||||
// there's no need of thumbnail when the content is local
|
||||
if (this.props.forExport) return null;
|
||||
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
const media = mediaFromContent(content);
|
||||
|
||||
|
@ -209,6 +215,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
private getFileBody = () => {
|
||||
if (this.props.forExport) return null;
|
||||
return this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const autoplay = SettingsStore.getValue("autoplayVideo");
|
||||
|
@ -222,8 +233,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
// Important: If we aren't autoplaying and we haven't decrypred it yet, show a video with a poster.
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
|
@ -254,6 +265,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
preload = "none";
|
||||
}
|
||||
}
|
||||
|
||||
const fileBody = this.getFileBody();
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<video
|
||||
|
@ -270,7 +283,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
poster={poster}
|
||||
onPlay={this.videoOnPlay}
|
||||
/>
|
||||
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
|
||||
{ fileBody }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { isVoiceMessage } from "../../../utils/EventUtils";
|
|||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
|
||||
public render() {
|
||||
if (isVoiceMessage(this.props.mxEvent)) {
|
||||
if (!this.props.forExport && isVoiceMessage(this.props.mxEvent)) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
} else {
|
||||
return <MAudioBody {...this.props} />;
|
||||
|
|
|
@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
|||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export enum ActionBarRenderingContext {
|
||||
Room,
|
||||
Thread
|
||||
}
|
||||
|
||||
interface IMessageActionBarProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions?: Relations;
|
||||
|
@ -142,7 +137,6 @@ interface IMessageActionBarProps {
|
|||
permalinkCreator?: RoomPermalinkCreator;
|
||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||
toggleThreadExpanded: () => void;
|
||||
renderingContext?: ActionBarRenderingContext;
|
||||
isQuoteExpanded?: boolean;
|
||||
}
|
||||
|
||||
|
@ -150,10 +144,6 @@ interface IMessageActionBarProps {
|
|||
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||
public static contextType = RoomContext;
|
||||
|
||||
public static defaultProps = {
|
||||
renderingContext: ActionBarRenderingContext.Room,
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
|
@ -217,8 +207,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
|
||||
private onEditClick = (ev: React.MouseEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -298,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.props.renderingContext === ActionBarRenderingContext.Room) {
|
||||
if (this.context.canReply && this.context.timelineRenderingType !== TimelineRenderingType.Thread) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
@ -334,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);
|
||||
|
|
|
@ -136,6 +136,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
forExport={this.props.forExport}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
|
|
|
@ -29,7 +29,6 @@ interface IProps {
|
|||
|
||||
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||
|
||||
let text = _t("Message deleted");
|
||||
const unsigned = mxEvent.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
|
||||
|
|
|
@ -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…");
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
|||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -240,6 +241,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onRoomExportClick = async () => {
|
||||
Modal.createTrackedDialog('export room dialog', '', ExportDialog, {
|
||||
room,
|
||||
});
|
||||
};
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
|
@ -280,6 +287,9 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ _t("Show files") }
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ _t("Export chat") }
|
||||
</Button>
|
||||
{ SettingsStore.getValue("feature_thread") && (
|
||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
||||
{ _t("Show threads") }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
|
|||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
@ -36,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } 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,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
|||
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';
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -66,7 +67,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
|||
return "";
|
||||
}
|
||||
|
||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
||||
function createEditContent(
|
||||
model: EditorModel,
|
||||
editedEvent: MatrixEvent,
|
||||
renderingContext?: TimelineRenderingType,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
|
@ -99,41 +104,49 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
|||
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
const relation = {
|
||||
"m.new_content": newContent,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": editedEvent.getId(),
|
||||
},
|
||||
}, contentBody);
|
||||
};
|
||||
|
||||
if (renderingContext === TimelineRenderingType.Thread) {
|
||||
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
|
||||
}
|
||||
|
||||
return Object.assign(relation, contentBody);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IEditMessageComposerProps extends MatrixClientProps {
|
||||
editState: EditorStateTransfer;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
saveDisabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private readonly dispatcherRef: string;
|
||||
private model: EditorModel = null;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
const isRestored = this.createEditorModel();
|
||||
const ev = this.props.editState.getEvent();
|
||||
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, ev, renderingContext);
|
||||
this.state = {
|
||||
saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]),
|
||||
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
|
@ -141,7 +154,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private getRoom(): Room {
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -162,10 +175,17 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
const previousEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: false,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (previousEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: previousEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
@ -174,12 +194,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
|
||||
const nextEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: true,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (nextEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: nextEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
} else {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: 'edit_event', event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -189,16 +221,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}`;
|
||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
||||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
private get events(): MatrixEvent[] {
|
||||
const liveTimelineEvents = this.context.liveTimeline.getEvents();
|
||||
const pendingEvents = this.getRoom().getPendingEvents();
|
||||
const isInThread = Boolean(this.props.editState.getEvent().getThread());
|
||||
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
||||
}
|
||||
|
||||
private cancelEdit = (): void => {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -326,8 +369,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, editedEvent, renderingContext);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
@ -381,7 +424,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
if (shouldSend) {
|
||||
this.cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
|
@ -389,7 +432,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -400,7 +447,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
this.props.mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,7 +475,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private createEditorModel(): boolean {
|
||||
const { editState } = this.props;
|
||||
const room = this.getRoom();
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||
|
||||
let parts;
|
||||
let isRestored = false;
|
||||
|
@ -493,3 +540,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||
export default EditMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
|
|||
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||
import TooltipButton from '../elements/TooltipButton';
|
||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
|
||||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from '../messages/ReactionsRow';
|
||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
@ -264,6 +264,8 @@ interface IProps {
|
|||
// for now.
|
||||
tileShape?: TileShape;
|
||||
|
||||
forExport?: boolean;
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour?: boolean;
|
||||
|
||||
|
@ -340,6 +342,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
static defaultProps = {
|
||||
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
||||
onHeightChanged: function() {},
|
||||
forExport: false,
|
||||
layout: Layout.Group,
|
||||
};
|
||||
|
||||
|
@ -382,7 +385,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
* or 'sent' receipt, for example.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private get isEligibleForSpecialReceipt() {
|
||||
private get isEligibleForSpecialReceipt(): boolean {
|
||||
// First, if there are other read receipts then just short-circuit this.
|
||||
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
|
||||
if (!this.props.mxEvent) return false;
|
||||
|
@ -453,16 +456,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
componentDidMount() {
|
||||
this.suppressReadReceiptAnimation = false;
|
||||
const client = this.context;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
|
||||
}
|
||||
if (!this.props.forExport) {
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
|
||||
}
|
||||
|
||||
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
||||
client.on("Room.receipt", this.onRoomReceipt);
|
||||
this.isListeningForReceipts = true;
|
||||
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
||||
client.on("Room.receipt", this.onRoomReceipt);
|
||||
this.isListeningForReceipts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
|
@ -698,6 +703,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
shouldHighlight() {
|
||||
if (this.props.forExport) return false;
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
|
@ -1056,17 +1062,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const renderingContext = this.props.tileShape === TileShape.Thread
|
||||
? ActionBarRenderingContext.Thread
|
||||
: ActionBarRenderingContext.Room;
|
||||
const actionBar = !isEditing ? <MessageActionBar
|
||||
const showMessageActionBar = !isEditing && !this.props.forExport;
|
||||
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
renderingContext={renderingContext}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||
/> : undefined;
|
||||
|
@ -1171,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
|
@ -1204,6 +1208,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
/>
|
||||
{ actionBar }
|
||||
</div>,
|
||||
|
@ -1224,6 +1230,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
|
@ -1247,6 +1254,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyThread}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
|
@ -1280,6 +1288,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
forExport={this.props.forExport}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
highlights={this.props.highlights}
|
||||
|
@ -1305,7 +1314,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||
function isMessageEvent(ev) {
|
||||
function isMessageEvent(ev: MatrixEvent): boolean {
|
||||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,8 @@ 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}`;
|
||||
|
@ -535,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) {
|
||||
|
|
|
@ -45,13 +45,15 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
|||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import SendMessageComposer from "./SendMessageComposer";
|
||||
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
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";
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
@ -193,6 +195,31 @@ 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;
|
||||
|
@ -219,8 +246,8 @@ interface IState {
|
|||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
|
@ -378,14 +405,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton) {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
await this.voiceRecordingButton.send();
|
||||
await this.voiceRecordingButton.current?.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageComposerInput.sendMessage();
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel) => {
|
||||
|
@ -432,6 +459,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} />,
|
||||
);
|
||||
|
@ -460,7 +492,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
|
@ -521,7 +553,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
|
@ -535,7 +567,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room} />);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -49,6 +49,8 @@ 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 {
|
||||
|
@ -134,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"
|
||||
|
@ -450,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) => {
|
||||
|
|
|
@ -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}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue