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.tsx
This commit is contained in:
commit
020eca6922
177 changed files with 6615 additions and 2121 deletions
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)
|
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",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.31.0",
|
"version": "3.32.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -25,9 +25,9 @@
|
||||||
"bin": {
|
"bin": {
|
||||||
"reskindex": "scripts/reskindex.js"
|
"reskindex": "scripts/reskindex.js"
|
||||||
},
|
},
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.ts",
|
||||||
"matrix_src_main": "./src/index.js",
|
"matrix_src_main": "./src/index.ts",
|
||||||
"matrix_lib_main": "./lib/index.js",
|
"matrix_lib_main": "./lib/index.ts",
|
||||||
"matrix_lib_typings": "./lib/index.d.ts",
|
"matrix_lib_typings": "./lib/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
|
@ -79,6 +79,7 @@
|
||||||
"highlight.js": "^10.5.0",
|
"highlight.js": "^10.5.0",
|
||||||
"html-entities": "^1.4.0",
|
"html-entities": "^1.4.0",
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.12.0",
|
"katex": "^0.12.0",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
@ -133,6 +134,8 @@
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/css-font-loading-module": "^0.0.6",
|
"@types/css-font-loading-module": "^0.0.6",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
|
"@types/enzyme": "^3.10.9",
|
||||||
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/linkifyjs": "^2.1.3",
|
"@types/linkifyjs": "^2.1.3",
|
||||||
|
@ -156,6 +159,7 @@
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
|
"enzyme-to-json": "^3.6.2",
|
||||||
"eslint": "7.18.0",
|
"eslint": "7.18.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||||
|
@ -166,9 +170,11 @@
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
|
"jest-raw-loader": "^1.0.1",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.3",
|
"matrix-react-test-utils": "^0.2.3",
|
||||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rrweb-snapshot": "1.1.7",
|
"rrweb-snapshot": "1.1.7",
|
||||||
|
@ -182,6 +188,7 @@
|
||||||
"@types/react": "17.0.14"
|
"@types/react": "17.0.14"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"snapshotSerializers": ["enzyme-to-json/serializer"],
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||||
|
@ -199,6 +206,7 @@
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
|
"^!!raw-loader!.*": "jest-raw-loader",
|
||||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
@import "./structures/_ViewSource.scss";
|
@import "./structures/_ViewSource.scss";
|
||||||
@import "./structures/auth/_CompleteSecurity.scss";
|
@import "./structures/auth/_CompleteSecurity.scss";
|
||||||
@import "./structures/auth/_Login.scss";
|
@import "./structures/auth/_Login.scss";
|
||||||
|
@import "./structures/auth/_SetupEncryptionBody.scss";
|
||||||
@import "./views/audio_messages/_AudioPlayer.scss";
|
@import "./views/audio_messages/_AudioPlayer.scss";
|
||||||
@import "./views/audio_messages/_PlayPauseButton.scss";
|
@import "./views/audio_messages/_PlayPauseButton.scss";
|
||||||
@import "./views/audio_messages/_PlaybackContainer.scss";
|
@import "./views/audio_messages/_PlaybackContainer.scss";
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
@import "./views/dialogs/_ChangelogDialog.scss";
|
@import "./views/dialogs/_ChangelogDialog.scss";
|
||||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
||||||
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
|
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
|
||||||
|
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.scss";
|
||||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||||
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||||
|
@ -82,6 +84,7 @@
|
||||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
|
@import "./views/dialogs/_ExportDialog.scss";
|
||||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||||
@import "./views/dialogs/_ForwardDialog.scss";
|
@import "./views/dialogs/_ForwardDialog.scss";
|
||||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
|
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
|
||||||
|
@ -268,6 +271,7 @@
|
||||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/spaces/_SpaceBasicSettings.scss";
|
@import "./views/spaces/_SpaceBasicSettings.scss";
|
||||||
|
@import "./views/spaces/_SpaceChildrenPicker.scss";
|
||||||
@import "./views/spaces/_SpaceCreateMenu.scss";
|
@import "./views/spaces/_SpaceCreateMenu.scss";
|
||||||
@import "./views/spaces/_SpacePublicShare.scss";
|
@import "./views/spaces/_SpacePublicShare.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
|
|
|
@ -34,4 +34,5 @@ limitations under the License.
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -422,7 +422,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
.mx_SpaceRoomView_inviteTeammates {
|
.mx_SpaceRoomView_inviteTeammates {
|
||||||
// XXX remove this when spaces leaves Beta
|
// XXX remove this when spaces leaves Beta
|
||||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||||
padding: 58px 16px 16px;
|
padding: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: $header-panel-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
|
@ -465,8 +465,13 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
|
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton {
|
||||||
|
color: $accent-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@ limitations under the License.
|
||||||
float: right;
|
float: right;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
line-height: $font-22px;
|
line-height: $font-22px;
|
||||||
color: $muted-fg-color;
|
color: $secondary-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,19 @@ limitations under the License.
|
||||||
margin: 0 auto;
|
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 {
|
.mx_CompleteSecurity_body {
|
||||||
font-size: $font-15px;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext } from "react";
|
.mx_SetupEncryptionBody_reset {
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
color: $light-fg-color;
|
||||||
|
margin-top: $font-14px;
|
||||||
|
|
||||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
|
||||||
MatrixClientContext.displayName = "MatrixClientContext";
|
color: $warning-color;
|
||||||
export default MatrixClientContext;
|
}
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
||||||
&.mx_Waveform_bar_100pct {
|
&.mx_Waveform_bar_100pct {
|
||||||
// Small animation to remove the mechanical feel of progress
|
// Small animation to remove the mechanical feel of progress
|
||||||
transition: background-color 250ms ease;
|
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;
|
background-color: $authpage-body-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field label {
|
|
||||||
color: $authpage-primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Field_labelAlwaysTopLeft label,
|
.mx_Field_labelAlwaysTopLeft label,
|
||||||
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
|
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
|
||||||
.mx_Field input:focus + label,
|
.mx_Field input:focus + label,
|
||||||
|
|
|
@ -75,7 +75,7 @@ limitations under the License.
|
||||||
@mixin ProgressBarBorderRadius 8px;
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpace_progressText {
|
.mx_AddExistingToSpaceDialog_progressText {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
66
res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss
Normal file
66
res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_ConfirmUserActionDialog .mx_Dialog_content {
|
.mx_ConfirmUserActionDialog .mx_Dialog_content .mx_ConfirmUserActionDialog_user {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,10 @@ limitations under the License.
|
||||||
.mx_ConfirmUserActionDialog_avatar {
|
.mx_ConfirmUserActionDialog_avatar {
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-top: -2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ConfirmUserActionDialog_name {
|
.mx_ConfirmUserActionDialog_name {
|
||||||
|
padding-top: 2px;
|
||||||
font-size: $font-18px;
|
font-size: $font-18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,16 +37,4 @@ limitations under the License.
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
background-color: $background;
|
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;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
|
margin-top: -13px; // match height of buttons to prevent height changing
|
||||||
|
|
||||||
.mx_ProgressBar {
|
.mx_ProgressBar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|
91
res/css/views/dialogs/_ExportDialog.scss
Normal file
91
res/css/views/dialogs/_ExportDialog.scss
Normal file
|
@ -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 {
|
.mx_InviteDialog_editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%; // Needed to make the Field inside grow
|
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;
|
border-radius: 4px;
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
@ -167,7 +167,7 @@ limitations under the License.
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $user-tile-hover-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,7 +395,7 @@ limitations under the License.
|
||||||
left: -24px;
|
left: -24px;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
padding-right: 24px;
|
padding-right: 24px;
|
||||||
border-top: 1px solid $message-body-panel-bg-color;
|
border-top: 1px solid $quinary-content;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -27,33 +27,13 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
max-height: 520px;
|
height: 520px;
|
||||||
|
|
||||||
.mx_Dialog_content {
|
.mx_Dialog_content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-y: auto;
|
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 {
|
.mx_LeaveSpaceDialog_section_warning {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 8px;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,22 @@ limitations under the License.
|
||||||
.mx_RoomUpgradeWarningDialog {
|
.mx_RoomUpgradeWarningDialog {
|
||||||
max-width: 38vw;
|
max-width: 38vw;
|
||||||
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 {
|
.mx_RoomUpgradeWarningDialog .mx_SettingsFlag {
|
||||||
|
|
|
@ -38,7 +38,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
& + .mx_SettingsTab_subheading {
|
& + .mx_SettingsTab_subheading {
|
||||||
border-top: 1px solid $message-body-panel-bg-color;
|
border-top: 1px solid $quinary-content;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,6 @@ limitations under the License.
|
||||||
color 0.25s ease-out 0.1s,
|
color 0.25s ease-out 0.1s,
|
||||||
transform 0.25s ease-out 0.1s,
|
transform 0.25s ease-out 0.1s,
|
||||||
background-color 0.25s ease-out 0.1s;
|
background-color 0.25s ease-out 0.1s;
|
||||||
color: $primary-content;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
|
@ -63,7 +63,7 @@ limitations under the License.
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.mx_MFileBody_info_icon {
|
.mx_MFileBody_info_icon {
|
||||||
background-color: $message-body-panel-icon-bg-color;
|
background-color: $system;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
@ -78,7 +78,7 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||||
background-color: $message-body-panel-icon-fg-color;
|
background-color: $secondary-content;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,11 @@ limitations under the License.
|
||||||
// have unique styles).
|
// have unique styles).
|
||||||
|
|
||||||
.mx_MediaBody {
|
.mx_MediaBody {
|
||||||
background-color: $message-body-panel-bg-color;
|
background-color: $quinary-content;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
max-width: 243px; // use max-width instead of width so it fits within right panels
|
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;
|
font-size: $font-14px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
|
|
@ -243,3 +243,7 @@ limitations under the License.
|
||||||
.mx_RoomSummaryCard_icon_settings::before {
|
.mx_RoomSummaryCard_icon_settings::before {
|
||||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
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 {
|
.mx_VerificationPanel_QRPhase_startOption {
|
||||||
background-color: $user-tile-hover-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -252,6 +252,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
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 {
|
.mx_MessageComposer_voiceMessage::before {
|
||||||
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
|
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,12 @@ limitations under the License.
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.mx_RoomSublist_minimized) {
|
||||||
|
.mx_RoomSublist_headerContainer {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_headerContainer {
|
.mx_RoomSublist_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -41,9 +47,7 @@ limitations under the License.
|
||||||
// The combined height must be set in the LeftPanel component for sticky headers
|
// The combined height must be set in the LeftPanel component for sticky headers
|
||||||
// to work correctly.
|
// to work correctly.
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
// Allow the container to collapse on itself if its children
|
height: 24px;
|
||||||
// are not in the normal document flow
|
|
||||||
max-height: 24px;
|
|
||||||
color: $roomlist-header-color;
|
color: $roomlist-header-color;
|
||||||
|
|
||||||
.mx_RoomSublist_stickable {
|
.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 {
|
.mx_RoomSublist_resizeBox {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -395,7 +391,7 @@ limitations under the License.
|
||||||
.mx_RoomSublist_skeletonUI {
|
.mx_RoomSublist_skeletonUI {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
height: 288px;
|
height: 240px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background: $roomsublist-skeleton-ui-bg;
|
background: $roomsublist-skeleton-ui-bg;
|
||||||
|
@ -410,3 +406,8 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/skeleton-ui.svg');
|
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 {
|
.mx_DevicesPanel {
|
||||||
display: table;
|
display: table;
|
||||||
table-layout: fixed;
|
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;
|
border-spacing: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,5 +67,7 @@ limitations under the License.
|
||||||
|
|
||||||
> .mx_AccessibleButton_kind_link {
|
> .mx_AccessibleButton_kind_link {
|
||||||
padding-left: 0; // to align with left side
|
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.
|
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 {
|
.mx_SecurityUserSettingsTab_deviceInfo {
|
||||||
display: table;
|
display: table;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
35
res/css/views/spaces/_SpaceChildrenPicker.scss
Normal file
35
res/css/views/spaces/_SpaceChildrenPicker.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
14
res/img/element-icons/export.svg
Normal file
14
res/img/element-icons/export.svg
Normal file
|
@ -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 |
5
res/img/element-icons/room/composer/poll.svg
Normal file
5
res/img/element-icons/room/composer/poll.svg
Normal file
|
@ -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-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: $primary-content;
|
$tooltip-timeline-fg-color: $primary-content;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: $background;
|
|
||||||
$interactive-tooltip-fg-color: $primary-content;
|
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$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-stop-border-color: $quaternary-content;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||||
$voice-record-icon-color: $quaternary-content;
|
$voice-record-icon-color: $quaternary-content;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $system;
|
||||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
$voice-playback-button-fg-color: $secondary-content;
|
||||||
|
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
|
@ -202,18 +202,8 @@ $kbd-border-color: #000000;
|
||||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: $base-color;
|
|
||||||
$interactive-tooltip-fg-color: #ffffff;
|
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$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
|
// See non-legacy dark for variable information
|
||||||
$voice-record-stop-border-color: #6F7882;
|
$voice-record-stop-border-color: #6F7882;
|
||||||
$voice-record-waveform-incomplete-fg-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';
|
$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
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
$accent-color: #03b381;
|
$accent-color: #03b381;
|
||||||
|
@ -32,12 +29,22 @@ $primary-bg-color: #ffffff;
|
||||||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||||
|
|
||||||
// Legacy theme backports
|
// Legacy theme backports
|
||||||
|
$accent: #0DBD8B;
|
||||||
|
$alert: #FF5B55;
|
||||||
|
$links: #0086e6;
|
||||||
$primary-content: $primary-fg-color;
|
$primary-content: $primary-fg-color;
|
||||||
$secondary-content: $secondary-fg-color;
|
$secondary-content: $secondary-fg-color;
|
||||||
$tertiary-content: $tertiary-fg-color;
|
$tertiary-content: $tertiary-fg-color;
|
||||||
$quaternary-content: #C1C6CD;
|
$quaternary-content: #C1C6CD;
|
||||||
$quinary-content: #e3e8f0;
|
$quinary-content: #e3e8f0;
|
||||||
|
$system: #F4F6FA;
|
||||||
$background: $primary-bg-color;
|
$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
|
// used for dialog box text
|
||||||
$light-fg-color: #747474;
|
$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-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: #27303a;
|
|
||||||
$interactive-tooltip-fg-color: #ffffff;
|
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$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
|
// See non-legacy _light for variable information
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
$voice-record-live-circle-color: #ff4b55;
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
$voice-record-stop-border-color: #E3E8F0;
|
||||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||||
$voice-record-icon-color: $tertiary-fg-color;
|
$voice-record-icon-color: $tertiary-fg-color;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $system;
|
||||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
$voice-playback-button-fg-color: $secondary-content;
|
||||||
|
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
|
@ -16,6 +16,25 @@ limitations under the License.
|
||||||
|
|
||||||
$font-family: var(--font-family, $font-family);
|
$font-family: var(--font-family, $font-family);
|
||||||
$monospace-font-family: var(--font-family-monospace, $monospace-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
|
||||||
$accent-color: var(--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);
|
$roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
||||||
//
|
//
|
||||||
// --sidebar-color
|
// --sidebar-color
|
||||||
$interactive-tooltip-bg-color: var(--sidebar-color);
|
|
||||||
$groupFilterPanel-bg-color: var(--sidebar-color);
|
$groupFilterPanel-bg-color: var(--sidebar-color);
|
||||||
$tooltip-timeline-bg-color: var(--sidebar-color);
|
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||||
|
|
|
@ -326,18 +326,8 @@ $inverted-bg-color: #27303a;
|
||||||
$tooltip-timeline-bg-color: $inverted-bg-color;
|
$tooltip-timeline-bg-color: $inverted-bg-color;
|
||||||
$tooltip-timeline-fg-color: $background;
|
$tooltip-timeline-fg-color: $background;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: #27303a;
|
|
||||||
$interactive-tooltip-fg-color: $background;
|
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$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
|
// These two don't change between themes. They are the $warning-color, but we don't
|
||||||
// want custom themes to affect them by accident.
|
// want custom themes to affect them by accident.
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$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-stop-border-color: $quinary-content;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||||
$voice-record-icon-color: $tertiary-content;
|
$voice-record-icon-color: $tertiary-content;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $system;
|
||||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
$voice-playback-button-fg-color: $secondary-content;
|
||||||
|
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
|
@ -51,6 +51,7 @@ import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||||
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
|
import { Skinner } from "../Skinner";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ declare global {
|
||||||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||||
mxActiveWidgetStore?: ActiveWidgetStore;
|
mxActiveWidgetStore?: ActiveWidgetStore;
|
||||||
|
mxSkinner?: Skinner;
|
||||||
mxOnRecaptchaLoaded?: () => void;
|
mxOnRecaptchaLoaded?: () => void;
|
||||||
electron?: Electron;
|
electron?: Electron;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +159,10 @@ declare global {
|
||||||
setSinkId(outputId: string);
|
setSinkId(outputId: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HTMLStyleElement {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Add Chrome-specific `instant` ScrollBehaviour
|
// Add Chrome-specific `instant` ScrollBehaviour
|
||||||
type _ScrollBehavior = ScrollBehavior | "instant";
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,11 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
declare module '!!raw-loader!*' {
|
||||||
ALL_MESSAGES,
|
const contents: string;
|
||||||
ALL_MESSAGES_LOUD,
|
export default contents;
|
||||||
MENTIONS_ONLY,
|
}
|
||||||
MUTE,
|
|
||||||
} from "./RoomNotifs";
|
|
||||||
|
|
||||||
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import * as sdk from './index';
|
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
|
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];
|
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +41,13 @@ function getIdServerDomain() {
|
||||||
* https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
|
* https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
|
||||||
*/
|
*/
|
||||||
export default class AddThreepid {
|
export default class AddThreepid {
|
||||||
|
private sessionId: string;
|
||||||
|
private submitUrl: string;
|
||||||
|
private clientSecret: string;
|
||||||
|
private bind: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
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
|
* @param {string} emailAddress The email address to add
|
||||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
* @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) => {
|
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
|
@ -72,7 +76,7 @@ export default class AddThreepid {
|
||||||
* @param {string} emailAddress The email address to add
|
* @param {string} emailAddress The email address to add
|
||||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
* @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;
|
this.bind = true;
|
||||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
// For separate bind, request a token directly from the IS.
|
// 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
|
* @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().
|
* @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(
|
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
|
@ -129,7 +133,7 @@ export default class AddThreepid {
|
||||||
* @param {string} phoneNumber The national or international formatted phone number to add
|
* @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().
|
* @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;
|
this.bind = true;
|
||||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
// For separate bind, request a token directly from the IS.
|
// 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
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the request failed.
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
async checkEmailLinkClicked() {
|
public async checkEmailLinkClicked(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
if (this.bind) {
|
if (this.bind) {
|
||||||
|
@ -175,7 +179,7 @@ export default class AddThreepid {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await this._makeAddThreepidOnlyRequest();
|
await this.makeAddThreepidOnlyRequest();
|
||||||
|
|
||||||
// The spec has always required this to use UI auth but synapse briefly
|
// 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.
|
// implemented it without, so this may just succeed and that's OK.
|
||||||
|
@ -186,9 +190,6 @@ export default class AddThreepid {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pop up an interactive auth dialog
|
|
||||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
|
||||||
|
|
||||||
const dialogAesthetics = {
|
const dialogAesthetics = {
|
||||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||||
title: _t("Use Single Sign On to continue"),
|
title: _t("Use Single Sign On to continue"),
|
||||||
|
@ -208,7 +209,7 @@ export default class AddThreepid {
|
||||||
title: _t("Add Email Address"),
|
title: _t("Add Email Address"),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
authData: e.data,
|
authData: e.data,
|
||||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||||
aestheticsForStagePhases: {
|
aestheticsForStagePhases: {
|
||||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||||
[SSOAuthEntry.UNSTABLE_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)
|
* @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({
|
return MatrixClientPeg.get().addThreePidOnly({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
auth,
|
auth,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a phone number verification code as entered by the user and validates
|
* 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
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the request failed.
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
async haveMsisdnToken(msisdnToken) {
|
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
|
||||||
const authClient = new IdentityAuthClient();
|
const authClient = new IdentityAuthClient();
|
||||||
const supportsSeparateAddAndBind =
|
const supportsSeparateAddAndBind =
|
||||||
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
||||||
|
@ -291,7 +292,7 @@ export default class AddThreepid {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await this._makeAddThreepidOnlyRequest();
|
await this.makeAddThreepidOnlyRequest();
|
||||||
|
|
||||||
// The spec has always required this to use UI auth but synapse briefly
|
// 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.
|
// implemented it without, so this may just succeed and that's OK.
|
||||||
|
@ -302,9 +303,6 @@ export default class AddThreepid {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pop up an interactive auth dialog
|
|
||||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
|
||||||
|
|
||||||
const dialogAesthetics = {
|
const dialogAesthetics = {
|
||||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||||
title: _t("Use Single Sign On to continue"),
|
title: _t("Use Single Sign On to continue"),
|
||||||
|
@ -324,7 +322,7 @@ export default class AddThreepid {
|
||||||
title: _t("Add Phone Number"),
|
title: _t("Add Phone Number"),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
authData: e.data,
|
authData: e.data,
|
||||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||||
aestheticsForStagePhases: {
|
aestheticsForStagePhases: {
|
||||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||||
[SSOAuthEntry.UNSTABLE_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
|
// space rooms cannot be DMs so skip the rest
|
||||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
|
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
|
||||||
|
|
||||||
let otherMember = null;
|
// If the room is not a DM don't fallback to a member avatar
|
||||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;
|
||||||
if (otherUserId) {
|
|
||||||
otherMember = room.getMember(otherUserId);
|
// If there are only two members in the DM use the avatar of the other member
|
||||||
} else {
|
const otherMember = room.getAvatarFallbackMember();
|
||||||
// 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 (otherMember?.getMxcAvatarUrl()) {
|
if (otherMember?.getMxcAvatarUrl()) {
|
||||||
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,3 +161,20 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
|
||||||
// Compare weekdays
|
// Compare weekdays
|
||||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
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 { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
||||||
import {
|
import {
|
||||||
|
@ -27,23 +27,25 @@ import {
|
||||||
doesIdentityServerHaveTerms,
|
doesIdentityServerHaveTerms,
|
||||||
useDefaultIdentityServer,
|
useDefaultIdentityServer,
|
||||||
} from './utils/IdentityServerUtils';
|
} from './utils/IdentityServerUtils';
|
||||||
import { abbreviateUrl } from './utils/UrlUtils';
|
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 class AbortedIdentityActionError extends Error {}
|
||||||
|
|
||||||
export default class IdentityAuthClient {
|
export default class IdentityAuthClient {
|
||||||
|
private accessToken: string;
|
||||||
|
private tempClient: MatrixClient;
|
||||||
|
private authEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new identity auth client
|
* Creates a new identity auth client
|
||||||
* @param {string} identityUrl The URL to contact the identity server with.
|
* @param {string} identityUrl The URL to contact the identity server with.
|
||||||
* When provided, this class will operate solely within memory, refusing to
|
* When provided, this class will operate solely within memory, refusing to
|
||||||
* persist any information such as tokens. Default null (not provided).
|
* persist any information such as tokens. Default null (not provided).
|
||||||
*/
|
*/
|
||||||
constructor(identityUrl = null) {
|
constructor(identityUrl?: string) {
|
||||||
this.accessToken = null;
|
|
||||||
this.authEnabled = true;
|
|
||||||
|
|
||||||
if (identityUrl) {
|
if (identityUrl) {
|
||||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
// 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
|
// do identity server auth. The functions don't take an identity URL
|
||||||
|
@ -54,32 +56,29 @@ export default class IdentityAuthClient {
|
||||||
baseUrl: "", // invalid by design
|
baseUrl: "", // invalid by design
|
||||||
idBaseUrl: identityUrl,
|
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();
|
return this.tempClient ? this.tempClient : MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeToken() {
|
private writeToken(): void {
|
||||||
if (this.tempClient) return; // temporary client: ignore
|
if (this.tempClient) return; // temporary client: ignore
|
||||||
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
_readToken() {
|
private readToken(): string {
|
||||||
if (this.tempClient) return null; // temporary client: ignore
|
if (this.tempClient) return null; // temporary client: ignore
|
||||||
return window.localStorage.getItem("mx_is_access_token");
|
return window.localStorage.getItem("mx_is_access_token");
|
||||||
}
|
}
|
||||||
|
|
||||||
hasCredentials() {
|
public hasCredentials(): boolean {
|
||||||
return this.accessToken != null; // undef or null
|
return Boolean(this.accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that resolves to the access_token string from the IS
|
// 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) {
|
if (!this.authEnabled) {
|
||||||
// The current IS doesn't support authentication
|
// The current IS doesn't support authentication
|
||||||
return null;
|
return null;
|
||||||
|
@ -87,21 +86,21 @@ export default class IdentityAuthClient {
|
||||||
|
|
||||||
let token = this.accessToken;
|
let token = this.accessToken;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = this._readToken();
|
token = this.readToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = await this.registerForToken(check);
|
token = await this.registerForToken(check);
|
||||||
if (token) {
|
if (token) {
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
this._writeToken();
|
this.writeToken();
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (check) {
|
if (check) {
|
||||||
try {
|
try {
|
||||||
await this._checkToken(token);
|
await this.checkToken(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
e instanceof TermsNotSignedError ||
|
e instanceof TermsNotSignedError ||
|
||||||
|
@ -114,7 +113,7 @@ export default class IdentityAuthClient {
|
||||||
token = await this.registerForToken();
|
token = await this.registerForToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
this._writeToken();
|
this.writeToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,11 +121,11 @@ export default class IdentityAuthClient {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkToken(token) {
|
private async checkToken(token: string): Promise<void> {
|
||||||
const identityServerUrl = this._matrixClient.getIdentityServerUrl();
|
const identityServerUrl = this.matrixClient.getIdentityServerUrl();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._matrixClient.getIdentityAccount(token);
|
await this.matrixClient.getIdentityAccount(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
||||||
logger.log("Identity server requires new terms to be agreed to");
|
logger.log("Identity server requires new terms to be agreed to");
|
||||||
|
@ -145,8 +144,8 @@ export default class IdentityAuthClient {
|
||||||
!doesAccountDataHaveIdentityServer() &&
|
!doesAccountDataHaveIdentityServer() &&
|
||||||
!(await doesIdentityServerHaveTerms(identityServerUrl))
|
!(await doesIdentityServerHaveTerms(identityServerUrl))
|
||||||
) {
|
) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const { finished } = Modal.createTrackedDialog(
|
||||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
'Default identity server terms warning', '',
|
||||||
QuestionDialog, {
|
QuestionDialog, {
|
||||||
title: _t("Identity server has no terms of service"),
|
title: _t("Identity server has no terms of service"),
|
||||||
description: (
|
description: (
|
||||||
|
@ -184,13 +183,13 @@ export default class IdentityAuthClient {
|
||||||
// See also https://github.com/vector-im/element-web/issues/10455.
|
// 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();
|
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||||
const { access_token: accessToken, token } =
|
const { access_token: accessToken, token } =
|
||||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||||
const identityAccessToken = token ? token : accessToken;
|
const identityAccessToken = token ? token : accessToken;
|
||||||
if (check) await this._checkToken(identityAccessToken);
|
if (check) await this.checkToken(identityAccessToken);
|
||||||
return identityAccessToken;
|
return identityAccessToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,21 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDom from "react-dom";
|
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.
|
* 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
|
* 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.
|
* automatic positional animation, look at react-shuffle or similar libraries.
|
||||||
*/
|
*/
|
||||||
export default class NodeAnimator extends React.Component {
|
export default class NodeAnimator extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private nodes = {};
|
||||||
// either a list of child nodes, or a single child.
|
private children: { [key: string]: React.DetailedReactHTMLElement<any, HTMLElement> };
|
||||||
children: PropTypes.any,
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
|
||||||
// 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 = {
|
|
||||||
startStyles: [],
|
startStyles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.nodes = {};
|
this.updateChildren(this.props.children);
|
||||||
this._updateChildren(this.props.children);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidUpdate(): void {
|
||||||
this._updateChildren(this.props.children);
|
this.updateChildren(this.props.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} node element to apply styles to
|
* @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}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_applyStyles(node, styles) {
|
private applyStyles(node: HTMLElement, styles: React.CSSProperties): void {
|
||||||
Object.entries(styles).forEach(([property, value]) => {
|
Object.entries(styles).forEach(([property, value]) => {
|
||||||
node.style[property] = value;
|
node.style[property] = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateChildren(newChildren) {
|
private updateChildren(newChildren: React.ReactNode): void {
|
||||||
const oldChildren = this.children || {};
|
const oldChildren = this.children || {};
|
||||||
this.children = {};
|
this.children = {};
|
||||||
React.Children.toArray(newChildren).forEach((c) => {
|
React.Children.toArray(newChildren).forEach((c: any) => {
|
||||||
if (oldChildren[c.key]) {
|
if (oldChildren[c.key]) {
|
||||||
const old = oldChildren[c.key];
|
const old = oldChildren[c.key];
|
||||||
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
||||||
|
|
||||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) {
|
||||||
this._applyStyles(oldNode, { 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);
|
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||||
}
|
}
|
||||||
// clone the old element with the props (and children) of the new element
|
// 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 {
|
} else {
|
||||||
// new element. If we have a startStyle, use that as the style and go through
|
// new element. If we have a startStyle, use that as the style and go through
|
||||||
// the enter animations
|
// the enter animations
|
||||||
const newProps = {};
|
const newProps: Partial<IChildProps> = {};
|
||||||
const restingStyle = c.props.style;
|
const restingStyle = c.props.style;
|
||||||
|
|
||||||
const startStyles = this.props.startStyles;
|
const startStyles = this.props.startStyles;
|
||||||
|
@ -76,7 +81,7 @@ export default class NodeAnimator extends React.Component {
|
||||||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
newProps.ref = ((n) => this._collectNode(
|
newProps.ref = ((n) => this.collectNode(
|
||||||
c.key, n, restingStyle,
|
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 (
|
if (
|
||||||
node &&
|
node &&
|
||||||
this.nodes[k] === undefined &&
|
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
|
// start from startStyle 1: 0 is the one we gave it
|
||||||
// to start with, so now we animate 1 etc.
|
// to start with, so now we animate 1 etc.
|
||||||
for (let i = 1; i < startStyles.length; ++i) {
|
for (let i = 1; i < startStyles.length; ++i) {
|
||||||
this._applyStyles(domNode, startStyles[i]);
|
this.applyStyles(domNode as HTMLElement, startStyles[i]);
|
||||||
// console.log("start:"
|
// console.log("start:"
|
||||||
// JSON.stringify(startStyles[i]),
|
// JSON.stringify(startStyles[i]),
|
||||||
// );
|
// );
|
||||||
|
@ -104,7 +109,7 @@ export default class NodeAnimator extends React.Component {
|
||||||
|
|
||||||
// and then we animate to the resting state
|
// and then we animate to the resting state
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._applyStyles(domNode, restingStyle);
|
this.applyStyles(domNode as HTMLElement, restingStyle);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// console.log("enter:",
|
// console.log("enter:",
|
||||||
|
@ -113,7 +118,7 @@ export default class NodeAnimator extends React.Component {
|
||||||
this.nodes[k] = node;
|
this.nodes[k] = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>{ Object.values(this.children) }</>
|
<>{ Object.values(this.children) }</>
|
||||||
);
|
);
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** The types of page which can be shown by the LoggedInView */
|
/** The types of page which can be shown by the LoggedInView */
|
||||||
export default {
|
enum PageType {
|
||||||
HomePage: "home_page",
|
HomePage = "home_page",
|
||||||
RoomView: "room_view",
|
RoomView = "room_view",
|
||||||
RoomDirectory: "room_directory",
|
RoomDirectory = "room_directory",
|
||||||
UserView: "user_view",
|
UserView = "user_view",
|
||||||
GroupView: "group_view",
|
GroupView = "group_view",
|
||||||
MyGroups: "my_groups",
|
MyGroups = "my_groups",
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default PageType;
|
|
@ -20,10 +20,11 @@ limitations under the License.
|
||||||
* registration code.
|
* registration code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import * as sdk from './index';
|
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
|
|
||||||
// Regex for what a "safe" or "Matrix-looking" localpart would be.
|
// 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
|
// 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
|
* @param {bool} options.screen_after
|
||||||
* If present the screen to redirect to after a successful login or register.
|
* 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 = {};
|
if (options === undefined) options = {};
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||||
hasCancelButton: true,
|
hasCancelButton: true,
|
||||||
quitOnly: true,
|
quitOnly: true,
|
|
@ -42,10 +42,15 @@ export interface IInviteResult {
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @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 {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
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
export function inviteMultipleToRoom(
|
||||||
const inviter = new MultiInviter(roomId);
|
roomId: string,
|
||||||
|
addresses: string[],
|
||||||
|
progressCallback?: () => void,
|
||||||
|
): Promise<IInviteResult> {
|
||||||
|
const inviter = new MultiInviter(roomId, progressCallback);
|
||||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
showAnyInviteErrors(result.states, room, result.inviter);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
@ -17,27 +17,31 @@ limitations under the License.
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
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 enum RoomNotifState {
|
||||||
export const ALL_MESSAGES = 'all_messages';
|
AllMessagesLoud = 'all_messages_loud',
|
||||||
export const MENTIONS_ONLY = 'mentions_only';
|
AllMessages = 'all_messages',
|
||||||
export const MUTE = 'mute';
|
MentionsOnly = 'mentions_only',
|
||||||
|
Mute = 'mute',
|
||||||
|
}
|
||||||
|
|
||||||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
export const BADGE_STATES = [RoomNotifState.AllMessages, RoomNotifState.AllMessagesLoud];
|
||||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
export const MENTION_BADGE_STATES = [...BADGE_STATES, RoomNotifState.MentionsOnly];
|
||||||
|
|
||||||
export function shouldShowNotifBadge(roomNotifState) {
|
export function shouldShowNotifBadge(roomNotifState: RoomNotifState): boolean {
|
||||||
return BADGE_STATES.includes(roomNotifState);
|
return BADGE_STATES.includes(roomNotifState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldShowMentionBadge(roomNotifState) {
|
export function shouldShowMentionBadge(roomNotifState: RoomNotifState): boolean {
|
||||||
return MENTION_BADGE_STATES.includes(roomNotifState);
|
return MENTION_BADGE_STATES.includes(roomNotifState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateNotificationCount(rooms) {
|
export function aggregateNotificationCount(rooms: Room[]): {count: number, highlight: boolean} {
|
||||||
return rooms.reduce((result, room) => {
|
return rooms.reduce<{count: number, highlight: boolean}>((result, room) => {
|
||||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
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
|
// use helper method to include highlights in the previous version of the room
|
||||||
const notificationCount = getUnreadNotificationCount(room);
|
const notificationCount = getUnreadNotificationCount(room);
|
||||||
|
|
||||||
|
@ -55,9 +59,9 @@ export function aggregateNotificationCount(rooms) {
|
||||||
}, { count: 0, highlight: false });
|
}, { count: 0, highlight: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoomHasBadge(room) {
|
export function getRoomHasBadge(room: Room): boolean {
|
||||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
|
||||||
const notificationCount = room.getUnreadNotificationCount();
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||||
|
@ -66,14 +70,14 @@ export function getRoomHasBadge(room) {
|
||||||
return notifBadges || mentionBadges;
|
return notifBadges || mentionBadges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoomNotifsState(roomId) {
|
export function getRoomNotifsState(roomId: string): RoomNotifState {
|
||||||
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
|
if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages;
|
||||||
|
|
||||||
// look through the override rules for a rule affecting this room:
|
// look through the override rules for a rule affecting this room:
|
||||||
// if one exists, it will take precedence.
|
// if one exists, it will take precedence.
|
||||||
const muteRule = findOverrideMuteRule(roomId);
|
const muteRule = findOverrideMuteRule(roomId);
|
||||||
if (muteRule) {
|
if (muteRule) {
|
||||||
return MUTE;
|
return RoomNotifState.Mute;
|
||||||
}
|
}
|
||||||
|
|
||||||
// for everything else, look at the room rule.
|
// 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
|
// 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
|
// (in particular this will be 'wrong' for one to one rooms because
|
||||||
// they will notify loudly for all messages)
|
// 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
|
// a mute at the room level will still allow mentions
|
||||||
// to notify
|
// to notify
|
||||||
if (isMuteRule(roomRule)) return MENTIONS_ONLY;
|
if (isMuteRule(roomRule)) return RoomNotifState.MentionsOnly;
|
||||||
|
|
||||||
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
|
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
|
||||||
if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD;
|
if (actionsObject.tweaks.sound) return RoomNotifState.AllMessagesLoud;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRoomNotifsState(roomId, newState) {
|
export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Promise<void> {
|
||||||
if (newState === MUTE) {
|
if (newState === RoomNotifState.Mute) {
|
||||||
return setRoomNotifsStateMuted(roomId);
|
return setRoomNotifsStateMuted(roomId);
|
||||||
} else {
|
} else {
|
||||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnreadNotificationCount(room, type=null) {
|
export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number {
|
||||||
let notificationCount = room.getUnreadNotificationCount(type);
|
let notificationCount = room.getUnreadNotificationCount(type);
|
||||||
|
|
||||||
// Check notification counts in the old room just in case there's some lost
|
// 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
|
// notifying the user for unread messages because they would have extreme
|
||||||
// difficulty changing their notification preferences away from "All Messages"
|
// difficulty changing their notification preferences away from "All Messages"
|
||||||
// and "Noisy".
|
// and "Noisy".
|
||||||
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
|
notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return notificationCount;
|
return notificationCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRoomNotifsStateMuted(roomId) {
|
function setRoomNotifsStateMuted(roomId: string): Promise<any> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
// delete the room rule
|
// delete the room rule
|
||||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||||
if (roomRule) {
|
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
|
// 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
|
// 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
|
// though, so using the room ID as the rule ID is logical and prevents
|
||||||
// duplicate copies of the rule.
|
// duplicate copies of the rule.
|
||||||
promises.push(cli.addPushRule('global', 'override', roomId, {
|
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
kind: 'event_match',
|
kind: 'event_match',
|
||||||
|
@ -162,30 +166,30 @@ function setRoomNotifsStateMuted(roomId) {
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise<any> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
const overrideMuteRule = findOverrideMuteRule(roomId);
|
const overrideMuteRule = findOverrideMuteRule(roomId);
|
||||||
if (overrideMuteRule) {
|
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);
|
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||||
if (roomRule) {
|
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') {
|
} else if (newState === RoomNotifState.MentionsOnly) {
|
||||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||||
actions: [
|
actions: [
|
||||||
'dont_notify',
|
'dont_notify',
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// https://matrix.org/jira/browse/SPEC-400
|
||||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
|
||||||
} else if ('all_messages_loud') {
|
} else if (newState === RoomNotifState.AllMessagesLoud) {
|
||||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||||
actions: [
|
actions: [
|
||||||
'notify',
|
'notify',
|
||||||
{
|
{
|
||||||
|
@ -195,13 +199,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// 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);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId) {
|
function findOverrideMuteRule(roomId: string): IAnnotatedPushRule {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli.pushRules ||
|
if (!cli.pushRules ||
|
||||||
!cli.pushRules['global'] ||
|
!cli.pushRules['global'] ||
|
||||||
|
@ -218,7 +222,7 @@ function findOverrideMuteRule(roomId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRuleForRoom(roomId, rule) {
|
function isRuleForRoom(roomId: string, rule: IAnnotatedPushRule): boolean {
|
||||||
if (rule.conditions.length !== 1) {
|
if (rule.conditions.length !== 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -226,6 +230,6 @@ function isRuleForRoom(roomId, rule) {
|
||||||
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
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');
|
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";
|
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);
|
const data = objectClone(event.data);
|
||||||
data.response = res;
|
data.response = res;
|
||||||
|
// @ts-ignore
|
||||||
event.source.postMessage(data, event.origin);
|
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);
|
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||||
const data = objectClone(event.data);
|
const data = objectClone(event.data);
|
||||||
data.response = {
|
data.response = {
|
||||||
|
@ -264,10 +282,11 @@ function sendError(event, msg, nestedError) {
|
||||||
if (nestedError) {
|
if (nestedError) {
|
||||||
data.response.error._error = nestedError;
|
data.response.error._error = nestedError;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
event.source.postMessage(data, event.origin);
|
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}`);
|
logger.log(`Received request to invite ${userId} into room ${roomId}`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
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;
|
const widgetId = event.data.widget_id;
|
||||||
let widgetType = event.data.type;
|
let widgetType = event.data.type;
|
||||||
const widgetUrl = event.data.url;
|
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();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
|
@ -382,7 +401,7 @@ function getWidgets(event, roomId) {
|
||||||
sendResponse(event, widgetStateEvents);
|
sendResponse(event, widgetStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoomEncState(event, roomId) {
|
function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
|
@ -398,7 +417,7 @@ function getRoomEncState(event, roomId) {
|
||||||
sendResponse(event, roomIsEncrypted);
|
sendResponse(event, roomIsEncrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPlumbingState(event, roomId, status) {
|
function setPlumbingState(event: MessageEvent<any>, roomId: string, status: string): void {
|
||||||
if (typeof status !== 'string') {
|
if (typeof status !== 'string') {
|
||||||
throw new Error('Plumbing state status should be a 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}`);
|
logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
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)) {
|
if (!(Number.isInteger(level) && level >= 0)) {
|
||||||
sendError(event, _t('Power level must be positive integer.'));
|
sendError(event, _t('Power level must be positive integer.'));
|
||||||
return;
|
return;
|
||||||
|
@ -446,40 +467,52 @@ function setBotPower(event, roomId, userId, level) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
try {
|
||||||
const powerEvent = new MatrixEvent(
|
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",
|
type: "m.room.power_levels",
|
||||||
content: powerLevels,
|
content: powerLevels,
|
||||||
},
|
},
|
||||||
);
|
));
|
||||||
|
return sendResponse(event, {
|
||||||
client.setPowerLevel(roomId, userId, level, powerEvent).then(() => {
|
|
||||||
sendResponse(event, {
|
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
} catch (err) {
|
||||||
sendError(event, err.message ? err.message : _t('Failed to send request.'), 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.`);
|
logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
||||||
returnStateEvent(event, roomId, "m.room.member", userId);
|
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.`);
|
logger.log(`join_rules of ${roomId} requested.`);
|
||||||
returnStateEvent(event, roomId, "m.room.join_rules", "");
|
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.`);
|
logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
||||||
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMembershipCount(event, roomId) {
|
function getMembershipCount(event: MessageEvent<any>, roomId: string): void {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
|
@ -494,7 +527,7 @@ function getMembershipCount(event, roomId) {
|
||||||
sendResponse(event, count);
|
sendResponse(event, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSendEvent(event, roomId) {
|
function canSendEvent(event: MessageEvent<any>, roomId: string): void {
|
||||||
const evType = "" + event.data.event_type; // force stringify
|
const evType = "" + event.data.event_type; // force stringify
|
||||||
const isState = Boolean(event.data.is_state);
|
const isState = Boolean(event.data.is_state);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -528,7 +561,7 @@ function canSendEvent(event, roomId) {
|
||||||
sendResponse(event, true);
|
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();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
|
@ -547,8 +580,9 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
sendResponse(event, stateEvent.getContent());
|
sendResponse(event, stateEvent.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMessage = function(event) {
|
const onMessage = function(event: MessageEvent<any>): void {
|
||||||
if (!event.origin) { // stupid chrome
|
if (!event.origin) { // stupid chrome
|
||||||
|
// @ts-ignore
|
||||||
event.origin = event.originalEvent.origin;
|
event.origin = event.originalEvent.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,8 +616,8 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data.action === "close_scalar") {
|
if (event.data.action === Action.CloseScalar) {
|
||||||
dis.dispatch({ action: "close_scalar" });
|
dis.dispatch({ action: Action.CloseScalar });
|
||||||
sendResponse(event, null);
|
sendResponse(event, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -596,10 +630,10 @@ const onMessage = function(event) {
|
||||||
// Get and set user widgets (not associated with a specific room)
|
// Get and set user widgets (not associated with a specific room)
|
||||||
// If roomId is specified, it must be validated, so room-based widgets agreed
|
// If roomId is specified, it must be validated, so room-based widgets agreed
|
||||||
// handled further down.
|
// handled further down.
|
||||||
if (event.data.action === "get_widgets") {
|
if (event.data.action === Action.GetWidgets) {
|
||||||
getWidgets(event, null);
|
getWidgets(event, null);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_widget") {
|
} else if (event.data.action === Action.SetWidgets) {
|
||||||
setWidget(event, null);
|
setWidget(event, null);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -614,28 +648,28 @@ const onMessage = function(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get and set room-based widgets
|
// Get and set room-based widgets
|
||||||
if (event.data.action === "get_widgets") {
|
if (event.data.action === Action.GetWidgets) {
|
||||||
getWidgets(event, roomId);
|
getWidgets(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_widget") {
|
} else if (event.data.action === Action.SetWidget) {
|
||||||
setWidget(event, roomId);
|
setWidget(event, roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These APIs don't require userId
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === Action.JoinRulesState) {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_plumbing_state") {
|
} else if (event.data.action === Action.SetPlumbingState) {
|
||||||
setPlumbingState(event, roomId, event.data.status);
|
setPlumbingState(event, roomId, event.data.status);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "get_membership_count") {
|
} else if (event.data.action === Action.GetMembershipCount) {
|
||||||
getMembershipCount(event, roomId);
|
getMembershipCount(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "get_room_enc_state") {
|
} else if (event.data.action === Action.GetRoomEncryptionState) {
|
||||||
getRoomEncState(event, roomId);
|
getRoomEncState(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "can_send_event") {
|
} else if (event.data.action === Action.CanSendEvent) {
|
||||||
canSendEvent(event, roomId);
|
canSendEvent(event, roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -645,20 +679,20 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (event.data.action) {
|
switch (event.data.action) {
|
||||||
case "membership_state":
|
case Action.MembershipState:
|
||||||
getMembershipState(event, roomId, userId);
|
getMembershipState(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
case "invite":
|
case Action.invite:
|
||||||
inviteUser(event, roomId, userId);
|
inviteUser(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
case "bot_options":
|
case Action.BotOptions:
|
||||||
botOptions(event, roomId, userId);
|
botOptions(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
case "set_bot_options":
|
case Action.SetBotOptions:
|
||||||
setBotOptions(event, roomId, userId);
|
setBotOptions(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
case "set_bot_power":
|
case Action.SetBotPower:
|
||||||
setBotPower(event, roomId, userId, event.data.level);
|
setBotPower(event, roomId, userId, event.data.level, event.data.ignoreIfGreater);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
|
@ -667,16 +701,16 @@ const onMessage = function(event) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let listenerCount = 0;
|
let listenerCount = 0;
|
||||||
let openManagerUrl = null;
|
let openManagerUrl: string = null;
|
||||||
|
|
||||||
export function startListening() {
|
export function startListening(): void {
|
||||||
if (listenerCount === 0) {
|
if (listenerCount === 0) {
|
||||||
window.addEventListener("message", onMessage, false);
|
window.addEventListener("message", onMessage, false);
|
||||||
}
|
}
|
||||||
listenerCount += 1;
|
listenerCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopListening() {
|
export function stopListening(): void {
|
||||||
listenerCount -= 1;
|
listenerCount -= 1;
|
||||||
if (listenerCount === 0) {
|
if (listenerCount === 0) {
|
||||||
window.removeEventListener("message", onMessage);
|
window.removeEventListener("message", onMessage);
|
||||||
|
@ -691,6 +725,6 @@ export function stopListening() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setOpenManagerUrl(url) {
|
export function setOpenManagerUrl(url: string): void {
|
||||||
openManagerUrl = url;
|
openManagerUrl = url;
|
||||||
}
|
}
|
|
@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Skinner {
|
import React from "react";
|
||||||
constructor() {
|
|
||||||
this.components = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!name) throw new Error(`Invalid component name: ${name}`);
|
||||||
if (this.components === null) {
|
if (this.components === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -30,7 +38,7 @@ class Skinner {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const doLookup = (components) => {
|
const doLookup = (components: IComponents): React.Component => {
|
||||||
if (!components) return null;
|
if (!components) return null;
|
||||||
let comp = components[name];
|
let comp = components[name];
|
||||||
// XXX: Temporarily also try 'views.' as we're currently
|
// XXX: Temporarily also try 'views.' as we're currently
|
||||||
|
@ -58,7 +66,7 @@ class Skinner {
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
load(skinObject) {
|
public load(skinObject: ISkinObject): void {
|
||||||
if (this.components !== null) {
|
if (this.components !== null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load a skin while a skin is already loaded"+
|
"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
|
// Now that we have a skin, load our components too
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const idx = require("./component-index");
|
const idx = require("./component-index");
|
||||||
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
|
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
|
||||||
for (const c in idx.components) {
|
for (const c in idx.components) {
|
||||||
|
@ -79,7 +88,7 @@ class Skinner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponent(name, comp) {
|
public addComponent(name: string, comp: any) {
|
||||||
let slot = name;
|
let slot = name;
|
||||||
if (comp.replaces !== undefined) {
|
if (comp.replaces !== undefined) {
|
||||||
if (comp.replaces.indexOf('.') > -1) {
|
if (comp.replaces.indexOf('.') > -1) {
|
||||||
|
@ -91,7 +100,7 @@ class Skinner {
|
||||||
this.components[slot] = comp;
|
this.components[slot] = comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
public reset(): void {
|
||||||
this.components = null;
|
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/
|
// 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
|
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||||
// ("Modules are cached based on their resolved filename")
|
// ("Modules are cached based on their resolved filename")
|
||||||
if (global.mxSkinner === undefined) {
|
if (window.mxSkinner === undefined) {
|
||||||
global.mxSkinner = new Skinner();
|
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 { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { UIFeature } from "./settings/UIFeature";
|
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||||
import { CHAT_EFFECTS } from "./effects";
|
import { CHAT_EFFECTS } from "./effects";
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from "./CallHandler";
|
||||||
import { guessAndSetDMRoom } from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
|
@ -56,6 +56,7 @@ import InfoDialog from "./components/views/dialogs/InfoDialog";
|
||||||
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
interface HTMLInputEvent extends Event {
|
interface HTMLInputEvent extends Event {
|
||||||
|
@ -403,6 +404,7 @@ export const Commands = [
|
||||||
command: 'invite',
|
command: 'invite',
|
||||||
args: '<user-id> [<reason>]',
|
args: '<user-id> [<reason>]',
|
||||||
description: _td('Invites user with given id to current room'),
|
description: _td('Invites user with given id to current room'),
|
||||||
|
isEnabled: () => shouldShowComponent(UIComponent.InviteUsers),
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const [address, reason] = args.split(/\s+(.+)/);
|
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 {
|
function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
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 {
|
function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
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") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
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;
|
return message;
|
||||||
};
|
};
|
||||||
|
@ -669,6 +690,7 @@ interface IHandlers {
|
||||||
|
|
||||||
const handlers: IHandlers = {
|
const handlers: IHandlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
|
'm.sticker': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -677,6 +699,7 @@ const stateHandlers: IHandlers = {
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.room.name': textForRoomNameEvent,
|
||||||
'm.room.topic': textForTopicEvent,
|
'm.room.topic': textForTopicEvent,
|
||||||
'm.room.member': textForMemberEvent,
|
'm.room.member': textForMemberEvent,
|
||||||
|
"m.room.avatar": textForRoomAvatarEvent,
|
||||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||||
'm.room.power_levels': textForPowerEvent,
|
'm.room.power_levels': textForPowerEvent,
|
||||||
|
|
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
|
|
||||||
export async function getThreepidsWithBindStatus(client, filterMedium) {
|
export async function getThreepidsWithBindStatus(
|
||||||
|
client: MatrixClient, filterMedium?: ThreepidMedium,
|
||||||
|
): Promise<IThreepid[]> {
|
||||||
const userId = client.getUserId();
|
const userId = client.getUserId();
|
||||||
|
|
||||||
let { threepids } = await client.getThreePids();
|
let { threepids } = await client.getThreePids();
|
||||||
|
@ -31,7 +35,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) {
|
||||||
const identityAccessToken = await authClient.getAccessToken({ check: false });
|
const identityAccessToken = await authClient.getAccessToken({ check: false });
|
||||||
|
|
||||||
// Restructure for lookup query
|
// 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);
|
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
|
||||||
|
|
||||||
// Record which are already bound
|
// Record which are already bound
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import FocusLock from "react-focus-lock";
|
||||||
|
|
||||||
import { Key } from "../../Keyboard";
|
import { Key } from "../../Keyboard";
|
||||||
import { Writeable } from "../../@types/common";
|
import { Writeable } from "../../@types/common";
|
||||||
|
@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
|
||||||
|
|
||||||
export interface IPosition {
|
export interface IPosition {
|
||||||
top?: number;
|
top?: number;
|
||||||
bottom?: 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.
|
// it will be mounted to a container at the root of the DOM.
|
||||||
mountAsChild?: boolean;
|
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
|
// Function to be called on menu close
|
||||||
onFinished();
|
onFinished();
|
||||||
// on resize callback
|
// 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.
|
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||||
@replaceableComponent("structures.ContextMenu")
|
@replaceableComponent("structures.ContextMenu")
|
||||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
private initialFocus: HTMLElement;
|
private readonly initialFocus: HTMLElement;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
|
@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuElem: null,
|
contextMenuElem: null,
|
||||||
};
|
};
|
||||||
|
@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
this.initialFocus.focus();
|
this.initialFocus.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectContextMenuRect = (element) => {
|
private collectContextMenuRect = (element: HTMLDivElement) => {
|
||||||
// We don't need to clean up when unmounting, so ignore
|
// We don't need to clean up when unmounting, so ignore
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
let first = element.querySelector('[role^="menuitem"]');
|
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
|
||||||
if (!first) {
|
|| element.querySelector<HTMLElement>('[tab-index]');
|
||||||
first = element.querySelector('[tab-index]');
|
|
||||||
}
|
|
||||||
if (first) {
|
if (first) {
|
||||||
first.focus();
|
first.focus();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
descending = true;
|
descending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
(element as HTMLElement).focus();
|
(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) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
// don't let keyboard handling escape the context menu
|
// don't let keyboard handling escape the context menu
|
||||||
ev.stopPropagation();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||||
style={{ ...position, ...wrapperStyle }}
|
style={{ ...position, ...wrapperStyle }}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
onClick={this.onClick}
|
||||||
onContextMenu={this.onContextMenuPreventBubbling}
|
onContextMenu={this.onContextMenuPreventBubbling}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -391,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
ref={this.collectContextMenuRect}
|
ref={this.collectContextMenuRect}
|
||||||
role={this.props.managed ? "menu" : undefined}
|
role={this.props.managed ? "menu" : undefined}
|
||||||
>
|
>
|
||||||
{ chevron }
|
{ body }
|
||||||
{ props.children }
|
|
||||||
</div>
|
</div>
|
||||||
{ background }
|
{ background }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,6 +37,7 @@ import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from '../views/rooms/EventTile';
|
import { TileShape } from '../views/rooms/EventTile';
|
||||||
import { Layout } from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -57,6 +58,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
// added to the timeline.
|
// added to the timeline.
|
||||||
private decryptingEvents = new Set<string>();
|
private decryptingEvents = new Set<string>();
|
||||||
public noRoom: boolean;
|
public noRoom: boolean;
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
timelineSet: null,
|
timelineSet: null,
|
||||||
|
@ -249,9 +251,11 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||||
|
|
||||||
if (this.state.timelineSet) {
|
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 (
|
return (
|
||||||
|
<RoomContext.Provider value={{
|
||||||
|
...this.context,
|
||||||
|
timelineRenderingType: TimelineRenderingType.File,
|
||||||
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_FilePanel"
|
className="mx_FilePanel"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -271,9 +275,14 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
layout={Layout.Group}
|
layout={Layout.Group}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
<RoomContext.Provider value={{
|
||||||
|
...this.context,
|
||||||
|
timelineRenderingType: TimelineRenderingType.File,
|
||||||
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_FilePanel"
|
className="mx_FilePanel"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -281,6 +290,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ interface IState {
|
||||||
stageState?: IStageStatus;
|
stageState?: IStageStatus;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
errorText?: string;
|
errorText?: string;
|
||||||
stageErrorText?: string;
|
errorCode?: string;
|
||||||
submitButtonEnabled: boolean;
|
submitButtonEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
authStage: null,
|
authStage: null,
|
||||||
busy: false,
|
busy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
stageErrorText: null,
|
errorCode: null,
|
||||||
submitButtonEnabled: false,
|
submitButtonEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -145,6 +145,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
const msg = error.message || error.toString();
|
const msg = error.message || error.toString();
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: msg,
|
errorText: msg,
|
||||||
|
errorCode: error.errcode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -186,6 +187,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
authStage: stageType,
|
authStage: stageType,
|
||||||
stageState: stageState,
|
stageState: stageState,
|
||||||
errorText: stageState.error,
|
errorText: stageState.error,
|
||||||
|
errorCode: stageState.errcode,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (oldStage !== stageType) {
|
if (oldStage !== stageType) {
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
|
@ -208,7 +210,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
stageErrorText: null,
|
errorCode: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
// 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);
|
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;
|
const stage = this.state.authStage;
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
|
@ -255,7 +265,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
clientSecret={this.authLogic.getClientSecret()}
|
clientSecret={this.authLogic.getClientSecret()}
|
||||||
stageParams={this.authLogic.getStageParams(stage)}
|
stageParams={this.authLogic.getStageParams(stage)}
|
||||||
submitAuthDict={this.submitAuthDict}
|
submitAuthDict={this.submitAuthDict}
|
||||||
errorText={this.state.stageErrorText}
|
errorText={this.state.errorText}
|
||||||
|
errorCode={this.state.errorCode}
|
||||||
busy={this.state.busy}
|
busy={this.state.busy}
|
||||||
inputs={this.props.inputs}
|
inputs={this.props.inputs}
|
||||||
stageState={this.state.stageState}
|
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';
|
import * as Lifecycle from '../../Lifecycle';
|
||||||
// LifecycleStore is not used but does listen to and dispatch actions
|
// LifecycleStore is not used but does listen to and dispatch actions
|
||||||
import '../../stores/LifecycleStore';
|
import '../../stores/LifecycleStore';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageType from '../../PageTypes';
|
||||||
|
|
||||||
import createRoom, { IOpts } from "../../createRoom";
|
import createRoom, { IOpts } from "../../createRoom";
|
||||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||||
|
@ -208,7 +208,7 @@ interface IState {
|
||||||
view: Views;
|
view: Views;
|
||||||
// What the LoggedInView would be showing if visible
|
// What the LoggedInView would be showing if visible
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
page_type?: PageTypes;
|
page_type?: PageType;
|
||||||
// The ID of the room we're viewing. This is either populated directly
|
// 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
|
// in the case where we view a room by ID or by RoomView when it resolves
|
||||||
// what ID an alias points at.
|
// what ID an alias points at.
|
||||||
|
@ -724,7 +724,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'view_my_groups':
|
case 'view_my_groups':
|
||||||
this.setPage(PageTypes.MyGroups);
|
this.setPage(PageType.MyGroups);
|
||||||
this.notifyNewScreen('groups');
|
this.notifyNewScreen('groups');
|
||||||
break;
|
break;
|
||||||
case 'view_group':
|
case 'view_group':
|
||||||
|
@ -763,7 +763,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
localStorage.setItem("mx_seenSpacesBeta", "1");
|
localStorage.setItem("mx_seenSpacesBeta", "1");
|
||||||
// We just dispatch the page change rather than have to worry about
|
// We just dispatch the page change rather than have to worry about
|
||||||
// what the logic is for each of these branches.
|
// 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' });
|
dis.dispatch({ action: 'view_last_screen' });
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({ action: 'view_my_groups' });
|
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({
|
this.setState({
|
||||||
page_type: pageType,
|
page_type: pageType,
|
||||||
});
|
});
|
||||||
|
@ -956,7 +956,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
view: Views.LOGGED_IN,
|
view: Views.LOGGED_IN,
|
||||||
currentRoomId: roomInfo.room_id || null,
|
currentRoomId: roomInfo.room_id || null,
|
||||||
page_type: PageTypes.RoomView,
|
page_type: PageType.RoomView,
|
||||||
threepidInvite: roomInfo.threepid_invite,
|
threepidInvite: roomInfo.threepid_invite,
|
||||||
roomOobData: roomInfo.oob_data,
|
roomOobData: roomInfo.oob_data,
|
||||||
ready: true,
|
ready: true,
|
||||||
|
@ -984,7 +984,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
currentGroupId: groupId,
|
currentGroupId: groupId,
|
||||||
currentGroupIsNew: payload.group_is_new,
|
currentGroupIsNew: payload.group_is_new,
|
||||||
});
|
});
|
||||||
this.setPage(PageTypes.GroupView);
|
this.setPage(PageType.GroupView);
|
||||||
this.notifyNewScreen('group/' + groupId);
|
this.notifyNewScreen('group/' + groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1027,7 +1027,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
justRegistered,
|
justRegistered,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
this.setPage(PageTypes.HomePage);
|
this.setPage(PageType.HomePage);
|
||||||
this.notifyNewScreen('home');
|
this.notifyNewScreen('home');
|
||||||
ThemeController.isLogin = false;
|
ThemeController.isLogin = false;
|
||||||
this.themeWatcher.recheck();
|
this.themeWatcher.recheck();
|
||||||
|
@ -1045,7 +1045,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
this.notifyNewScreen('user/' + userId);
|
this.notifyNewScreen('user/' + userId);
|
||||||
this.setState({ currentUserId: 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 TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
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 CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
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
|
// 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
|
// 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,
|
prevEvent: MatrixEvent,
|
||||||
mxEvent: MatrixEvent,
|
mxEvent: MatrixEvent,
|
||||||
showHiddenEvents: boolean,
|
showHiddenEvents: boolean,
|
||||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
ghostReadMarkers,
|
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 => {
|
private calculateRoomMembersCount = (): void => {
|
||||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
return { nextEvent, nextTile };
|
return { nextEvent, nextTile };
|
||||||
}
|
}
|
||||||
|
|
||||||
private get roomHasPendingEdit(): string {
|
private get pendingEditItem(): string | undefined {
|
||||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
try {
|
||||||
|
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEventTiles(): ReactNode[] {
|
private getEventTiles(): ReactNode[] {
|
||||||
this.eventNodes = {};
|
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) {
|
if (grouper) {
|
||||||
ret.push(...grouper.getTiles());
|
ret.push(...grouper.getTiles());
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from "../views/rooms/EventTile";
|
import { TileShape } from "../views/rooms/EventTile";
|
||||||
import { Layout } from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
@ -34,6 +35,7 @@ interface IProps {
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.NotificationPanel")
|
@replaceableComponent("structures.NotificationPanel")
|
||||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
|
static contextType = RoomContext;
|
||||||
render() {
|
render() {
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{ _t('You’re all caught up') }</h2>
|
<h2>{ _t('You’re all caught up') }</h2>
|
||||||
|
@ -61,8 +63,13 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
content = <Spinner />;
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
return <RoomContext.Provider value={{
|
||||||
|
...this.context,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Notification,
|
||||||
|
}}>
|
||||||
|
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||||
{ content }
|
{ content }
|
||||||
</BaseCard>;
|
</BaseCard>
|
||||||
|
</RoomContext.Provider>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
import RoomContext from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
||||||
debuglog = logger.log.bind(console);
|
debuglog = logger.log.bind(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IRoomProps extends MatrixClientProps {
|
||||||
threepidInvite: IThreepidInvite;
|
threepidInvite: IThreepidInvite;
|
||||||
oobData?: IOOBData;
|
oobData?: IOOBData;
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ interface IProps {
|
||||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IState {
|
export interface IRoomState {
|
||||||
room?: Room;
|
room?: Room;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
roomAlias?: 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.
|
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||||
wasContextSwitch?: boolean;
|
wasContextSwitch?: boolean;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
|
timelineRenderingType: TimelineRenderingType;
|
||||||
|
liveTimeline?: EventTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomView")
|
@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 dispatcherRef: string;
|
||||||
private readonly roomStoreToken: EventSubscription;
|
private readonly roomStoreToken: EventSubscription;
|
||||||
private readonly rightPanelStoreToken: EventSubscription;
|
private readonly rightPanelStoreToken: EventSubscription;
|
||||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
showDisplaynameChanges: true,
|
showDisplaynameChanges: true,
|
||||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
|
liveTimeline: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const roomId = RoomViewStore.getRoomId();
|
const roomId = RoomViewStore.getRoomId();
|
||||||
|
|
||||||
const newState: Pick<IState, any> = {
|
const newState: Pick<IRoomState, any> = {
|
||||||
roomId,
|
roomId,
|
||||||
roomAlias: RoomViewStore.getRoomAlias(),
|
roomAlias: RoomViewStore.getRoomAlias(),
|
||||||
roomLoading: RoomViewStore.isRoomLoading(),
|
roomLoading: RoomViewStore.isRoomLoading(),
|
||||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.onSearchClick();
|
this.onSearchClick();
|
||||||
break;
|
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;
|
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||||
this.setState({ editState }, () => {
|
this.setState({ editState }, () => {
|
||||||
if (payload.event) {
|
if (payload.event) {
|
||||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.updateE2EStatus(room);
|
this.updateE2EStatus(room);
|
||||||
this.updatePermissions(room);
|
this.updatePermissions(room);
|
||||||
this.checkWidgets(room);
|
this.checkWidgets(room);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
liveTimeline: room.getLiveTimeline(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room) {
|
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, {
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
KeyboardEvent,
|
||||||
|
KeyboardEventHandler,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
KeyboardEvent,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
useContext,
|
|
||||||
SetStateAction,
|
|
||||||
Dispatch,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
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 { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import classNames from "classnames";
|
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 dis from "../../dispatcher/dispatcher";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
@ -333,6 +334,30 @@ interface IHierarchyLevelProps {
|
||||||
onToggleClick?(parentId: string, childId: string): void;
|
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 = ({
|
export const HierarchyLevel = ({
|
||||||
root,
|
root,
|
||||||
roomSet,
|
roomSet,
|
||||||
|
@ -353,7 +378,7 @@ export const HierarchyLevel = ({
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
||||||
const room = hierarchy.roomMap.get(ev.state_key);
|
const room = hierarchy.roomMap.get(ev.state_key);
|
||||||
if (room && roomSet.has(room)) {
|
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;
|
return result;
|
||||||
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
||||||
|
@ -361,7 +386,7 @@ export const HierarchyLevel = ({
|
||||||
const newParents = new Set(parents).add(root.room_id);
|
const newParents = new Set(parents).add(root.room_id);
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
childRooms.map(room => (
|
uniqBy(childRooms, "room_id").map(room => (
|
||||||
<Tile
|
<Tile
|
||||||
key={room.room_id}
|
key={room.room_id}
|
||||||
room={room}
|
room={room}
|
||||||
|
@ -410,50 +435,39 @@ export const HierarchyLevel = ({
|
||||||
|
|
||||||
const INITIAL_PAGE_SIZE = 20;
|
const INITIAL_PAGE_SIZE = 20;
|
||||||
|
|
||||||
export const useSpaceSummary = (space: Room): {
|
export const useRoomHierarchy = (space: Room): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
rooms: IHierarchyRoom[];
|
rooms: IHierarchyRoom[];
|
||||||
hierarchy: RoomHierarchy;
|
hierarchy: RoomHierarchy;
|
||||||
loadMore(pageSize?: number): Promise <void>;
|
loadMore(pageSize?: number): Promise <void>;
|
||||||
} => {
|
} => {
|
||||||
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||||
|
|
||||||
const resetHierarchy = useCallback(() => {
|
const resetHierarchy = useCallback(() => {
|
||||||
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
||||||
setHierarchy(hierarchy);
|
|
||||||
|
|
||||||
let discard = false;
|
|
||||||
hierarchy.load().then(() => {
|
hierarchy.load().then(() => {
|
||||||
if (discard) return;
|
if (space !== hierarchy.root) return; // discard stale results
|
||||||
setRooms(hierarchy.rooms);
|
setRooms(hierarchy.rooms);
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
|
setHierarchy(hierarchy);
|
||||||
return () => {
|
|
||||||
discard = true;
|
|
||||||
};
|
|
||||||
}, [space]);
|
}, [space]);
|
||||||
useEffect(resetHierarchy, [resetHierarchy]);
|
useEffect(resetHierarchy, [resetHierarchy]);
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, (payload => {
|
useDispatcher(defaultDispatcher, (payload => {
|
||||||
if (payload.action === Action.UpdateSpaceHierarchy) {
|
if (payload.action === Action.UpdateSpaceHierarchy) {
|
||||||
setLoading(true);
|
|
||||||
setRooms([]); // TODO
|
setRooms([]); // TODO
|
||||||
resetHierarchy();
|
resetHierarchy();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const loadMore = useCallback(async (pageSize?: number) => {
|
const loadMore = useCallback(async (pageSize?: number) => {
|
||||||
if (!hierarchy.canLoadMore || hierarchy.noSupport) return;
|
if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
await hierarchy.load(pageSize);
|
await hierarchy.load(pageSize);
|
||||||
setRooms(hierarchy.rooms);
|
setRooms(hierarchy.rooms);
|
||||||
setLoading(false);
|
|
||||||
}, [hierarchy]);
|
}, [hierarchy]);
|
||||||
|
|
||||||
|
const loading = hierarchy?.loading ?? true;
|
||||||
return { loading, rooms, hierarchy, loadMore };
|
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 [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>>(() => {
|
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||||
if (!rooms?.length) return new Set();
|
if (!rooms?.length) return new Set();
|
||||||
|
@ -648,8 +662,6 @@ const SpaceHierarchy = ({
|
||||||
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||||
{ ({ onKeyDownHandler }) => {
|
{ ({ onKeyDownHandler }) => {
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
let loader: JSX.Element;
|
|
||||||
|
|
||||||
if (loading && !rooms.length) {
|
if (loading && !rooms.length) {
|
||||||
content = <Spinner />;
|
content = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -671,19 +683,20 @@ const SpaceHierarchy = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
|
} else if (!hierarchy.canLoadMore) {
|
||||||
if (hierarchy.canLoadMore) {
|
|
||||||
loader = <div ref={loaderRef}>
|
|
||||||
<Spinner />
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
results = <div className="mx_SpaceHierarchy_noResults">
|
results = <div className="mx_SpaceHierarchy_noResults">
|
||||||
<h3>{ _t("No results found") }</h3>
|
<h3>{ _t("No results found") }</h3>
|
||||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loader: JSX.Element;
|
||||||
|
if (hierarchy.canLoadMore) {
|
||||||
|
loader = <div ref={loaderRef}>
|
||||||
|
<Spinner />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
content = <>
|
content = <>
|
||||||
<div className="mx_SpaceHierarchy_listHeader">
|
<div className="mx_SpaceHierarchy_listHeader">
|
||||||
<h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4>
|
<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 { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||||
|
import { UIComponent } from "../../settings/UIFeature";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -412,7 +414,9 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
||||||
const userId = cli.getUserId();
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
let inviteButton;
|
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 = (
|
inviteButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
|
@ -730,7 +734,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
||||||
<BetaPill />
|
|
||||||
{ _t("<b>This is an experimental feature.</b> For now, " +
|
{ _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.", {}, {
|
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
||||||
b: sub => <b>{ sub }</b>,
|
b: sub => <b>{ sub }</b>,
|
||||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
||||||
import { Action } from '../../dispatcher/actions';
|
import { Action } from '../../dispatcher/actions';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||||
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -47,10 +49,14 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
|
editState?: EditorStateTransfer;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ThreadView")
|
@replaceableComponent("structures.ThreadView")
|
||||||
export default class ThreadView extends React.Component<IProps, IState> {
|
export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||||
|
|
||||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
this.setupThread(payload.event);
|
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) => {
|
private setupThread = (mxEv: MatrixEvent) => {
|
||||||
|
@ -124,6 +147,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<RoomContext.Provider value={{
|
||||||
|
...this.context,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Thread,
|
||||||
|
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||||
|
}}>
|
||||||
|
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_ThreadView"
|
className="mx_ThreadView"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -149,9 +178,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
membersLoaded={true}
|
membersLoaded={true}
|
||||||
|
editState={this.state.editState}
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
<MessageComposer
|
|
||||||
|
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
replyInThread={true}
|
replyInThread={true}
|
||||||
|
@ -160,8 +191,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
e2eStatus={this.props.e2eStatus}
|
e2eStatus={this.props.e2eStatus}
|
||||||
compact={true}
|
compact={true}
|
||||||
/>
|
/>) }
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import * as sdk from '../../../index';
|
||||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: () => void;
|
onFinished: () => void;
|
||||||
|
@ -27,6 +28,7 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
phase: Phase;
|
phase: Phase;
|
||||||
|
lostKeys: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.auth.CompleteSecurity")
|
@replaceableComponent("structures.auth.CompleteSecurity")
|
||||||
|
@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
store.on("update", this.onStoreUpdate);
|
store.on("update", this.onStoreUpdate);
|
||||||
store.start();
|
store.start();
|
||||||
this.state = { phase: store.phase };
|
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStoreUpdate = (): void => {
|
private onStoreUpdate = (): void => {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
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 {
|
public componentWillUnmount(): void {
|
||||||
|
@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||||
const { phase } = this.state;
|
const { phase, lostKeys } = this.state;
|
||||||
let icon;
|
let icon;
|
||||||
let title;
|
let title;
|
||||||
|
|
||||||
if (phase === Phase.Loading) {
|
if (phase === Phase.Loading) {
|
||||||
return null;
|
return null;
|
||||||
} else if (phase === Phase.Intro) {
|
} else if (phase === Phase.Intro) {
|
||||||
|
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" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Verify this login");
|
title = _t("Verify this login");
|
||||||
|
}
|
||||||
} else if (phase === Phase.Done) {
|
} else if (phase === Phase.Done) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||||
title = _t("Session verified");
|
title = _t("Session verified");
|
||||||
|
@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||||
} else if (phase === Phase.Busy) {
|
} else if (phase === Phase.Busy) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Verify this login");
|
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 {
|
} else {
|
||||||
throw new Error(`Unknown phase ${phase}`);
|
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 (
|
return (
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
<CompleteSecurityBody>
|
<CompleteSecurityBody>
|
||||||
<h2 className="mx_CompleteSecurity_header">
|
<h2 className="mx_CompleteSecurity_header">
|
||||||
{ icon }
|
{ icon }
|
||||||
{ title }
|
{ title }
|
||||||
|
{ skipButton }
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mx_CompleteSecurity_body">
|
<div className="mx_CompleteSecurity_body">
|
||||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||||
|
|
|
@ -46,6 +46,7 @@ interface IState {
|
||||||
phase: Phase;
|
phase: Phase;
|
||||||
verificationRequest: VerificationRequest;
|
verificationRequest: VerificationRequest;
|
||||||
backupInfo: IKeyBackupInfo;
|
backupInfo: IKeyBackupInfo;
|
||||||
|
lostKeys: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
@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.
|
// Because of the latter, it lives in the state.
|
||||||
verificationRequest: store.verificationRequest,
|
verificationRequest: store.verificationRequest,
|
||||||
backupInfo: store.backupInfo,
|
backupInfo: store.backupInfo,
|
||||||
|
lostKeys: store.lostKeys(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
phase: store.phase,
|
phase: store.phase,
|
||||||
verificationRequest: store.verificationRequest,
|
verificationRequest: store.verificationRequest,
|
||||||
backupInfo: store.backupInfo,
|
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 = () => {
|
private onSkipConfirmClick = () => {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
store.skipConfirm();
|
store.skipConfirm();
|
||||||
|
@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
store.returnAfterSkip();
|
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 = () => {
|
private onDoneClick = () => {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
store.done();
|
store.done();
|
||||||
|
@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
phase,
|
phase,
|
||||||
|
lostKeys,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (this.state.verificationRequest) {
|
if (this.state.verificationRequest) {
|
||||||
|
@ -143,17 +158,35 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
isRoomEncrypted={false}
|
isRoomEncrypted={false}
|
||||||
/>;
|
/>;
|
||||||
} else if (phase === Phase.Intro) {
|
} else if (phase === Phase.Intro) {
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
|
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
|
||||||
|
{ _t("Proceed with reset") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
let recoveryKeyPrompt;
|
let recoveryKeyPrompt;
|
||||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
|
||||||
} else if (store.keyInfo) {
|
} else if (store.keyInfo) {
|
||||||
recoveryKeyPrompt = _t("Use Security Key");
|
recoveryKeyPrompt = _t("Verify with Security Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
let useRecoveryKeyButton;
|
let useRecoveryKeyButton;
|
||||||
if (recoveryKeyPrompt) {
|
if (recoveryKeyPrompt) {
|
||||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||||
{ recoveryKeyPrompt }
|
{ recoveryKeyPrompt }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
@ -161,7 +194,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
let verifyButton;
|
let verifyButton;
|
||||||
if (store.hasDevicesToVerifyAgainst) {
|
if (store.hasDevicesToVerifyAgainst) {
|
||||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||||
{ _t("Use another login") }
|
{ _t("Verify with another login") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,12 +207,18 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
{ verifyButton }
|
{ verifyButton }
|
||||||
{ useRecoveryKeyButton }
|
{ useRecoveryKeyButton }
|
||||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
</div>
|
||||||
{ _t("Skip") }
|
<div className="mx_SetupEncryptionBody_reset">
|
||||||
</AccessibleButton>
|
{ _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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else if (phase === Phase.Done) {
|
} else if (phase === Phase.Done) {
|
||||||
let message;
|
let message;
|
||||||
if (this.state.backupInfo) {
|
if (this.state.backupInfo) {
|
||||||
|
@ -215,14 +254,13 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
) }</p>
|
) }</p>
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="warning"
|
kind="danger_outline"
|
||||||
kind="secondary"
|
|
||||||
onClick={this.onSkipConfirmClick}
|
onClick={this.onSkipConfirmClick}
|
||||||
>
|
>
|
||||||
{ _t("Skip") }
|
{ _t("I'll verify later") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="danger"
|
kind="primary"
|
||||||
onClick={this.onSkipBackClick}
|
onClick={this.onSkipBackClick}
|
||||||
>
|
>
|
||||||
{ _t("Go Back") }
|
{ _t("Go Back") }
|
||||||
|
@ -230,6 +268,30 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else {
|
} 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
|
* matrixClient: A matrix client. May be a different one to the one
|
||||||
* currently being used generally (eg. to register with
|
* 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
|
* loginType: the login type of the auth stage being attempted
|
||||||
* authSessionId: session id from the server
|
* authSessionId: session id from the server
|
||||||
* clientSecret: The client secret in use for identity server auth sessions
|
* clientSecret: The client secret in use for identity server auth sessions
|
||||||
|
@ -84,6 +84,7 @@ interface IAuthEntryProps {
|
||||||
loginType: string;
|
loginType: string;
|
||||||
authSessionId: string;
|
authSessionId: string;
|
||||||
errorText?: string;
|
errorText?: string;
|
||||||
|
errorCode?: string;
|
||||||
// Is the auth logic currently waiting for something to happen?
|
// Is the auth logic currently waiting for something to happen?
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
onPhaseChange: (phase: number) => void;
|
onPhaseChange: (phase: number) => void;
|
||||||
|
@ -427,18 +428,29 @@ export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEn
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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,
|
// 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
|
// 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
|
// 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 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,
|
// 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.
|
// assume that the link has been clicked and the server will realise when we poll.
|
||||||
if (this.props.inputs.emailAddress === undefined) {
|
// We only have a session ID if the user has clicked the link in their email,
|
||||||
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
|
// so show a loading state instead of "an email has been sent to..." because
|
||||||
// that's confusing when you've already read that email.
|
// 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 />;
|
return <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -448,6 +460,7 @@ export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEn
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||||
|
{ errorSection }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
// The onClick to give the avatar
|
// The onClick to give the avatar
|
||||||
onClick?: React.MouseEventHandler;
|
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;
|
viewUserOnClick?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
style?: any;
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent, ReactNode } from 'react';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -25,12 +27,13 @@ import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import Field from '../elements/Field';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||||
member: RoomMember;
|
member?: RoomMember;
|
||||||
// group member object. Supply either this or 'member'
|
// group member object. Supply either this or 'member'
|
||||||
groupMember: GroupMemberType;
|
groupMember?: GroupMemberType;
|
||||||
// needed if a group member is specified
|
// needed if a group member is specified
|
||||||
matrixClient?: MatrixClient;
|
matrixClient?: MatrixClient;
|
||||||
action: string; // eg. 'Ban'
|
action: string; // eg. 'Ban'
|
||||||
|
@ -41,9 +44,15 @@ interface IProps {
|
||||||
// be the string entered.
|
// be the string entered.
|
||||||
askReason?: boolean;
|
askReason?: boolean;
|
||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
onFinished: (success: boolean, reason?: string) => void;
|
onFinished: (success: boolean, reason?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming an operation on another user.
|
* A dialog for confirming an operation on another user.
|
||||||
* Takes a user ID and a verb, displays the target user prominently
|
* 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)
|
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
||||||
export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
|
||||||
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
danger: false,
|
danger: false,
|
||||||
askReason: false,
|
askReason: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
public onOk = (): void => {
|
constructor(props: IProps) {
|
||||||
this.props.onFinished(true, this.reasonField.current?.value);
|
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);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onReasonChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
reason: ev.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||||
|
|
||||||
let reasonBox;
|
let reasonBox;
|
||||||
if (this.props.askReason) {
|
if (this.props.askReason) {
|
||||||
reasonBox = (
|
reasonBox = (
|
||||||
<div>
|
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
<Field
|
||||||
ref={this.reasonField}
|
type="text"
|
||||||
placeholder={_t("Reason")}
|
onChange={this.onReasonChange}
|
||||||
|
value={this.state.reason}
|
||||||
|
className="mx_ConfirmUserActionDialog_reasonField"
|
||||||
|
label={_t("Reason")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,19 +127,23 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className="mx_ConfirmUserActionDialog"
|
className={classNames("mx_ConfirmUserActionDialog", this.props.className)}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
||||||
|
<div className="mx_ConfirmUserActionDialog_user">
|
||||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ reasonBox }
|
{ reasonBox }
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
<DialogButtons primaryButton={this.props.action}
|
<DialogButtons primaryButton={this.props.action}
|
||||||
onPrimaryButtonClick={this.onOk}
|
onPrimaryButtonClick={this.onOk}
|
||||||
primaryButtonClass={confirmButtonClass}
|
primaryButtonClass={confirmButtonClass}
|
||||||
|
|
|
@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { UserTab } from "./UserSettingsDialog";
|
import { UserTab } from "./UserSettingsDialog";
|
||||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||||
|
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -90,10 +92,22 @@ export interface IGroupSummary {
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* 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 CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>(null);
|
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 [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(true);
|
setProgress(Progress.ValidatingInputs);
|
||||||
|
|
||||||
// require & validate the space name field
|
// require & validate the space name field
|
||||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceNameField.current.focus();
|
spaceNameField.current.focus();
|
||||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// validate the space name alias field but do not require it
|
// validate the space name alias field but do not require it
|
||||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceAliasField.current.focus();
|
spaceAliasField.current.focus();
|
||||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setProgress(Progress.FetchingData);
|
||||||
|
|
||||||
const [rooms, members, invitedMembers] = await Promise.all([
|
const [rooms, members, invitedMembers] = await Promise.all([
|
||||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
cli.getGroupInvitedUsers(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[]>();
|
const viaMap = new Map<string, string[]>();
|
||||||
for (const { roomId, canonicalAlias } of rooms) {
|
for (const { roomId, canonicalAlias } of rooms) {
|
||||||
const room = cli.getRoom(roomId);
|
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 spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||||
creation_content: {
|
creation_content: {
|
||||||
|
@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
via: viaMap.get(roomId) || [],
|
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,
|
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
|
// eagerly remove it from the community panel
|
||||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||||
|
|
||||||
|
@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
setError(e);
|
setError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusy(false);
|
setProgress(Progress.NotStarted);
|
||||||
};
|
};
|
||||||
|
|
||||||
let footer;
|
let footer;
|
||||||
|
@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
{ _t("Retry") }
|
{ _t("Retry") }
|
||||||
</AccessibleButton>
|
</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 {
|
} else {
|
||||||
footer = <>
|
footer = <>
|
||||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
{ _t("Create Space") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
397
src/components/views/dialogs/ExportDialog.tsx
Normal file
397
src/components/views/dialogs/ExportDialog.tsx
Normal file
|
@ -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.
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
@ -22,108 +22,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "../dialogs/BaseDialog";
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||||
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>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -144,6 +43,7 @@ const isOnlyAdmin = (room: Room): boolean => {
|
||||||
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||||
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
|
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
|
||||||
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
|
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
|
||||||
|
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
|
||||||
|
|
||||||
let rejoinWarning;
|
let rejoinWarning;
|
||||||
if (space.getJoinRule() !== JoinRule.Public) {
|
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?") }
|
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
{ spaceChildren.length > 0 && (
|
||||||
|
<SpaceChildrenPicker
|
||||||
space={space}
|
space={space}
|
||||||
spaceChildren={spaceChildren}
|
spaceChildren={spaceChildren}
|
||||||
roomsToLeave={roomsToLeave}
|
selected={selectedRooms}
|
||||||
setRoomsToLeave={setRoomsToLeave}
|
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 && <div className="mx_LeaveSpaceDialog_section_warning">
|
||||||
{ onlyAdminWarning }
|
{ onlyAdminWarning }
|
||||||
|
|
|
@ -44,18 +44,31 @@ interface IProps {
|
||||||
initialTabId?: string;
|
initialTabId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||||
export default class RoomSettingsDialog extends React.Component<IProps> {
|
export default class RoomSettingsDialog extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { roomName: '' };
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
|
this.onRoomName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload): void => {
|
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[] {
|
private getTabs(): Tab[] {
|
||||||
const tabs: Tab[] = [];
|
const tabs: Tab[] = [];
|
||||||
|
|
||||||
|
@ -122,7 +141,7 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
const roomName = this.state.roomName;
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_RoomSettingsDialog'
|
className='mx_RoomSettingsDialog'
|
||||||
|
|
|
@ -28,15 +28,25 @@ import { IDialogProps } from "./IDialogProps";
|
||||||
import BugReportDialog from './BugReportDialog';
|
import BugReportDialog from './BugReportDialog';
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
|
export interface IFinishedOpts {
|
||||||
|
continue: boolean;
|
||||||
|
invite: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
targetVersion: string;
|
targetVersion: string;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
|
doUpgrade?(opts: IFinishedOpts, fn: (progressText: string, progress: number, total: number) => void): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
inviteUsersToNewRoom: boolean;
|
inviteUsersToNewRoom: boolean;
|
||||||
|
progressText?: string;
|
||||||
|
progress?: number;
|
||||||
|
total?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
@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 room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
||||||
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
||||||
this.currentVersion = room?.getVersion() || "1";
|
this.currentVersion = room?.getVersion();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
inviteUsersToNewRoom: true,
|
inviteUsersToNewRoom: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onProgressCallback = (progressText: string, progress: number, total: number): void => {
|
||||||
|
this.setState({ progressText, progress, total });
|
||||||
|
};
|
||||||
|
|
||||||
private onContinue = () => {
|
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 = () => {
|
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 (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_RoomUpgradeWarningDialog'
|
className='mx_RoomUpgradeWarningDialog'
|
||||||
|
@ -154,12 +196,7 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
||||||
</p>
|
</p>
|
||||||
{ inviteToggle }
|
{ inviteToggle }
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
{ footer }
|
||||||
primaryButton={_t("Upgrade")}
|
|
||||||
onPrimaryButtonClick={this.onContinue}
|
|
||||||
cancelButton={_t("Cancel")}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,7 +268,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonRect = handle.current.getBoundingClientRect();
|
const buttonRect = handle.current.getBoundingClientRect();
|
||||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} focusLock>
|
||||||
<div className="mx_NetworkDropdown_menu">
|
<div className="mx_NetworkDropdown_menu">
|
||||||
{ options }
|
{ options }
|
||||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
<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;
|
this.ignoreEvent = ev;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onChevronClick = (ev: React.MouseEvent) => {
|
||||||
|
if (this.state.expanded) {
|
||||||
|
this.setState({ expanded: false });
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||||
if (this.props.disabled) return;
|
if (this.props.disabled) return;
|
||||||
|
|
||||||
|
@ -375,7 +383,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{ currentValue }
|
{ currentValue }
|
||||||
<span className="mx_Dropdown_arrow" />
|
<span onClick={this.onChevronClick} className="mx_Dropdown_arrow" />
|
||||||
{ menu }
|
{ menu }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -143,6 +143,10 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this.input.focus();
|
this.input.focus();
|
||||||
|
// programmatic does not fire onFocus handler
|
||||||
|
this.setState({
|
||||||
|
focused: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onFocus = (ev) => {
|
private onFocus = (ev) => {
|
||||||
|
|
|
@ -53,6 +53,7 @@ interface IProps {
|
||||||
layout?: Layout;
|
layout?: Layout;
|
||||||
// Whether to always show a timestamp
|
// Whether to always show a timestamp
|
||||||
alwaysShowTimestamps?: boolean;
|
alwaysShowTimestamps?: boolean;
|
||||||
|
forExport?: boolean;
|
||||||
isQuoteExpanded?: boolean;
|
isQuoteExpanded?: boolean;
|
||||||
setQuoteExpanded: (isExpanded: boolean) => void;
|
setQuoteExpanded: (isExpanded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
@ -381,6 +382,17 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</blockquote>;
|
</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) {
|
} else if (this.state.loading) {
|
||||||
header = <Spinner w={16} h={16} />;
|
header = <Spinner w={16} h={16} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,17 @@ function getDaysArray(): string[] {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
ts: number;
|
ts: number;
|
||||||
|
forExport?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.DateSeparator")
|
@replaceableComponent("views.messages.DateSeparator")
|
||||||
export default class DateSeparator extends React.Component<IProps> {
|
export default class DateSeparator extends React.Component<IProps> {
|
||||||
private getLabel() {
|
private getLabel() {
|
||||||
const date = new Date(this.props.ts);
|
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 today = new Date();
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
|
|
|
@ -33,6 +33,7 @@ export interface IBodyProps {
|
||||||
onHeightChanged: () => void;
|
onHeightChanged: () => void;
|
||||||
|
|
||||||
showUrlPreview?: boolean;
|
showUrlPreview?: boolean;
|
||||||
|
forExport?: boolean;
|
||||||
tileShape: TileShape;
|
tileShape: TileShape;
|
||||||
maxImageHeight?: number;
|
maxImageHeight?: number;
|
||||||
replacingEventId?: string;
|
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) {
|
if (!this.state.playback) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MAudioBody">
|
<span className="mx_MAudioBody">
|
||||||
|
|
|
@ -123,6 +123,11 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
this.state = {};
|
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 {
|
private get content(): IMediaEventContent {
|
||||||
return this.props.mxEvent.getContent<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) {
|
public componentDidUpdate(prevProps, prevState) {
|
||||||
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
|
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
|
||||||
this.props.onHeightChanged();
|
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;
|
const showDownloadLink = this.props.tileShape || !this.props.showGenericPlaceholder;
|
||||||
|
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
|
|
|
@ -179,6 +179,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
protected getContentUrl(): string {
|
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) {
|
if (this.media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
|
@ -372,7 +375,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
let placeholder = null;
|
let placeholder = null;
|
||||||
let gifLabel = null;
|
let gifLabel = null;
|
||||||
|
|
||||||
if (!this.state.imgLoaded) {
|
if (!this.props.forExport && !this.state.imgLoaded) {
|
||||||
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,7 +465,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
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 }
|
{ children }
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
@ -490,6 +493,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
protected getFileBody(): string | JSX.Element {
|
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
|
// We only ever need the download bar if we're appearing outside of the timeline
|
||||||
if (this.props.tileShape) {
|
if (this.props.tileShape) {
|
||||||
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||||
|
@ -510,7 +514,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
const contentUrl = this.getContentUrl();
|
const contentUrl = this.getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this.isGif() && SettingsStore.getValue("autoplayGifs")) {
|
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) {
|
||||||
thumbUrl = contentUrl;
|
thumbUrl = contentUrl;
|
||||||
} else {
|
} else {
|
||||||
thumbUrl = this.getThumbUrl();
|
thumbUrl = this.getThumbUrl();
|
||||||
|
|
|
@ -79,7 +79,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
private getContentUrl(): string|null {
|
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) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,6 +96,9 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThumbUrl(): string|null {
|
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 content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
|
|
||||||
|
@ -209,6 +215,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
this.props.onHeightChanged();
|
this.props.onHeightChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getFileBody = () => {
|
||||||
|
if (this.props.forExport) return null;
|
||||||
|
return this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const autoplay = SettingsStore.getValue("autoplayVideo");
|
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.
|
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
// The attachment is decrypted in componentDidMount.
|
// The attachment is decrypted in componentDidMount.
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a spinner.
|
||||||
|
@ -254,6 +265,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
preload = "none";
|
preload = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileBody = this.getFileBody();
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVideoBody">
|
<span className="mx_MVideoBody">
|
||||||
<video
|
<video
|
||||||
|
@ -270,7 +283,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
poster={poster}
|
poster={poster}
|
||||||
onPlay={this.videoOnPlay}
|
onPlay={this.videoOnPlay}
|
||||||
/>
|
/>
|
||||||
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
|
{ fileBody }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { isVoiceMessage } from "../../../utils/EventUtils";
|
||||||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||||
export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
|
export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
|
||||||
public render() {
|
public render() {
|
||||||
if (isVoiceMessage(this.props.mxEvent)) {
|
if (!this.props.forExport && isVoiceMessage(this.props.mxEvent)) {
|
||||||
return <MVoiceMessageBody {...this.props} />;
|
return <MVoiceMessageBody {...this.props} />;
|
||||||
} else {
|
} else {
|
||||||
return <MAudioBody {...this.props} />;
|
return <MAudioBody {...this.props} />;
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
||||||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import Toolbar from "../../../accessibility/Toolbar";
|
import Toolbar from "../../../accessibility/Toolbar";
|
||||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ActionBarRenderingContext {
|
|
||||||
Room,
|
|
||||||
Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMessageActionBarProps {
|
interface IMessageActionBarProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
reactions?: Relations;
|
reactions?: Relations;
|
||||||
|
@ -142,7 +137,6 @@ interface IMessageActionBarProps {
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||||
toggleThreadExpanded: () => void;
|
toggleThreadExpanded: () => void;
|
||||||
renderingContext?: ActionBarRenderingContext;
|
|
||||||
isQuoteExpanded?: boolean;
|
isQuoteExpanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,10 +144,6 @@ interface IMessageActionBarProps {
|
||||||
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||||
public static contextType = RoomContext;
|
public static contextType = RoomContext;
|
||||||
|
|
||||||
public static defaultProps = {
|
|
||||||
renderingContext: ActionBarRenderingContext.Room,
|
|
||||||
};
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||||
this.props.mxEvent.on("Event.status", this.onSent);
|
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 => {
|
private onEditClick = (ev: React.MouseEvent): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'edit_event',
|
action: Action.EditEvent,
|
||||||
event: this.props.mxEvent,
|
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.
|
// 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
|
// 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()`.
|
// 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, <>
|
toolbarOpts.splice(0, 0, <>
|
||||||
<RovingAccessibleTooltipButton
|
<RovingAccessibleTooltipButton
|
||||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
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) {
|
if (allowCancel) {
|
||||||
toolbarOpts.push(cancelSendingButton);
|
toolbarOpts.push(cancelSendingButton);
|
||||||
|
|
|
@ -136,6 +136,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
forExport={this.props.forExport}
|
||||||
maxImageHeight={this.props.maxImageHeight}
|
maxImageHeight={this.props.maxImageHeight}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
|
|
|
@ -29,7 +29,6 @@ interface IProps {
|
||||||
|
|
||||||
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
||||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||||
|
|
||||||
let text = _t("Message deleted");
|
let text = _t("Message deleted");
|
||||||
const unsigned = mxEvent.getUnsigned();
|
const unsigned = mxEvent.getUnsigned();
|
||||||
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
|
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
|
||||||
|
|
|
@ -49,16 +49,18 @@ const EncryptionInfo: React.FC<IProps> = ({
|
||||||
isSelfVerification,
|
isSelfVerification,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
let content: JSX.Element;
|
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;
|
let text: string;
|
||||||
if (waitingForOtherParty) {
|
if (waitingForOtherParty) {
|
||||||
if (isSelfVerification) {
|
|
||||||
text = _t("Accept on your other login…");
|
|
||||||
} else {
|
|
||||||
text = _t("Waiting for %(displayName)s to accept…", {
|
text = _t("Waiting for %(displayName)s to accept…", {
|
||||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
text = _t("Accepting…");
|
text = _t("Accepting…");
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
||||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import ExportDialog from "../dialogs/ExportDialog";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
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 isRoomEncrypted = useIsEncrypted(cli, room);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const e2eStatus = roomContext.e2eStatus;
|
const e2eStatus = roomContext.e2eStatus;
|
||||||
|
@ -280,6 +287,9 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||||
{ _t("Show files") }
|
{ _t("Show files") }
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||||
|
{ _t("Export chat") }
|
||||||
|
</Button>
|
||||||
{ SettingsStore.getValue("feature_thread") && (
|
{ SettingsStore.getValue("feature_thread") && (
|
||||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
||||||
{ _t("Show threads") }
|
{ _t("Show threads") }
|
||||||
|
|
|
@ -70,8 +70,12 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
|
||||||
|
import { bulkSpaceBehaviour } from "../../../utils/space";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
deviceId: string;
|
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 roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
|
||||||
const onInviteUserButton = async () => {
|
const onInviteUserButton = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -532,7 +536,7 @@ interface IBaseProps {
|
||||||
stopUpdating(): void;
|
stopUpdating(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
|
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
// check if user can be kicked/disinvited
|
// check if user can be kicked/disinvited
|
||||||
|
@ -542,21 +546,38 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
|
||||||
const { finished } = Modal.createTrackedDialog(
|
const { finished } = Modal.createTrackedDialog(
|
||||||
'Confirm User Action Dialog',
|
'Confirm User Action Dialog',
|
||||||
'onKick',
|
'onKick',
|
||||||
ConfirmUserActionDialog,
|
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
|
||||||
{
|
{
|
||||||
member,
|
member,
|
||||||
action: member.membership === "invite" ? _t("Disinvite") : _t("Kick"),
|
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",
|
askReason: member.membership === "join",
|
||||||
danger: true,
|
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;
|
if (!proceed) return;
|
||||||
|
|
||||||
startUpdating();
|
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
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
logger.log("Kick success");
|
logger.log("Kick success");
|
||||||
|
@ -656,34 +677,69 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
|
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
const isBanned = member.membership === "ban";
|
||||||
const onBanOrUnban = async () => {
|
const onBanOrUnban = async () => {
|
||||||
const { finished } = Modal.createTrackedDialog(
|
const { finished } = Modal.createTrackedDialog(
|
||||||
'Confirm User Action Dialog',
|
'Confirm User Action Dialog',
|
||||||
'onBanOrUnban',
|
'onBanOrUnban',
|
||||||
ConfirmUserActionDialog,
|
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
|
||||||
{
|
{
|
||||||
member,
|
member,
|
||||||
action: member.membership === 'ban' ? _t("Unban") : _t("Ban"),
|
action: isBanned ? _t("Unban") : _t("Ban"),
|
||||||
title: member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"),
|
title: isBanned
|
||||||
askReason: member.membership !== 'ban',
|
? _t("Unban from %(roomName)s", { roomName: room.name })
|
||||||
danger: member.membership !== 'ban',
|
: _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;
|
if (!proceed) return;
|
||||||
|
|
||||||
startUpdating();
|
startUpdating();
|
||||||
let promise;
|
|
||||||
if (member.membership === 'ban') {
|
const fn = (roomId: string) => {
|
||||||
promise = cli.unban(member.roomId, member.userId);
|
if (isBanned) {
|
||||||
|
return cli.unban(roomId, member.userId);
|
||||||
} else {
|
} else {
|
||||||
promise = cli.ban(member.roomId, member.userId, reason || undefined);
|
return cli.ban(roomId, member.userId, reason || undefined);
|
||||||
}
|
}
|
||||||
promise.then(() => {
|
};
|
||||||
|
|
||||||
|
bulkSpaceBehaviour(room, rooms, room => fn(room.roomId)).then(() => {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
logger.log("Ban success");
|
logger.log("Ban success");
|
||||||
|
@ -699,12 +755,12 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
|
||||||
};
|
};
|
||||||
|
|
||||||
let label = _t("Ban");
|
let label = _t("Ban");
|
||||||
if (member.membership === 'ban') {
|
if (isBanned) {
|
||||||
label = _t("Unban");
|
label = _t("Unban");
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames("mx_UserInfo_field", {
|
const classes = classNames("mx_UserInfo_field", {
|
||||||
mx_UserInfo_destructive: member.membership !== 'ban',
|
mx_UserInfo_destructive: !isBanned,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
||||||
|
@ -817,18 +873,28 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
const isMe = me.userId === member.userId;
|
const isMe = me.userId === member.userId;
|
||||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||||
|
|
||||||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
kickButton = <RoomKickButton
|
||||||
|
room={room}
|
||||||
|
member={member}
|
||||||
|
startUpdating={startUpdating}
|
||||||
|
stopUpdating={stopUpdating}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
||||||
redactButton = (
|
redactButton = (
|
||||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
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 = (
|
muteButton = (
|
||||||
<MuteToggleButton
|
<MuteToggleButton
|
||||||
member={member}
|
member={member}
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface IState {
|
||||||
avatarFile: File;
|
avatarFile: File;
|
||||||
originalTopic: string;
|
originalTopic: string;
|
||||||
topic: string;
|
topic: string;
|
||||||
enableProfileSave: boolean;
|
profileFieldsTouched: Record<string, boolean>;
|
||||||
canSetName: boolean;
|
canSetName: boolean;
|
||||||
canSetTopic: boolean;
|
canSetTopic: boolean;
|
||||||
canSetAvatar: boolean;
|
canSetAvatar: boolean;
|
||||||
|
@ -71,7 +71,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
originalTopic: topic,
|
originalTopic: topic,
|
||||||
topic: topic,
|
topic: topic,
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {},
|
||||||
canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()),
|
canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()),
|
||||||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', 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({
|
this.setState({
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
avatarFile: 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> => {
|
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.enableProfileSave) return;
|
if (!this.isSaveEnabled()) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {},
|
||||||
displayName: this.state.originalDisplayName,
|
displayName: this.state.originalDisplayName,
|
||||||
topic: this.state.originalTopic,
|
topic: this.state.originalTopic,
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
|
@ -110,8 +117,8 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.enableProfileSave) return;
|
if (!this.isSaveEnabled()) return;
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ profileFieldsTouched: {} });
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -156,18 +163,38 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ displayName: e.target.value });
|
this.setState({ displayName: e.target.value });
|
||||||
if (this.state.originalDisplayName === e.target.value) {
|
if (this.state.originalDisplayName === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ enableProfileSave: true });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||||
this.setState({ topic: e.target.value });
|
this.setState({ topic: e.target.value });
|
||||||
if (this.state.originalTopic === e.target.value) {
|
if (this.state.originalTopic === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
topic: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} 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({
|
this.setState({
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
avatar: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -187,7 +217,10 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: String(ev.target.result),
|
avatarUrl: String(ev.target.result),
|
||||||
avatarFile: file,
|
avatarFile: file,
|
||||||
enableProfileSave: true,
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
@ -205,14 +238,14 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.cancelProfileChanges}
|
onClick={this.cancelProfileChanges}
|
||||||
kind="link"
|
kind="link"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.isSaveEnabled()}
|
||||||
>
|
>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.saveProfile}
|
onClick={this.saveProfile}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.isSaveEnabled()}
|
||||||
>
|
>
|
||||||
{ _t("Save") }
|
{ _t("Save") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
|
||||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
||||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
@ -36,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import SendHistoryManager from '../../../SendHistoryManager';
|
import SendHistoryManager from '../../../SendHistoryManager';
|
||||||
import Modal from '../../../Modal';
|
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 { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
@ -46,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 {
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
const html = mxEvent.getContent().formatted_body;
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
@ -66,7 +67,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
function createEditContent(
|
||||||
|
model: EditorModel,
|
||||||
|
editedEvent: MatrixEvent,
|
||||||
|
renderingContext?: TimelineRenderingType,
|
||||||
|
): IContent {
|
||||||
const isEmote = containsEmote(model);
|
const isEmote = containsEmote(model);
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
model = stripEmoteCommand(model);
|
model = stripEmoteCommand(model);
|
||||||
|
@ -99,41 +104,49 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
||||||
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign({
|
const relation = {
|
||||||
"m.new_content": newContent,
|
"m.new_content": newContent,
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"rel_type": "m.replace",
|
"rel_type": "m.replace",
|
||||||
"event_id": editedEvent.getId(),
|
"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;
|
editState: EditorStateTransfer;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
saveDisabled: boolean;
|
saveDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||||
static contextType = MatrixClientContext;
|
static contextType = RoomContext;
|
||||||
context!: React.ContextType<typeof MatrixClientContext>;
|
context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private model: EditorModel = null;
|
private model: EditorModel = null;
|
||||||
|
|
||||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||||
|
|
||||||
const isRestored = this.createEditorModel();
|
const isRestored = this.createEditorModel();
|
||||||
const ev = this.props.editState.getEvent();
|
const ev = this.props.editState.getEvent();
|
||||||
|
|
||||||
|
const renderingContext = this.context.timelineRenderingType;
|
||||||
|
const editContent = createEditContent(this.model, ev, renderingContext);
|
||||||
this.state = {
|
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);
|
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||||
|
@ -141,7 +154,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRoom(): Room {
|
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 => {
|
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()) {
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
const previousEvent = findEditableEvent({
|
||||||
this.props.editState.getEvent().getId());
|
events: this.events,
|
||||||
|
isForward: false,
|
||||||
|
fromEventId: this.props.editState.getEvent().getId(),
|
||||||
|
});
|
||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: previousEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -174,12 +194,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||||
return;
|
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) {
|
if (nextEvent) {
|
||||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: nextEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: 'edit_event', event: null });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -189,16 +221,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private get editorRoomKey(): string {
|
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 {
|
private get editorStateKey(): string {
|
||||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
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 => {
|
private cancelEdit = (): void => {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: "edit_event", event: null });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
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);
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
}
|
}
|
||||||
|
const renderingContext = this.context.timelineRenderingType;
|
||||||
const editContent = createEditContent(this.model, editedEvent);
|
const editContent = createEditContent(this.model, editedEvent, renderingContext);
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
let shouldSend = true;
|
let shouldSend = true;
|
||||||
|
@ -381,7 +424,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
if (shouldSend) {
|
if (shouldSend) {
|
||||||
this.cancelPreviousPendingEdit();
|
this.cancelPreviousPendingEdit();
|
||||||
const prom = this.context.sendMessage(roomId, editContent);
|
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: "message_sent" });
|
dis.dispatch({ action: "message_sent" });
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||||
|
@ -389,7 +432,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the event editing and focus composer
|
// 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);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -400,7 +447,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
previousEdit.status === EventStatus.QUEUED ||
|
previousEdit.status === EventStatus.QUEUED ||
|
||||||
previousEdit.status === EventStatus.NOT_SENT
|
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 {
|
private createEditorModel(): boolean {
|
||||||
const { editState } = this.props;
|
const { editState } = this.props;
|
||||||
const room = this.getRoom();
|
const room = this.getRoom();
|
||||||
const partCreator = new CommandPartCreator(room, this.context);
|
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||||
|
|
||||||
let parts;
|
let parts;
|
||||||
let isRestored = false;
|
let isRestored = false;
|
||||||
|
@ -493,3 +540,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||||
|
export default EditMessageComposerWithMatrixClient;
|
||||||
|
|
|
@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
|
||||||
import MessageTimestamp from '../messages/MessageTimestamp';
|
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||||
import TooltipButton from '../elements/TooltipButton';
|
import TooltipButton from '../elements/TooltipButton';
|
||||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||||
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
|
import MessageActionBar from "../messages/MessageActionBar";
|
||||||
import ReactionsRow from '../messages/ReactionsRow';
|
import ReactionsRow from '../messages/ReactionsRow';
|
||||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
|
@ -264,6 +264,8 @@ interface IProps {
|
||||||
// for now.
|
// for now.
|
||||||
tileShape?: TileShape;
|
tileShape?: TileShape;
|
||||||
|
|
||||||
|
forExport?: boolean;
|
||||||
|
|
||||||
// show twelve hour timestamps
|
// show twelve hour timestamps
|
||||||
isTwelveHour?: boolean;
|
isTwelveHour?: boolean;
|
||||||
|
|
||||||
|
@ -340,6 +342,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
||||||
onHeightChanged: function() {},
|
onHeightChanged: function() {},
|
||||||
|
forExport: false,
|
||||||
layout: Layout.Group,
|
layout: Layout.Group,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -382,7 +385,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
* or 'sent' receipt, for example.
|
* or 'sent' receipt, for example.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private get isEligibleForSpecialReceipt() {
|
private get isEligibleForSpecialReceipt(): boolean {
|
||||||
// First, if there are other read receipts then just short-circuit this.
|
// 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.readReceipts && this.props.readReceipts.length > 0) return false;
|
||||||
if (!this.props.mxEvent) return false;
|
if (!this.props.mxEvent) return false;
|
||||||
|
@ -453,6 +456,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.suppressReadReceiptAnimation = false;
|
this.suppressReadReceiptAnimation = false;
|
||||||
const client = this.context;
|
const client = this.context;
|
||||||
|
if (!this.props.forExport) {
|
||||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||||
|
@ -464,6 +468,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
client.on("Room.receipt", this.onRoomReceipt);
|
client.on("Room.receipt", this.onRoomReceipt);
|
||||||
this.isListeningForReceipts = true;
|
this.isListeningForReceipts = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_thread")) {
|
if (SettingsStore.getValue("feature_thread")) {
|
||||||
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
||||||
|
@ -698,6 +703,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldHighlight() {
|
shouldHighlight() {
|
||||||
|
if (this.props.forExport) return false;
|
||||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||||
if (!actions || !actions.tweaks) { return false; }
|
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
|
const showMessageActionBar = !isEditing && !this.props.forExport;
|
||||||
? ActionBarRenderingContext.Thread
|
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||||
: ActionBarRenderingContext.Room;
|
|
||||||
const actionBar = !isEditing ? <MessageActionBar
|
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
getTile={this.getTile}
|
getTile={this.getTile}
|
||||||
getReplyThread={this.getReplyThread}
|
getReplyThread={this.getReplyThread}
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
renderingContext={renderingContext}
|
|
||||||
isQuoteExpanded={isQuoteExpanded}
|
isQuoteExpanded={isQuoteExpanded}
|
||||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
@ -1171,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
editState={this.props.editState}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
@ -1204,6 +1208,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
editState={this.props.editState}
|
||||||
|
replacingEventId={this.props.replacingEventId}
|
||||||
/>
|
/>
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>,
|
</div>,
|
||||||
|
@ -1224,6 +1230,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
editState={this.props.editState}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<a
|
<a
|
||||||
|
@ -1247,6 +1254,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
parentEv={this.props.mxEvent}
|
parentEv={this.props.mxEvent}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
ref={this.replyThread}
|
ref={this.replyThread}
|
||||||
|
forExport={this.props.forExport}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||||
|
@ -1280,6 +1288,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
{ thread }
|
{ thread }
|
||||||
<EventTileType ref={this.tile}
|
<EventTileType ref={this.tile}
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
|
forExport={this.props.forExport}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
highlights={this.props.highlights}
|
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
|
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||||
function isMessageEvent(ev) {
|
function isMessageEvent(ev: MatrixEvent): boolean {
|
||||||
return (messageTypes.includes(ev.getType()));
|
return (messageTypes.includes(ev.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,8 @@ import MemberTile from "./MemberTile";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
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";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`;
|
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);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
let inviteButton;
|
let inviteButton;
|
||||||
|
|
||||||
if (room && room.getMyMembership() === 'join') {
|
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||||
let inviteButtonText = _t("Invite to this room");
|
let inviteButtonText = _t("Invite to this room");
|
||||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||||
if (chat && chat.roomId === this.props.roomId) {
|
if (chat && chat.roomId === this.props.roomId) {
|
||||||
|
|
|
@ -45,13 +45,15 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
||||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||||
import SendMessageComposer from "./SendMessageComposer";
|
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import EditorModel from "../../../editor/model";
|
import EditorModel from "../../../editor/model";
|
||||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import InfoDialog from "../dialogs/InfoDialog";
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
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 {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -219,8 +246,8 @@ interface IState {
|
||||||
@replaceableComponent("views.rooms.MessageComposer")
|
@replaceableComponent("views.rooms.MessageComposer")
|
||||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private messageComposerInput: SendMessageComposer;
|
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private instanceId: number;
|
private instanceId: number;
|
||||||
|
|
||||||
|
@ -378,14 +405,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage = async () => {
|
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
|
// There shouldn't be any text message to send when a voice recording is active, so
|
||||||
// just send out the voice recording.
|
// just send out the voice recording.
|
||||||
await this.voiceRecordingButton.send();
|
await this.voiceRecordingButton.current?.send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageComposerInput.sendMessage();
|
this.messageComposerInput.current?.sendMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = (model: EditorModel) => {
|
private onChange = (model: EditorModel) => {
|
||||||
|
@ -432,6 +459,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
if (!this.state.haveRecording) {
|
if (!this.state.haveRecording) {
|
||||||
|
if (SettingsStore.getValue("feature_polls")) {
|
||||||
|
buttons.push(
|
||||||
|
<PollButton key="polls" />,
|
||||||
|
);
|
||||||
|
}
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||||
);
|
);
|
||||||
|
@ -460,7 +492,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||||
title={_t("Send voice message")}
|
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) {
|
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
ref={(c) => this.messageComposerInput = c}
|
ref={this.messageComposerInput}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
placeholder={this.renderPlaceholderText()}
|
placeholder={this.renderPlaceholderText()}
|
||||||
|
@ -535,7 +567,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
controls.push(<VoiceRecordComposerTile
|
controls.push(<VoiceRecordComposerTile
|
||||||
key="controls_voice_record"
|
key="controls_voice_record"
|
||||||
ref={c => this.voiceRecordingButton = c}
|
ref={this.voiceRecordingButton}
|
||||||
room={this.props.room} />);
|
room={this.props.room} />);
|
||||||
} else if (this.state.tombstone) {
|
} else if (this.state.tombstone) {
|
||||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
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 MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
import { showSpaceInvite } from "../../../utils/space";
|
import { showSpaceInvite } from "../../../utils/space";
|
||||||
import { privateShouldBeEncrypted } from "../../../createRoom";
|
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||||
import EventTileBubble from "../messages/EventTileBubble";
|
import EventTileBubble from "../messages/EventTileBubble";
|
||||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
|
|
||||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||||
|
@ -150,7 +152,7 @@ const NewRoomIntro = () => {
|
||||||
{ _t("Invite to just this room") }
|
{ _t("Invite to just this room") }
|
||||||
</AccessibleButton> }
|
</AccessibleButton> }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (room.canInvite(cli.getUserId())) {
|
} else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_NewRoomIntro_inviteButton"
|
className="mx_NewRoomIntro_inviteButton"
|
||||||
|
|
|
@ -49,6 +49,8 @@ import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
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";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -134,6 +136,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||||
MatrixClientPeg.get().getUserId());
|
MatrixClientPeg.get().getUserId());
|
||||||
|
|
||||||
return <IconizedContextMenuOptionList first>
|
return <IconizedContextMenuOptionList first>
|
||||||
|
{
|
||||||
|
shouldShowComponent(UIComponent.CreateRooms)
|
||||||
|
? (<>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
label={_t("Create new room")}
|
label={_t("Create new room")}
|
||||||
iconClassName="mx_RoomList_iconPlus"
|
iconClassName="mx_RoomList_iconPlus"
|
||||||
|
@ -160,6 +165,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||||
tooltip={canAddRooms ? undefined
|
tooltip={canAddRooms ? undefined
|
||||||
: _t("You do not have permissions to add rooms to this space")}
|
: _t("You do not have permissions to add rooms to this space")}
|
||||||
/>
|
/>
|
||||||
|
</>)
|
||||||
|
: null
|
||||||
|
}
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
label={_t("Explore rooms")}
|
label={_t("Explore rooms")}
|
||||||
iconClassName="mx_RoomList_iconBrowse"
|
iconClassName="mx_RoomList_iconBrowse"
|
||||||
|
@ -450,8 +458,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSublists(): React.ReactElement[] {
|
private renderSublists(): React.ReactElement[] {
|
||||||
// show a skeleton UI if the user is in no rooms and they are not filtering
|
// 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 &&
|
const showSkeleton = !this.state.isNameFiltering && !this.state.suggestedRooms?.length &&
|
||||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||||
|
|
||||||
return TAG_ORDER.reduce((tags, tagId) => {
|
return TAG_ORDER.reduce((tags, tagId) => {
|
||||||
|
|
|
@ -35,6 +35,8 @@ import InviteReason from "../elements/InviteReason";
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
|
@ -339,8 +341,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
case MessageCase.NotLoggedIn: {
|
case MessageCase.NotLoggedIn: {
|
||||||
title = _t("Join the conversation with an account");
|
title = _t("Join the conversation with an account");
|
||||||
|
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||||
primaryActionLabel = _t("Sign Up");
|
primaryActionLabel = _t("Sign Up");
|
||||||
primaryActionHandler = this.onRegisterClick;
|
primaryActionHandler = this.onRegisterClick;
|
||||||
|
}
|
||||||
secondaryActionLabel = _t("Sign In");
|
secondaryActionLabel = _t("Sign In");
|
||||||
secondaryActionHandler = this.onLoginClick;
|
secondaryActionHandler = this.onLoginClick;
|
||||||
if (this.props.previewLoading) {
|
if (this.props.previewLoading) {
|
||||||
|
|
|
@ -55,6 +55,8 @@ import { ListNotificationState } from "../../../stores/notifications/ListNotific
|
||||||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
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 SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||||
const RESIZE_HANDLE_HEIGHT = 4; // 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;
|
let addRoomButton = null;
|
||||||
if (!!this.props.onAddRoom) {
|
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
|
||||||
addRoomButton = (
|
addRoomButton = (
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
@ -687,6 +689,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.props.addRoomContextMenu) {
|
} else if (this.props.addRoomContextMenu) {
|
||||||
|
// We assume that shouldShowComponent() is checked by the context menu itself.
|
||||||
addRoomButton = (
|
addRoomButton = (
|
||||||
<ContextMenuTooltipButton
|
<ContextMenuTooltipButton
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue