Merge branch 'develop' into fayed/fix-verification-dialog
commit
651e943b31
103
CHANGELOG.md
103
CHANGELOG.md
|
@ -1,3 +1,106 @@
|
|||
Changes in [3.31.0](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0) (2021-09-27)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
|
||||
* Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
|
||||
* Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
|
||||
* Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
|
||||
* Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
|
||||
* Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
|
||||
* Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
|
||||
* Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
|
||||
* Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
|
||||
* Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
|
||||
* Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
|
||||
* Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
|
||||
* Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
|
||||
* Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
|
||||
* Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
|
||||
* hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
|
||||
* Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
|
||||
* Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
|
||||
* Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
|
||||
* Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
|
||||
* Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
|
||||
* Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
|
||||
* Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
|
||||
* Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
|
||||
* Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
|
||||
* Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
|
||||
* Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
|
||||
* Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
|
||||
* Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
|
||||
* Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
|
||||
* Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
|
||||
* Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
|
||||
* Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
|
||||
|
||||
Changes in [3.31.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.2) (2021-09-22)
|
||||
=============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
|
||||
|
||||
Changes in [3.31.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.1) (2021-09-21)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
|
||||
* Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
|
||||
* Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
|
||||
* Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
|
||||
* Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
|
||||
* Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
|
||||
* Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
|
||||
* Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
|
||||
* Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
|
||||
* Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
|
||||
* Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
|
||||
* Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
|
||||
* Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
|
||||
* Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
|
||||
* Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
|
||||
* hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
|
||||
* Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
|
||||
* Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Revert Firefox composer deletion hacks ([\#6844](https://github.com/matrix-org/matrix-react-sdk/pull/6844)). Fixes vector-im/element-web#19103 and vector-im/element-web#19103. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
|
||||
* Fix pill deletion on Firefox 78 ([\#6832](https://github.com/matrix-org/matrix-react-sdk/pull/6832)). Fixes vector-im/element-web#19077 and vector-im/element-web#19077. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
|
||||
* Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
|
||||
* Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
|
||||
* Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
|
||||
* Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
|
||||
* Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
|
||||
* Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
|
||||
* Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
|
||||
* Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
|
||||
* Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
|
||||
* Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
|
||||
* Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
|
||||
* Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
|
||||
* Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
|
||||
* Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
|
||||
|
||||
Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14)
|
||||
===================================================================================================
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ Code should be committed as follows:
|
|||
* CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
|
||||
* Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
|
||||
React components in matrix-react-sdk are come in two different flavours:
|
||||
React components in matrix-react-sdk come in two different flavours:
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
more complicated business logic of the app, delegating their actual presentation
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
|
|
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.30.0",
|
||||
"version": "3.31.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -25,9 +25,9 @@
|
|||
"bin": {
|
||||
"reskindex": "scripts/reskindex.js"
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"matrix_src_main": "./src/index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
|
@ -79,6 +79,7 @@
|
|||
"highlight.js": "^10.5.0",
|
||||
"html-entities": "^1.4.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.12.0",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
|
@ -133,6 +134,7 @@
|
|||
"@types/counterpart": "^0.18.1",
|
||||
"@types/css-font-loading-module": "^0.0.6",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/linkifyjs": "^2.1.3",
|
||||
|
@ -166,9 +168,11 @@
|
|||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.3",
|
||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rrweb-snapshot": "1.1.7",
|
||||
|
@ -199,6 +203,7 @@
|
|||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
// autogenerated by rethemendex.sh
|
||||
@import "./_animations.scss";
|
||||
@import "./_common.scss";
|
||||
@import "./_font-sizes.scss";
|
||||
@import "./_font-weights.scss";
|
||||
@import "./structures/_AutoHideScrollbar.scss";
|
||||
@import "./structures/_BackdropPanel.scss";
|
||||
@import "./structures/_CompatibilityPage.scss";
|
||||
@import "./structures/_ContextualMenu.scss";
|
||||
@import "./structures/_CreateRoom.scss";
|
||||
|
@ -17,7 +19,6 @@
|
|||
@import "./structures/_LeftPanelWidget.scss";
|
||||
@import "./structures/_MainSplit.scss";
|
||||
@import "./structures/_MatrixChat.scss";
|
||||
@import "./structures/_BackdropPanel.scss";
|
||||
@import "./structures/_MyGroups.scss";
|
||||
@import "./structures/_NonUrgentToastContainer.scss";
|
||||
@import "./structures/_NotificationPanel.scss";
|
||||
|
@ -82,6 +83,7 @@
|
|||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_ExportDialog.scss";
|
||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||
@import "./views/dialogs/_ForwardDialog.scss";
|
||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
|
||||
|
@ -244,6 +246,7 @@
|
|||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||
@import "./views/settings/_EmailAddresses.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
@import "./views/settings/_JoinRuleSettings.scss";
|
||||
@import "./views/settings/_LayoutSwitcher.scss";
|
||||
@import "./views/settings/_Notifications.scss";
|
||||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
|
|
|
@ -89,7 +89,6 @@ limitations under the License.
|
|||
margin: 0px auto;
|
||||
|
||||
overflow: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mx_RoomView_auxPanel_fullHeight {
|
||||
|
|
|
@ -122,7 +122,7 @@ limitations under the License.
|
|||
float: right;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-22px;
|
||||
color: $muted-fg-color;
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
|||
&.mx_Waveform_bar_100pct {
|
||||
// Small animation to remove the mechanical feel of progress
|
||||
transition: background-color 250ms ease;
|
||||
background-color: $message-body-panel-fg-color;
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ExportDialog {
|
||||
.mx_ExportDialog_subheading {
|
||||
font-size: $font-16px;
|
||||
display: block;
|
||||
font-family: $font-family;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-content;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&.mx_ExportDialog_Exporting {
|
||||
.mx_ExportDialog_options {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_Field_select::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_RadioButton input[type="radio"]:checked + div > div {
|
||||
background: $greyed-fg-color;
|
||||
}
|
||||
|
||||
.mx_RadioButton input[type=radio]:checked + div {
|
||||
border-color: unset;
|
||||
}
|
||||
|
||||
.mx_Field_valid.mx_Field label,
|
||||
.mx_Field_valid.mx_Field:focus-within label {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.mx_Field_valid.mx_Field, .mx_Field_valid.mx_Field:focus-within {
|
||||
border-color: $input-border-color;
|
||||
}
|
||||
|
||||
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
||||
background: $greyed-fg-color;
|
||||
border-color: $greyed-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ExportDialog_progress {
|
||||
.mx_Dialog_buttons {
|
||||
margin-top: unset;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.mx_Spinner {
|
||||
width: unset;
|
||||
height: unset;
|
||||
flex: unset;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_RadioButton > .mx_RadioButton_content {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.mx_Field_postfix {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
.mx_InviteDialog_editor {
|
||||
flex: 1;
|
||||
width: 100%; // Needed to make the Field inside grow
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
min-height: 25px;
|
||||
padding-left: 8px;
|
||||
|
@ -167,7 +167,7 @@ limitations under the License.
|
|||
padding: 5px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
@ -395,7 +395,7 @@ limitations under the License.
|
|||
left: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-top: 1px solid $message-body-panel-bg-color;
|
||||
border-top: 1px solid $quinary-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -38,7 +38,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
& + .mx_SettingsTab_subheading {
|
||||
border-top: 1px solid $message-body-panel-bg-color;
|
||||
border-top: 1px solid $quinary-content;
|
||||
margin-top: 0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
|
|
@ -59,3 +59,14 @@ limitations under the License.
|
|||
border-left-color: $username-variant8-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ReplyThread--expanded {
|
||||
.mx_EventTile_body {
|
||||
display: block;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
.mx_EventTile_collapsedCodeBlock {
|
||||
// !important needed due to .mx_ReplyTile .mx_EventTile_content .mx_EventTile_pre_container > pre
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ a.mx_Pill {
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 1ch);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mx_Pill {
|
||||
|
|
|
@ -63,7 +63,7 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
|
||||
.mx_MFileBody_info_icon {
|
||||
background-color: $message-body-panel-icon-bg-color;
|
||||
background-color: $system;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
|
@ -78,7 +78,7 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: cover;
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||
background-color: $message-body-panel-icon-fg-color;
|
||||
background-color: $secondary-content;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
||||
|
|
|
@ -18,11 +18,11 @@ limitations under the License.
|
|||
// have unique styles).
|
||||
|
||||
.mx_MediaBody {
|
||||
background-color: $message-body-panel-bg-color;
|
||||
background-color: $quinary-content;
|
||||
border-radius: 12px;
|
||||
max-width: 243px; // use max-width instead of width so it fits within right panels
|
||||
|
||||
color: $message-body-panel-fg-color;
|
||||
color: $secondary-content;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
|
||||
|
|
|
@ -117,6 +117,16 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/download.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_expandMessageButton::after {
|
||||
mask-size: 12px;
|
||||
mask-image: url('$(res)/img/element-icons/expand-message.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_collapseMessageButton::after {
|
||||
mask-size: 12px;
|
||||
mask-image: url('$(res)/img/element-icons/collapse-message.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
|
||||
background-color: transparent; // hide the download icon mask
|
||||
}
|
||||
|
|
|
@ -243,3 +243,7 @@ limitations under the License.
|
|||
.mx_RoomSummaryCard_icon_settings::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_export::before {
|
||||
mask-image: url('$(res)/img/element-icons/export.svg');
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_VerificationPanel_QRPhase_startOption {
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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_JoinRuleSettings_upgradeRequired {
|
||||
margin-left: 16px;
|
||||
padding: 4px 16px;
|
||||
border: 1px solid $accent-color;
|
||||
border-radius: 8px;
|
||||
color: $accent-color;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
}
|
||||
|
||||
.mx_JoinRuleSettings_spacesWithAccess {
|
||||
> h4 {
|
||||
color: $secondary-content;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: 500;
|
||||
font-size: $font-14px;
|
||||
line-height: 32px; // matches height of avatar for v-align
|
||||
color: $secondary-content;
|
||||
display: inline-block;
|
||||
|
||||
img.mx_RoomAvatar_isSpaceRoom,
|
||||
.mx_RoomAvatar_isSpaceRoom img {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
& + span {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_JoinRuleSettings_radioButton {
|
||||
padding-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mx_RadioButton_content {
|
||||
margin-left: 14px;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-content;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& + span {
|
||||
display: inline-block;
|
||||
margin-left: 34px;
|
||||
margin-bottom: 16px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-content;
|
||||
|
||||
& + .mx_RadioButton {
|
||||
border-top: 1px solid $menu-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_JoinRuleSettings_linkButton {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
|
@ -67,5 +67,7 @@ limitations under the License.
|
|||
|
||||
> .mx_AccessibleButton_kind_link {
|
||||
padding-left: 0; // to align with left side
|
||||
padding-right: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,37 +19,6 @@ limitations under the License.
|
|||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_SecurityRoomSettingsTab_spacesWithAccess {
|
||||
> h4 {
|
||||
color: $secondary-content;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: 500;
|
||||
font-size: $font-14px;
|
||||
line-height: 32px; // matches height of avatar for v-align
|
||||
color: $secondary-content;
|
||||
display: inline-block;
|
||||
|
||||
img.mx_RoomAvatar_isSpaceRoom,
|
||||
.mx_RoomAvatar_isSpaceRoom img {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
& + span {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecurityRoomSettingsTab_warning {
|
||||
|
@ -68,47 +37,3 @@ limitations under the License.
|
|||
border-bottom: 1px solid $menu-border-color;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.mx_SecurityRoomSettingsTab_upgradeRequired {
|
||||
margin-left: 16px;
|
||||
padding: 4px 16px;
|
||||
border: 1px solid $accent-color;
|
||||
border-radius: 8px;
|
||||
color: $accent-color;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
}
|
||||
|
||||
.mx_SecurityRoomSettingsTab_joinRule {
|
||||
.mx_RadioButton {
|
||||
padding-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mx_RadioButton_content {
|
||||
margin-left: 14px;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-content;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
margin-left: 34px;
|
||||
margin-bottom: 16px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-content;
|
||||
|
||||
& + .mx_RadioButton {
|
||||
border-top: 1px solid $menu-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$spacePanelWidth: 71px;
|
||||
$spacePanelWidth: 68px;
|
||||
|
||||
.mx_SpaceCreateMenu_wrapper {
|
||||
// background blur everything except SpacePanel
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#737D8C" fill-rule="evenodd" d="M.2192.234A.753.753 0 011.2815.2321l3.7243 3.7003L8.7181.2202A.753.753 0 019.7805.2185a.747.747 0 01.0017 1.0589L5.5396 5.52a.753.753 0 01-1.0624.0018L.221 1.2928A.747.747 0 01.2192.234zM9.7822 13.7663a.7529.7529 0 01-1.0623.0017l-3.7243-3.7003L1.2833 13.78a.753.753 0 01-1.0624.0018.7471.7471 0 01-.0017-1.059l4.2426-4.2426a.753.753 0 011.0624-.0017l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 543 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#17191C" fill-rule="evenodd" d="M.2192 8.494a.753.753 0 011.0623-.0018l3.7243 3.7003 3.7123-3.7123a.753.753 0 011.0624-.0017.747.747 0 01.0017 1.059L5.5396 13.78a.753.753 0 01-1.0624.0018L.221 9.5528A.747.747 0 01.2192 8.494zM9.7822 5.5063A.753.753 0 018.72 5.508L4.9956 1.8077 1.2833 5.52a.753.753 0 01-1.0624.0018.747.747 0 01-.0017-1.059L4.4618.2202A.753.753 0 015.5242.2185l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 538 B |
|
@ -0,0 +1,14 @@
|
|||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47716 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12.7071 17.7071C12.6112 17.803 12.5007 17.8753 12.3828 17.9241L11.2929 17.7071L11.2925 17.7067L7.2929 13.7071C6.90237 13.3166 6.90237 12.6834 7.2929 12.2929C7.68342 11.9024 8.31658 11.9024 8.70711 12.2929L11 14.5858L11 7C11 6.44771 11.4477 6 12 6C12.5523 6 13 6.44771 13 7L13 14.5858L15.2929 12.2929C15.6834 11.9024 16.3166 11.9024 16.7071 12.2929C17.0976 12.6834 17.0976 13.3166 16.7071 13.7071L12.7071 17.7071ZM12.3828 17.9241L11.295 17.7092C11.4758 17.8889 11.7249 18 12 18C12.1356 18 12.2649 17.973 12.3828 17.9241Z"
|
||||
fill="#C1C6CD"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 821 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#737D8C" fill-rule="evenodd" d="M1 2.75A.75.75 0 0 1 1.75 2h.005a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75ZM1 6.75A.75.75 0 0 1 1.75 6h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 1 6.75ZM1 9.75A.75.75 0 0 1 1.75 9h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 1 9.75ZM1 13.75a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1-.75-.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -206,23 +206,13 @@ $kbd-border-color: #000000;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: $primary-content;
|
||||
|
||||
$interactive-tooltip-bg-color: $background;
|
||||
$interactive-tooltip-fg-color: $primary-content;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-content;
|
||||
$message-body-panel-bg-color: $quinary-content;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
$message-body-panel-icon-fg-color: $secondary-content;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-content;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||
$voice-record-icon-color: $quaternary-content;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// Appearance tab colors
|
||||
$appearance-tab-border-color: $room-highlight-color;
|
||||
|
|
|
@ -202,18 +202,8 @@ $kbd-border-color: #000000;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: #21262C;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
|
|
|
@ -326,26 +326,16 @@ $kbd-border-color: $reaction-row-button-border-color;
|
|||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -16,6 +16,25 @@ limitations under the License.
|
|||
|
||||
$font-family: var(--font-family, $font-family);
|
||||
$monospace-font-family: var(--font-family-monospace, $monospace-font-family);
|
||||
|
||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741
|
||||
$accent: var(--accent);
|
||||
$alert: var(--alert);
|
||||
$links: var(--links);
|
||||
$primary-content: var(--primary-content);
|
||||
$secondary-content: var(--secondary-content);
|
||||
$tertiary-content: var(--tertiary-content);
|
||||
$quaternary-content: var(--quaternary-content);
|
||||
$quinary-content: var(--quinary-content);
|
||||
$system: var(--system);
|
||||
$background: var(--background);
|
||||
$panels: rgba($system, 0.9);
|
||||
$panel-base: var(--panel-base); // This color is not intended for use in the app
|
||||
$panel-selected: rgba($panel-base, 0.3);
|
||||
$panel-hover: rgba($panel-base, 0.1);
|
||||
$panel-actions: rgba($panel-base, 0.2);
|
||||
$space-nav: rgba($panel-base, 0.1);
|
||||
|
||||
//
|
||||
// --accent-color
|
||||
$accent-color: var(--accent-color);
|
||||
|
@ -48,7 +67,6 @@ $roomheader-bg-color: var(--timeline-background-color);
|
|||
$roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
||||
//
|
||||
// --sidebar-color
|
||||
$interactive-tooltip-bg-color: var(--sidebar-color);
|
||||
$groupFilterPanel-bg-color: var(--sidebar-color);
|
||||
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||
|
|
|
@ -326,18 +326,8 @@ $inverted-bg-color: #27303a;
|
|||
$tooltip-timeline-bg-color: $inverted-bg-color;
|
||||
$tooltip-timeline-fg-color: $background;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
$interactive-tooltip-fg-color: $background;
|
||||
|
||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-fg-color: $secondary-content;
|
||||
$message-body-panel-bg-color: $quinary-content;
|
||||
$message-body-panel-icon-bg-color: $system;
|
||||
$message-body-panel-icon-fg-color: $secondary-content;
|
||||
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
|
@ -346,8 +336,8 @@ $voice-record-live-circle-color: #ff4b55;
|
|||
$voice-record-stop-border-color: $quinary-content;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-content;
|
||||
$voice-record-icon-color: $tertiary-content;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
$voice-playback-button-bg-color: $system;
|
||||
$voice-playback-button-fg-color: $secondary-content;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -49,6 +49,9 @@ import PerformanceMonitor from "../performance";
|
|||
import UIStore from "../stores/UIStore";
|
||||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import { Skinner } from "../Skinner";
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
|
@ -92,6 +95,8 @@ declare global {
|
|||
mxUIStore: UIStore;
|
||||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||
mxActiveWidgetStore?: ActiveWidgetStore;
|
||||
mxSkinner?: Skinner;
|
||||
mxOnRecaptchaLoaded?: () => void;
|
||||
electron?: Electron;
|
||||
}
|
||||
|
@ -154,6 +159,10 @@ declare global {
|
|||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface HTMLStyleElement {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Add Chrome-specific `instant` ScrollBehaviour
|
||||
type _ScrollBehavior = ScrollBehavior | "instant";
|
||||
|
||||
|
@ -223,6 +232,15 @@ declare global {
|
|||
) => string;
|
||||
isReady: () => boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-var, camelcase
|
||||
var mx_rage_logger: ConsoleLogger;
|
||||
// eslint-disable-next-line no-var, camelcase
|
||||
var mx_rage_initPromise: Promise<void>;
|
||||
// eslint-disable-next-line no-var, camelcase
|
||||
var mx_rage_initStoragePromise: Promise<void>;
|
||||
// eslint-disable-next-line no-var, camelcase
|
||||
var mx_rage_store: IndexedDBLogStore;
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,11 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ALL_MESSAGES,
|
||||
ALL_MESSAGES_LOUD,
|
||||
MENTIONS_ONLY,
|
||||
MUTE,
|
||||
} from "./RoomNotifs";
|
||||
|
||||
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
|
||||
declare module '!!raw-loader!*' {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
|
||||
import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
function getIdServerDomain() {
|
||||
function getIdServerDomain(): string {
|
||||
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
}
|
||||
|
||||
|
@ -40,10 +41,13 @@ function getIdServerDomain() {
|
|||
* https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
|
||||
*/
|
||||
export default class AddThreepid {
|
||||
private sessionId: string;
|
||||
private submitUrl: string;
|
||||
private clientSecret: string;
|
||||
private bind: boolean;
|
||||
|
||||
constructor() {
|
||||
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
||||
this.sessionId = null;
|
||||
this.submitUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +56,7 @@ export default class AddThreepid {
|
|||
* @param {string} emailAddress The email address to add
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
addEmailAddress(emailAddress) {
|
||||
public addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
|
@ -72,7 +76,7 @@ export default class AddThreepid {
|
|||
* @param {string} emailAddress The email address to add
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
async bindEmailAddress(emailAddress) {
|
||||
public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
|
@ -105,7 +109,7 @@ export default class AddThreepid {
|
|||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
addMsisdn(phoneCountry, phoneNumber) {
|
||||
public addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||
).then((res) => {
|
||||
|
@ -129,7 +133,7 @@ export default class AddThreepid {
|
|||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
async bindMsisdn(phoneCountry, phoneNumber) {
|
||||
public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
|
@ -161,7 +165,7 @@ export default class AddThreepid {
|
|||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
async checkEmailLinkClicked() {
|
||||
public async checkEmailLinkClicked(): Promise<any[]> {
|
||||
try {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (this.bind) {
|
||||
|
@ -175,7 +179,7 @@ export default class AddThreepid {
|
|||
});
|
||||
} else {
|
||||
try {
|
||||
await this._makeAddThreepidOnlyRequest();
|
||||
await this.makeAddThreepidOnlyRequest();
|
||||
|
||||
// The spec has always required this to use UI auth but synapse briefly
|
||||
// implemented it without, so this may just succeed and that's OK.
|
||||
|
@ -186,9 +190,6 @@ export default class AddThreepid {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -208,7 +209,7 @@ export default class AddThreepid {
|
|||
title: _t("Add Email Address"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: e.data,
|
||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
|
@ -235,16 +236,16 @@ export default class AddThreepid {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Object} auth UI auth object
|
||||
* @param {{type: string, session?: string}} auth UI auth object
|
||||
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
|
||||
*/
|
||||
_makeAddThreepidOnlyRequest = (auth) => {
|
||||
private makeAddThreepidOnlyRequest = (auth?: {type: string, session?: string}): Promise<{}> => {
|
||||
return MatrixClientPeg.get().addThreePidOnly({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
auth,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a phone number verification code as entered by the user and validates
|
||||
|
@ -254,7 +255,7 @@ export default class AddThreepid {
|
|||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
async haveMsisdnToken(msisdnToken) {
|
||||
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const supportsSeparateAddAndBind =
|
||||
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
||||
|
@ -291,7 +292,7 @@ export default class AddThreepid {
|
|||
});
|
||||
} else {
|
||||
try {
|
||||
await this._makeAddThreepidOnlyRequest();
|
||||
await this.makeAddThreepidOnlyRequest();
|
||||
|
||||
// The spec has always required this to use UI auth but synapse briefly
|
||||
// implemented it without, so this may just succeed and that's OK.
|
||||
|
@ -302,9 +303,6 @@ export default class AddThreepid {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -324,7 +322,7 @@ export default class AddThreepid {
|
|||
title: _t("Add Phone Number"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: e.data,
|
||||
makeRequest: this._makeAddThreepidOnlyRequest,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
|
@ -161,3 +161,20 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
|
|||
// Compare weekdays
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
}
|
||||
|
||||
export function formatFullDateNoDay(date: Date) {
|
||||
return _t("%(date)s at %(time)s", {
|
||||
date: date.toLocaleDateString().replace(/\//g, '-'),
|
||||
time: date.toLocaleTimeString().replace(/:/g, '-'),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFullDateNoDayNoTime(date: Date) {
|
||||
return (
|
||||
date.getFullYear() +
|
||||
"/" +
|
||||
pad(date.getMonth() + 1) +
|
||||
"/" +
|
||||
pad(date.getDate())
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { createClient, MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
||||
import {
|
||||
|
@ -27,23 +27,25 @@ import {
|
|||
doesIdentityServerHaveTerms,
|
||||
useDefaultIdentityServer,
|
||||
} from './utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from './utils/UrlUtils';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import { abbreviateUrl } from "./utils/UrlUtils";
|
||||
|
||||
export class AbortedIdentityActionError extends Error {}
|
||||
|
||||
export default class IdentityAuthClient {
|
||||
private accessToken: string;
|
||||
private tempClient: MatrixClient;
|
||||
private authEnabled = true;
|
||||
|
||||
/**
|
||||
* Creates a new identity auth client
|
||||
* @param {string} identityUrl The URL to contact the identity server with.
|
||||
* When provided, this class will operate solely within memory, refusing to
|
||||
* persist any information such as tokens. Default null (not provided).
|
||||
*/
|
||||
constructor(identityUrl = null) {
|
||||
this.accessToken = null;
|
||||
this.authEnabled = true;
|
||||
|
||||
constructor(identityUrl?: string) {
|
||||
if (identityUrl) {
|
||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
||||
// do identity server auth. The functions don't take an identity URL
|
||||
|
@ -54,32 +56,29 @@ export default class IdentityAuthClient {
|
|||
baseUrl: "", // invalid by design
|
||||
idBaseUrl: identityUrl,
|
||||
});
|
||||
} else {
|
||||
// Indicates that we're using the real client, not some workaround.
|
||||
this.tempClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
get _matrixClient() {
|
||||
private get matrixClient(): MatrixClient {
|
||||
return this.tempClient ? this.tempClient : MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
_writeToken() {
|
||||
private writeToken(): void {
|
||||
if (this.tempClient) return; // temporary client: ignore
|
||||
window.localStorage.setItem("mx_is_access_token", this.accessToken);
|
||||
}
|
||||
|
||||
_readToken() {
|
||||
private readToken(): string {
|
||||
if (this.tempClient) return null; // temporary client: ignore
|
||||
return window.localStorage.getItem("mx_is_access_token");
|
||||
}
|
||||
|
||||
hasCredentials() {
|
||||
return this.accessToken != null; // undef or null
|
||||
public hasCredentials(): boolean {
|
||||
return Boolean(this.accessToken);
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to the access_token string from the IS
|
||||
async getAccessToken({ check = true } = {}) {
|
||||
public async getAccessToken({ check = true } = {}): Promise<string> {
|
||||
if (!this.authEnabled) {
|
||||
// The current IS doesn't support authentication
|
||||
return null;
|
||||
|
@ -87,21 +86,21 @@ export default class IdentityAuthClient {
|
|||
|
||||
let token = this.accessToken;
|
||||
if (!token) {
|
||||
token = this._readToken();
|
||||
token = this.readToken();
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
token = await this.registerForToken(check);
|
||||
if (token) {
|
||||
this.accessToken = token;
|
||||
this._writeToken();
|
||||
this.writeToken();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
if (check) {
|
||||
try {
|
||||
await this._checkToken(token);
|
||||
await this.checkToken(token);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof TermsNotSignedError ||
|
||||
|
@ -114,7 +113,7 @@ export default class IdentityAuthClient {
|
|||
token = await this.registerForToken();
|
||||
if (token) {
|
||||
this.accessToken = token;
|
||||
this._writeToken();
|
||||
this.writeToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,11 +121,11 @@ export default class IdentityAuthClient {
|
|||
return token;
|
||||
}
|
||||
|
||||
async _checkToken(token) {
|
||||
const identityServerUrl = this._matrixClient.getIdentityServerUrl();
|
||||
private async checkToken(token: string): Promise<void> {
|
||||
const identityServerUrl = this.matrixClient.getIdentityServerUrl();
|
||||
|
||||
try {
|
||||
await this._matrixClient.getIdentityAccount(token);
|
||||
await this.matrixClient.getIdentityAccount(token);
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
||||
logger.log("Identity server requires new terms to be agreed to");
|
||||
|
@ -145,8 +144,8 @@ export default class IdentityAuthClient {
|
|||
!doesAccountDataHaveIdentityServer() &&
|
||||
!(await doesIdentityServerHaveTerms(identityServerUrl))
|
||||
) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Default identity server terms warning', '',
|
||||
QuestionDialog, {
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
|
@ -184,13 +183,13 @@ export default class IdentityAuthClient {
|
|||
// See also https://github.com/vector-im/element-web/issues/10455.
|
||||
}
|
||||
|
||||
async registerForToken(check=true) {
|
||||
public async registerForToken(check = true): Promise<string> {
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
if (check) await this.checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
}
|
||||
}
|
|
@ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
UserActivity.sharedInstance().start();
|
||||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.start();
|
||||
ActiveWidgetStore.instance.start();
|
||||
CallHandler.sharedInstance().start();
|
||||
|
||||
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
|
||||
|
@ -892,7 +892,7 @@ export function stopMatrixClient(unsetClient = true): void {
|
|||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
Presence.stop();
|
||||
ActiveWidgetStore.stop();
|
||||
ActiveWidgetStore.instance.stop();
|
||||
IntegrationManagers.sharedInstance().stopWatching();
|
||||
Mjolnir.sharedInstance().stop();
|
||||
DeviceListener.sharedInstance().stop();
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
import React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
interface IChildProps {
|
||||
style: React.CSSProperties;
|
||||
ref: (node: React.ReactInstance) => void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// either a list of child nodes, or a single child.
|
||||
children: React.ReactNode;
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition?: object;
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: React.CSSProperties[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The NodeAnimator contains components and animates transitions.
|
||||
|
@ -9,55 +24,45 @@ import PropTypes from 'prop-types';
|
|||
* from DOM order. This makes it a lot simpler and lighter: if you need fully
|
||||
* automatic positional animation, look at react-shuffle or similar libraries.
|
||||
*/
|
||||
export default class NodeAnimator extends React.Component {
|
||||
static propTypes = {
|
||||
// either a list of child nodes, or a single child.
|
||||
children: PropTypes.any,
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition: PropTypes.object,
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class NodeAnimator extends React.Component<IProps> {
|
||||
private nodes = {};
|
||||
private children: { [key: string]: React.DetailedReactHTMLElement<any, HTMLElement> };
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
startStyles: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.nodes = {};
|
||||
this._updateChildren(this.props.children);
|
||||
this.updateChildren(this.props.children);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._updateChildren(this.props.children);
|
||||
public componentDidUpdate(): void {
|
||||
this.updateChildren(this.props.children);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} node element to apply styles to
|
||||
* @param {object} styles a key/value pair of CSS properties
|
||||
* @param {React.CSSProperties} styles a key/value pair of CSS properties
|
||||
* @returns {void}
|
||||
*/
|
||||
_applyStyles(node, styles) {
|
||||
private applyStyles(node: HTMLElement, styles: React.CSSProperties): void {
|
||||
Object.entries(styles).forEach(([property, value]) => {
|
||||
node.style[property] = value;
|
||||
});
|
||||
}
|
||||
|
||||
_updateChildren(newChildren) {
|
||||
private updateChildren(newChildren: React.ReactNode): void {
|
||||
const oldChildren = this.children || {};
|
||||
this.children = {};
|
||||
React.Children.toArray(newChildren).forEach((c) => {
|
||||
React.Children.toArray(newChildren).forEach((c: any) => {
|
||||
if (oldChildren[c.key]) {
|
||||
const old = oldChildren[c.key];
|
||||
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
||||
|
||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
||||
this._applyStyles(oldNode, { left: c.props.style.left });
|
||||
if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) {
|
||||
this.applyStyles(oldNode as HTMLElement, { left: c.props.style.left });
|
||||
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
// clone the old element with the props (and children) of the new element
|
||||
|
@ -66,7 +71,7 @@ export default class NodeAnimator extends React.Component {
|
|||
} else {
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
// the enter animations
|
||||
const newProps = {};
|
||||
const newProps: Partial<IChildProps> = {};
|
||||
const restingStyle = c.props.style;
|
||||
|
||||
const startStyles = this.props.startStyles;
|
||||
|
@ -76,7 +81,7 @@ export default class NodeAnimator extends React.Component {
|
|||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||
}
|
||||
|
||||
newProps.ref = ((n) => this._collectNode(
|
||||
newProps.ref = ((n) => this.collectNode(
|
||||
c.key, n, restingStyle,
|
||||
));
|
||||
|
||||
|
@ -85,7 +90,7 @@ export default class NodeAnimator extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_collectNode(k, node, restingStyle) {
|
||||
private collectNode(k: string, node: React.ReactInstance, restingStyle: React.CSSProperties): void {
|
||||
if (
|
||||
node &&
|
||||
this.nodes[k] === undefined &&
|
||||
|
@ -96,7 +101,7 @@ export default class NodeAnimator extends React.Component {
|
|||
// start from startStyle 1: 0 is the one we gave it
|
||||
// to start with, so now we animate 1 etc.
|
||||
for (let i = 1; i < startStyles.length; ++i) {
|
||||
this._applyStyles(domNode, startStyles[i]);
|
||||
this.applyStyles(domNode as HTMLElement, startStyles[i]);
|
||||
// console.log("start:"
|
||||
// JSON.stringify(startStyles[i]),
|
||||
// );
|
||||
|
@ -104,7 +109,7 @@ export default class NodeAnimator extends React.Component {
|
|||
|
||||
// and then we animate to the resting state
|
||||
setTimeout(() => {
|
||||
this._applyStyles(domNode, restingStyle);
|
||||
this.applyStyles(domNode as HTMLElement, restingStyle);
|
||||
}, 0);
|
||||
|
||||
// console.log("enter:",
|
||||
|
@ -113,7 +118,7 @@ export default class NodeAnimator extends React.Component {
|
|||
this.nodes[k] = node;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>{ Object.values(this.children) }</>
|
||||
);
|
|
@ -16,11 +16,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/** The types of page which can be shown by the LoggedInView */
|
||||
export default {
|
||||
HomePage: "home_page",
|
||||
RoomView: "room_view",
|
||||
RoomDirectory: "room_directory",
|
||||
UserView: "user_view",
|
||||
GroupView: "group_view",
|
||||
MyGroups: "my_groups",
|
||||
};
|
||||
enum PageType {
|
||||
HomePage = "home_page",
|
||||
RoomView = "room_view",
|
||||
RoomDirectory = "room_directory",
|
||||
UserView = "user_view",
|
||||
GroupView = "group_view",
|
||||
MyGroups = "my_groups",
|
||||
}
|
||||
|
||||
export default PageType;
|
|
@ -20,10 +20,11 @@ limitations under the License.
|
|||
* registration code.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
|
||||
// Regex for what a "safe" or "Matrix-looking" localpart would be.
|
||||
// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
|
||||
|
@ -41,9 +42,11 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
|||
* @param {bool} options.screen_after
|
||||
* If present the screen to redirect to after a successful login or register.
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
export async function startAnyRegistrationFlow(
|
||||
// eslint-disable-next-line camelcase
|
||||
options: { go_home_on_cancel?: boolean, go_welcome_on_cancel?: boolean, screen_after?: boolean},
|
||||
): Promise<void> {
|
||||
if (options === undefined) options = {};
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
|
@ -17,27 +17,31 @@ limitations under the License.
|
|||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IAnnotatedPushRule, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||
|
||||
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
|
||||
export const ALL_MESSAGES = 'all_messages';
|
||||
export const MENTIONS_ONLY = 'mentions_only';
|
||||
export const MUTE = 'mute';
|
||||
export enum RoomNotifState {
|
||||
AllMessagesLoud = 'all_messages_loud',
|
||||
AllMessages = 'all_messages',
|
||||
MentionsOnly = 'mentions_only',
|
||||
Mute = 'mute',
|
||||
}
|
||||
|
||||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
||||
export const BADGE_STATES = [RoomNotifState.AllMessages, RoomNotifState.AllMessagesLoud];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, RoomNotifState.MentionsOnly];
|
||||
|
||||
export function shouldShowNotifBadge(roomNotifState) {
|
||||
export function shouldShowNotifBadge(roomNotifState: RoomNotifState): boolean {
|
||||
return BADGE_STATES.includes(roomNotifState);
|
||||
}
|
||||
|
||||
export function shouldShowMentionBadge(roomNotifState) {
|
||||
export function shouldShowMentionBadge(roomNotifState: RoomNotifState): boolean {
|
||||
return MENTION_BADGE_STATES.includes(roomNotifState);
|
||||
}
|
||||
|
||||
export function aggregateNotificationCount(rooms) {
|
||||
return rooms.reduce((result, room) => {
|
||||
export function aggregateNotificationCount(rooms: Room[]): {count: number, highlight: boolean} {
|
||||
return rooms.reduce<{count: number, highlight: boolean}>((result, room) => {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
|
||||
// use helper method to include highlights in the previous version of the room
|
||||
const notificationCount = getUnreadNotificationCount(room);
|
||||
|
||||
|
@ -55,9 +59,9 @@ export function aggregateNotificationCount(rooms) {
|
|||
}, { count: 0, highlight: false });
|
||||
}
|
||||
|
||||
export function getRoomHasBadge(room) {
|
||||
export function getRoomHasBadge(room: Room): boolean {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||
|
@ -66,14 +70,14 @@ export function getRoomHasBadge(room) {
|
|||
return notifBadges || mentionBadges;
|
||||
}
|
||||
|
||||
export function getRoomNotifsState(roomId) {
|
||||
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
|
||||
export function getRoomNotifsState(roomId: string): RoomNotifState {
|
||||
if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages;
|
||||
|
||||
// look through the override rules for a rule affecting this room:
|
||||
// if one exists, it will take precedence.
|
||||
const muteRule = findOverrideMuteRule(roomId);
|
||||
if (muteRule) {
|
||||
return MUTE;
|
||||
return RoomNotifState.Mute;
|
||||
}
|
||||
|
||||
// for everything else, look at the room rule.
|
||||
|
@ -89,27 +93,27 @@ export function getRoomNotifsState(roomId) {
|
|||
// XXX: We have to assume the default is to notify for all messages
|
||||
// (in particular this will be 'wrong' for one to one rooms because
|
||||
// they will notify loudly for all messages)
|
||||
if (!roomRule || !roomRule.enabled) return ALL_MESSAGES;
|
||||
if (!roomRule || !roomRule.enabled) return RoomNotifState.AllMessages;
|
||||
|
||||
// a mute at the room level will still allow mentions
|
||||
// to notify
|
||||
if (isMuteRule(roomRule)) return MENTIONS_ONLY;
|
||||
if (isMuteRule(roomRule)) return RoomNotifState.MentionsOnly;
|
||||
|
||||
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
|
||||
if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD;
|
||||
if (actionsObject.tweaks.sound) return RoomNotifState.AllMessagesLoud;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setRoomNotifsState(roomId, newState) {
|
||||
if (newState === MUTE) {
|
||||
export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Promise<void> {
|
||||
if (newState === RoomNotifState.Mute) {
|
||||
return setRoomNotifsStateMuted(roomId);
|
||||
} else {
|
||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||
}
|
||||
}
|
||||
|
||||
export function getUnreadNotificationCount(room, type=null) {
|
||||
export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number {
|
||||
let notificationCount = room.getUnreadNotificationCount(type);
|
||||
|
||||
// Check notification counts in the old room just in case there's some lost
|
||||
|
@ -124,21 +128,21 @@ export function getUnreadNotificationCount(room, type=null) {
|
|||
// notifying the user for unread messages because they would have extreme
|
||||
// difficulty changing their notification preferences away from "All Messages"
|
||||
// and "Noisy".
|
||||
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
|
||||
notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
function setRoomNotifsStateMuted(roomId) {
|
||||
function setRoomNotifsStateMuted(roomId: string): Promise<any> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const promises = [];
|
||||
|
||||
// delete the room rule
|
||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||
if (roomRule) {
|
||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
|
||||
}
|
||||
|
||||
// add/replace an override rule to squelch everything in this room
|
||||
|
@ -146,7 +150,7 @@ function setRoomNotifsStateMuted(roomId) {
|
|||
// is an override rule, not a room rule: it still pertains to this room
|
||||
// though, so using the room ID as the rule ID is logical and prevents
|
||||
// duplicate copies of the rule.
|
||||
promises.push(cli.addPushRule('global', 'override', roomId, {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
|
||||
conditions: [
|
||||
{
|
||||
kind: 'event_match',
|
||||
|
@ -162,30 +166,30 @@ function setRoomNotifsStateMuted(roomId) {
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise<any> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const promises = [];
|
||||
|
||||
const overrideMuteRule = findOverrideMuteRule(roomId);
|
||||
if (overrideMuteRule) {
|
||||
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.Override, overrideMuteRule.rule_id));
|
||||
}
|
||||
|
||||
if (newState === 'all_messages') {
|
||||
if (newState === RoomNotifState.AllMessages) {
|
||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||
if (roomRule) {
|
||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
||||
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
|
||||
}
|
||||
} else if (newState === 'mentions_only') {
|
||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||
} else if (newState === RoomNotifState.MentionsOnly) {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||
actions: [
|
||||
'dont_notify',
|
||||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
} else if ('all_messages_loud') {
|
||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
|
||||
} else if (newState === RoomNotifState.AllMessagesLoud) {
|
||||
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
|
||||
actions: [
|
||||
'notify',
|
||||
{
|
||||
|
@ -195,13 +199,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
|||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function findOverrideMuteRule(roomId) {
|
||||
function findOverrideMuteRule(roomId: string): IAnnotatedPushRule {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.pushRules ||
|
||||
!cli.pushRules['global'] ||
|
||||
|
@ -218,7 +222,7 @@ function findOverrideMuteRule(roomId) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function isRuleForRoom(roomId, rule) {
|
||||
function isRuleForRoom(roomId: string, rule: IAnnotatedPushRule): boolean {
|
||||
if (rule.conditions.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
@ -226,6 +230,6 @@ function isRuleForRoom(roomId, rule) {
|
|||
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
||||
}
|
||||
|
||||
function isMuteRule(rule) {
|
||||
function isMuteRule(rule: IAnnotatedPushRule): boolean {
|
||||
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||
}
|
|
@ -247,13 +247,31 @@ import { objectClone } from "./utils/objects";
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
function sendResponse(event, res) {
|
||||
enum Action {
|
||||
CloseScalar = "close_scalar",
|
||||
GetWidgets = "get_widgets",
|
||||
SetWidgets = "set_widgets",
|
||||
SetWidget = "set_widget",
|
||||
JoinRulesState = "join_rules_state",
|
||||
SetPlumbingState = "set_plumbing_state",
|
||||
GetMembershipCount = "get_membership_count",
|
||||
GetRoomEncryptionState = "get_room_enc_state",
|
||||
CanSendEvent = "can_send_event",
|
||||
MembershipState = "membership_state",
|
||||
invite = "invite",
|
||||
BotOptions = "bot_options",
|
||||
SetBotOptions = "set_bot_options",
|
||||
SetBotPower = "set_bot_power",
|
||||
}
|
||||
|
||||
function sendResponse(event: MessageEvent<any>, res: any): void {
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
// @ts-ignore
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
function sendError(event, msg, nestedError) {
|
||||
function sendError(event: MessageEvent<any>, msg: string, nestedError?: Error): void {
|
||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
|
@ -264,10 +282,11 @@ function sendError(event, msg, nestedError) {
|
|||
if (nestedError) {
|
||||
data.response.error._error = nestedError;
|
||||
}
|
||||
// @ts-ignore
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
function inviteUser(event, roomId, userId) {
|
||||
function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`Received request to invite ${userId} into room ${roomId}`);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
|
@ -295,7 +314,7 @@ function inviteUser(event, roomId, userId) {
|
|||
});
|
||||
}
|
||||
|
||||
function setWidget(event, roomId) {
|
||||
function setWidget(event: MessageEvent<any>, roomId: string): void {
|
||||
const widgetId = event.data.widget_id;
|
||||
let widgetType = event.data.type;
|
||||
const widgetUrl = event.data.url;
|
||||
|
@ -356,7 +375,7 @@ function setWidget(event, roomId) {
|
|||
}
|
||||
}
|
||||
|
||||
function getWidgets(event, roomId) {
|
||||
function getWidgets(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -382,7 +401,7 @@ function getWidgets(event, roomId) {
|
|||
sendResponse(event, widgetStateEvents);
|
||||
}
|
||||
|
||||
function getRoomEncState(event, roomId) {
|
||||
function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -398,7 +417,7 @@ function getRoomEncState(event, roomId) {
|
|||
sendResponse(event, roomIsEncrypted);
|
||||
}
|
||||
|
||||
function setPlumbingState(event, roomId, status) {
|
||||
function setPlumbingState(event: MessageEvent<any>, roomId: string, status: string): void {
|
||||
if (typeof status !== 'string') {
|
||||
throw new Error('Plumbing state status should be a string');
|
||||
}
|
||||
|
@ -417,7 +436,7 @@ function setPlumbingState(event, roomId, status) {
|
|||
});
|
||||
}
|
||||
|
||||
function setBotOptions(event, roomId, userId) {
|
||||
function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
|
@ -433,7 +452,7 @@ function setBotOptions(event, roomId, userId) {
|
|||
});
|
||||
}
|
||||
|
||||
function setBotPower(event, roomId, userId, level) {
|
||||
function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, level: number): void {
|
||||
if (!(Number.isInteger(level) && level >= 0)) {
|
||||
sendError(event, _t('Power level must be positive integer.'));
|
||||
return;
|
||||
|
@ -464,22 +483,22 @@ function setBotPower(event, roomId, userId, level) {
|
|||
});
|
||||
}
|
||||
|
||||
function getMembershipState(event, roomId, userId) {
|
||||
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.member", userId);
|
||||
}
|
||||
|
||||
function getJoinRules(event, roomId) {
|
||||
function getJoinRules(event: MessageEvent<any>, roomId: string): void {
|
||||
logger.log(`join_rules of ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.join_rules", "");
|
||||
}
|
||||
|
||||
function botOptions(event, roomId, userId) {
|
||||
function botOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||
logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
||||
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||
}
|
||||
|
||||
function getMembershipCount(event, roomId) {
|
||||
function getMembershipCount(event: MessageEvent<any>, roomId: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -494,7 +513,7 @@ function getMembershipCount(event, roomId) {
|
|||
sendResponse(event, count);
|
||||
}
|
||||
|
||||
function canSendEvent(event, roomId) {
|
||||
function canSendEvent(event: MessageEvent<any>, roomId: string): void {
|
||||
const evType = "" + event.data.event_type; // force stringify
|
||||
const isState = Boolean(event.data.is_state);
|
||||
const client = MatrixClientPeg.get();
|
||||
|
@ -528,7 +547,7 @@ function canSendEvent(event, roomId) {
|
|||
sendResponse(event, true);
|
||||
}
|
||||
|
||||
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||
function returnStateEvent(event: MessageEvent<any>, roomId: string, eventType: string, stateKey: string): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
|
@ -547,8 +566,9 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
|||
sendResponse(event, stateEvent.getContent());
|
||||
}
|
||||
|
||||
const onMessage = function(event) {
|
||||
const onMessage = function(event: MessageEvent<any>): void {
|
||||
if (!event.origin) { // stupid chrome
|
||||
// @ts-ignore
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
|
@ -582,8 +602,8 @@ const onMessage = function(event) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.data.action === "close_scalar") {
|
||||
dis.dispatch({ action: "close_scalar" });
|
||||
if (event.data.action === Action.CloseScalar) {
|
||||
dis.dispatch({ action: Action.CloseScalar });
|
||||
sendResponse(event, null);
|
||||
return;
|
||||
}
|
||||
|
@ -596,10 +616,10 @@ const onMessage = function(event) {
|
|||
// Get and set user widgets (not associated with a specific room)
|
||||
// If roomId is specified, it must be validated, so room-based widgets agreed
|
||||
// handled further down.
|
||||
if (event.data.action === "get_widgets") {
|
||||
if (event.data.action === Action.GetWidgets) {
|
||||
getWidgets(event, null);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
} else if (event.data.action === Action.SetWidgets) {
|
||||
setWidget(event, null);
|
||||
return;
|
||||
} else {
|
||||
|
@ -614,28 +634,28 @@ const onMessage = function(event) {
|
|||
}
|
||||
|
||||
// Get and set room-based widgets
|
||||
if (event.data.action === "get_widgets") {
|
||||
if (event.data.action === Action.GetWidgets) {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
} else if (event.data.action === Action.SetWidget) {
|
||||
setWidget(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
if (event.data.action === Action.JoinRulesState) {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_plumbing_state") {
|
||||
} else if (event.data.action === Action.SetPlumbingState) {
|
||||
setPlumbingState(event, roomId, event.data.status);
|
||||
return;
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
} else if (event.data.action === Action.GetMembershipCount) {
|
||||
getMembershipCount(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_room_enc_state") {
|
||||
} else if (event.data.action === Action.GetRoomEncryptionState) {
|
||||
getRoomEncState(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
} else if (event.data.action === Action.CanSendEvent) {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
@ -645,19 +665,19 @@ const onMessage = function(event) {
|
|||
return;
|
||||
}
|
||||
switch (event.data.action) {
|
||||
case "membership_state":
|
||||
case Action.MembershipState:
|
||||
getMembershipState(event, roomId, userId);
|
||||
break;
|
||||
case "invite":
|
||||
case Action.invite:
|
||||
inviteUser(event, roomId, userId);
|
||||
break;
|
||||
case "bot_options":
|
||||
case Action.BotOptions:
|
||||
botOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_options":
|
||||
case Action.SetBotOptions:
|
||||
setBotOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_power":
|
||||
case Action.SetBotPower:
|
||||
setBotPower(event, roomId, userId, event.data.level);
|
||||
break;
|
||||
default:
|
||||
|
@ -667,16 +687,16 @@ const onMessage = function(event) {
|
|||
};
|
||||
|
||||
let listenerCount = 0;
|
||||
let openManagerUrl = null;
|
||||
let openManagerUrl: string = null;
|
||||
|
||||
export function startListening() {
|
||||
export function startListening(): void {
|
||||
if (listenerCount === 0) {
|
||||
window.addEventListener("message", onMessage, false);
|
||||
}
|
||||
listenerCount += 1;
|
||||
}
|
||||
|
||||
export function stopListening() {
|
||||
export function stopListening(): void {
|
||||
listenerCount -= 1;
|
||||
if (listenerCount === 0) {
|
||||
window.removeEventListener("message", onMessage);
|
||||
|
@ -691,6 +711,6 @@ export function stopListening() {
|
|||
}
|
||||
}
|
||||
|
||||
export function setOpenManagerUrl(url) {
|
||||
export function setOpenManagerUrl(url: string): void {
|
||||
openManagerUrl = url;
|
||||
}
|
|
@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
class Skinner {
|
||||
constructor() {
|
||||
this.components = null;
|
||||
}
|
||||
import React from "react";
|
||||
|
||||
getComponent(name) {
|
||||
export interface IComponents {
|
||||
[key: string]: React.Component;
|
||||
}
|
||||
|
||||
export interface ISkinObject {
|
||||
components: IComponents;
|
||||
}
|
||||
|
||||
export class Skinner {
|
||||
public components: IComponents = null;
|
||||
|
||||
public getComponent(name: string): React.Component {
|
||||
if (!name) throw new Error(`Invalid component name: ${name}`);
|
||||
if (this.components === null) {
|
||||
throw new Error(
|
||||
|
@ -30,7 +38,7 @@ class Skinner {
|
|||
);
|
||||
}
|
||||
|
||||
const doLookup = (components) => {
|
||||
const doLookup = (components: IComponents): React.Component => {
|
||||
if (!components) return null;
|
||||
let comp = components[name];
|
||||
// XXX: Temporarily also try 'views.' as we're currently
|
||||
|
@ -58,7 +66,7 @@ class Skinner {
|
|||
return comp;
|
||||
}
|
||||
|
||||
load(skinObject) {
|
||||
public load(skinObject: ISkinObject): void {
|
||||
if (this.components !== null) {
|
||||
throw new Error(
|
||||
"Attempted to load a skin while a skin is already loaded"+
|
||||
|
@ -72,6 +80,7 @@ class Skinner {
|
|||
}
|
||||
|
||||
// Now that we have a skin, load our components too
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const idx = require("./component-index");
|
||||
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
|
||||
for (const c in idx.components) {
|
||||
|
@ -79,7 +88,7 @@ class Skinner {
|
|||
}
|
||||
}
|
||||
|
||||
addComponent(name, comp) {
|
||||
public addComponent(name: string, comp: any) {
|
||||
let slot = name;
|
||||
if (comp.replaces !== undefined) {
|
||||
if (comp.replaces.indexOf('.') > -1) {
|
||||
|
@ -91,7 +100,7 @@ class Skinner {
|
|||
this.components[slot] = comp;
|
||||
}
|
||||
|
||||
reset() {
|
||||
public reset(): void {
|
||||
this.components = null;
|
||||
}
|
||||
}
|
||||
|
@ -105,8 +114,8 @@ class Skinner {
|
|||
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||
// ("Modules are cached based on their resolved filename")
|
||||
if (global.mxSkinner === undefined) {
|
||||
global.mxSkinner = new Skinner();
|
||||
if (window.mxSkinner === undefined) {
|
||||
window.mxSkinner = new Skinner();
|
||||
}
|
||||
export default global.mxSkinner;
|
||||
export default window.mxSkinner;
|
||||
|
|
@ -166,6 +166,11 @@ function textForTopicEvent(ev: MatrixEvent): () => string | null {
|
|||
});
|
||||
}
|
||||
|
||||
function textForRoomAvatarEvent(ev: MatrixEvent): () => string | null {
|
||||
const senderDisplayName = ev?.sender?.name || ev.getSender();
|
||||
return () => _t('%(senderDisplayName)s changed the room avatar.', { senderDisplayName });
|
||||
}
|
||||
|
||||
function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
|
@ -289,11 +294,27 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null {
|
|||
function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
||||
return () => {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
let message = ev.getContent().body;
|
||||
if (ev.isRedacted()) {
|
||||
message = _t("Message deleted");
|
||||
const unsigned = ev.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned?.redacted_because?.sender;
|
||||
if (redactedBecauseUserId && redactedBecauseUserId !== ev.getSender()) {
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const sender = room?.getMember(redactedBecauseUserId);
|
||||
message = _t("Message deleted by %(name)s", { name: sender?.name
|
||||
|| redactedBecauseUserId });
|
||||
}
|
||||
}
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
||||
} else if (ev.getType() == "m.sticker") {
|
||||
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
|
||||
} else {
|
||||
// in this case, parse it as a plain text message
|
||||
message = senderDisplayName + ': ' + message;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
@ -669,6 +690,7 @@ interface IHandlers {
|
|||
|
||||
const handlers: IHandlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.sticker': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
};
|
||||
|
||||
|
@ -677,6 +699,7 @@ const stateHandlers: IHandlers = {
|
|||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
"m.room.avatar": textForRoomAvatarEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
|
|
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
|
||||
export async function getThreepidsWithBindStatus(client, filterMedium) {
|
||||
export async function getThreepidsWithBindStatus(
|
||||
client: MatrixClient, filterMedium?: ThreepidMedium,
|
||||
): Promise<IThreepid[]> {
|
||||
const userId = client.getUserId();
|
||||
|
||||
let { threepids } = await client.getThreePids();
|
||||
|
@ -31,7 +35,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) {
|
|||
const identityAccessToken = await authClient.getAccessToken({ check: false });
|
||||
|
||||
// Restructure for lookup query
|
||||
const query = threepids.map(({ medium, address }) => [medium, address]);
|
||||
const query = threepids.map(({ medium, address }): [string, string] => [medium, address]);
|
||||
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
|
||||
|
||||
// Record which are already bound
|
|
@ -45,7 +45,7 @@ function getOrCreateContainer(): HTMLDivElement {
|
|||
|
||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||
|
||||
interface IPosition {
|
||||
export interface IPosition {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
|
@ -226,6 +226,11 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
// Don't allow clicks to escape the context menu wrapper
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
@ -383,6 +388,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
<div
|
||||
|
@ -430,7 +436,11 @@ export type AboveLeftOf = IPosition & {
|
|||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => {
|
||||
export const aboveLeftOf = (
|
||||
elementRect: DOMRect,
|
||||
chevronFace = ChevronFace.None,
|
||||
vPadding = 0,
|
||||
): AboveLeftOf => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
|
|
|
@ -42,7 +42,7 @@ import linkifyMatrix from "../../linkify-matrix";
|
|||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import PageType from '../../PageTypes';
|
||||
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||
|
@ -207,7 +207,7 @@ interface IState {
|
|||
view: Views;
|
||||
// What the LoggedInView would be showing if visible
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type?: PageTypes;
|
||||
page_type?: PageType;
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
|
@ -723,7 +723,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_my_groups':
|
||||
this.setPage(PageTypes.MyGroups);
|
||||
this.setPage(PageType.MyGroups);
|
||||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
|
@ -756,7 +756,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
localStorage.setItem("mx_seenSpacesBeta", "1");
|
||||
// We just dispatch the page change rather than have to worry about
|
||||
// what the logic is for each of these branches.
|
||||
if (this.state.page_type === PageTypes.MyGroups) {
|
||||
if (this.state.page_type === PageType.MyGroups) {
|
||||
dis.dispatch({ action: 'view_last_screen' });
|
||||
} else {
|
||||
dis.dispatch({ action: 'view_my_groups' });
|
||||
|
@ -842,7 +842,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private setPage(pageType: string) {
|
||||
private setPage(pageType: PageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
});
|
||||
|
@ -949,7 +949,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
page_type: PageType.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
ready: true,
|
||||
|
@ -977,7 +977,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this.setPage(PageTypes.GroupView);
|
||||
this.setPage(PageType.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
|
||||
|
@ -1020,7 +1020,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
justRegistered,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.setPage(PageTypes.HomePage);
|
||||
this.setPage(PageType.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
ThemeController.isLogin = false;
|
||||
this.themeWatcher.recheck();
|
||||
|
@ -1038,7 +1038,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
this.setState({ currentUserId: userId });
|
||||
this.setPage(PageTypes.UserView);
|
||||
this.setPage(PageType.UserView);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
|||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -60,7 +62,7 @@ const groupedEvents = [
|
|||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(
|
||||
export function shouldFormContinuation(
|
||||
prevEvent: MatrixEvent,
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRoomMembersCount = (): void => {
|
||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return { nextEvent, nextTile };
|
||||
}
|
||||
|
||||
private get roomHasPendingEdit(): string {
|
||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
private get pendingEditItem(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this.roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this.roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -78,7 +78,6 @@ import { objectHasDiff } from "../../utils/objects";
|
|||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { throttle } from "lodash";
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
|
@ -92,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -103,7 +103,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -114,7 +114,7 @@ interface IProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
|
@ -158,7 +158,6 @@ export interface IState {
|
|||
// used by componentDidUpdate to avoid unnecessary checks
|
||||
atEndOfLiveTimelineInit: boolean;
|
||||
showTopUnreadMessagesBar: boolean;
|
||||
auxPanelMaxHeight?: number;
|
||||
statusBarVisible: boolean;
|
||||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
|
@ -189,10 +188,12 @@ export interface IState {
|
|||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
liveTimeline?: EventTimeline;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
@ -249,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -338,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState: Pick<IState, any> = {
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
|
@ -565,10 +568,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -656,9 +655,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this.roomStoreToken) {
|
||||
|
@ -817,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if we're trying to edit events in wrong rendering context
|
||||
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
|
@ -941,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
};
|
||||
|
||||
private async calculateRecommendedVersion(room: Room) {
|
||||
|
@ -1619,28 +1621,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
};
|
||||
}
|
||||
|
||||
private onResize = () => {
|
||||
// It seems flexbox doesn't give us a way to constrain the auxPanel height to have
|
||||
// a minimum of the height of the video element, whilst also capping it from pushing out the page
|
||||
// so we have to do it via JS instead. In this implementation we cap the height by putting
|
||||
// a maxHeight on the underlying remote video tag.
|
||||
|
||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||
(54 + // height of RoomHeader
|
||||
36 + // height of the status area
|
||||
51 + // minimum height of the message composer
|
||||
120); // amount of desired scrollback
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
|
||||
this.setState({ auxPanelMaxHeight });
|
||||
}
|
||||
};
|
||||
|
||||
private onStatusBarVisible = () => {
|
||||
if (this.unmounted || this.state.statusBarVisible) return;
|
||||
this.setState({ statusBarVisible: true });
|
||||
|
@ -1941,11 +1921,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
const auxPanel = (
|
||||
<AuxPanel
|
||||
room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={this.context.credentials.userId}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
showApps={this.state.showApps}
|
||||
onResize={this.onResize}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ aux }
|
||||
|
@ -2120,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
|
|
|
@ -277,8 +277,15 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
// so check difference <= 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||
//
|
||||
// We therefore leave a bit of wiggle-room and assume we're at the
|
||||
// bottom if the unscrolled area is less than one pixel high.
|
||||
//
|
||||
// non-standard DPI settings also seem to have effect here and can
|
||||
// actually lead to scrollTop+clientHeight being *larger* than
|
||||
// scrollHeight. (observed in element-desktop on Ubuntu 20.04)
|
||||
//
|
||||
return sn.scrollHeight - (sn.scrollTop + sn.clientHeight) <= 1;
|
||||
};
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
|
|
|
@ -446,13 +446,13 @@ export const useSpaceSummary = (space: Room): {
|
|||
}));
|
||||
|
||||
const loadMore = useCallback(async (pageSize?: number) => {
|
||||
if (!hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
if (loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
|
||||
setLoading(true);
|
||||
await hierarchy.load(pageSize);
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
}, [hierarchy]);
|
||||
}, [loading, hierarchy]);
|
||||
|
||||
return { loading, rooms, hierarchy, loadMore };
|
||||
};
|
||||
|
@ -648,8 +648,6 @@ const SpaceHierarchy = ({
|
|||
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => {
|
||||
let content: JSX.Element;
|
||||
let loader: JSX.Element;
|
||||
|
||||
if (loading && !rooms.length) {
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
|
@ -671,19 +669,20 @@ const SpaceHierarchy = ({
|
|||
}}
|
||||
/>
|
||||
</>;
|
||||
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
} else {
|
||||
} else if (!hierarchy.canLoadMore) {
|
||||
results = <div className="mx_SpaceHierarchy_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let loader: JSX.Element;
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceHierarchy_listHeader">
|
||||
<h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4>
|
||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,10 +49,14 @@ interface IProps {
|
|||
interface IState {
|
||||
replyToEvent?: MatrixEvent;
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.setupThread(payload.event);
|
||||
}
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if it's not a thread context
|
||||
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
|
||||
// Quit early if that's not a thread event
|
||||
if (payload.event && !payload.event.getThread()) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
|
@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Notif}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
/>
|
||||
) }
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||
resizeMethod?: ResizeMethod;
|
||||
// The onClick to give the avatar
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick?: boolean;
|
||||
title?: string;
|
||||
style?: any;
|
||||
|
|
|
@ -34,10 +34,10 @@ import ForwardDialog from "../dialogs/ForwardDialog";
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import ReportEventDialog from '../dialogs/ReportEventDialog';
|
||||
import ViewSource from '../../structures/ViewSource';
|
||||
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||
import ShareDialog from '../dialogs/ShareDialog';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
|
||||
|
||||
export function canCancel(eventStatus: EventStatus): boolean {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -52,7 +52,8 @@ export interface IOperableEventTile {
|
|||
getEventTileOps(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends IPosition {
|
||||
chevronFace: ChevronFace;
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: MatrixEvent;
|
||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||
|
@ -138,34 +139,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private onRedactClick = (): void => {
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed: boolean, reason?: string) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
this.props.onCloseDialog?.();
|
||||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
undefined,
|
||||
reason ? { reason } : {},
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
// only show the dialog if failing for something other than a network error
|
||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||
// detached queue and we show the room status bar to allow retry
|
||||
if (typeof code !== "undefined") {
|
||||
// display error message stating you couldn't delete this.
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this message. (%(code)s)', { code }),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
const { mxEvent, onCloseDialog } = this.props;
|
||||
createRedactEventDialog({
|
||||
mxEvent,
|
||||
onCloseDialog,
|
||||
});
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
|
|
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ErrorDialog from './ErrorDialog';
|
||||
import TextInputDialog from "./TextInputDialog";
|
||||
|
||||
interface IProps {
|
||||
|
@ -42,3 +46,40 @@ export default class ConfirmRedactDialog extends React.Component<IProps> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createRedactEventDialog({
|
||||
mxEvent,
|
||||
onCloseDialog = () => {},
|
||||
}: {
|
||||
mxEvent: MatrixEvent;
|
||||
onCloseDialog?: () => void;
|
||||
}) {
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed: boolean, reason?: string) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
onCloseDialog?.();
|
||||
await cli.redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
mxEvent.getId(),
|
||||
undefined,
|
||||
reason ? { reason } : {},
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
// only show the dialog if failing for something other than a network error
|
||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||
// detached queue and we show the room status bar to allow retry
|
||||
if (typeof code !== "undefined") {
|
||||
// display error message stating you couldn't delete this.
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this message. (%(code)s)', { code }),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -131,8 +131,13 @@ interface IProps {
|
|||
}
|
||||
|
||||
const isOnlyAdmin = (room: Room): boolean => {
|
||||
return !room.getJoinedMembers().some(member => {
|
||||
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
|
||||
const userId = room.client.getUserId();
|
||||
if (room.getMember(userId).powerLevelNorm !== 100) {
|
||||
return false; // user is not an admin
|
||||
}
|
||||
return room.getJoinedMembers().every(member => {
|
||||
// return true if every other member has a lower power level (we are highest)
|
||||
return member.userId === userId || member.powerLevelNorm < 100;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
|
||||
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
if (this.sgWidget) this.sgWidget.stop();
|
||||
}
|
||||
|
@ -198,8 +198,8 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
|
||||
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
|
||||
}
|
||||
|
|
|
@ -178,6 +178,14 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.ignoreEvent = ev;
|
||||
};
|
||||
|
||||
private onChevronClick = (ev: React.MouseEvent) => {
|
||||
if (this.state.expanded) {
|
||||
this.setState({ expanded: false });
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
|
@ -375,7 +383,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
<span onClick={this.onChevronClick} className="mx_Dropdown_arrow" />
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -39,13 +39,13 @@ export default class PersistentApp extends React.Component<{}, IState> {
|
|||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ export default class PersistentApp extends React.Component<{}, IState> {
|
|||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
|
||||
ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
@ -68,23 +68,23 @@ export default class PersistentApp extends React.Component<{}, IState> {
|
|||
|
||||
private onActiveWidgetStoreUpdate = (): void => {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
if (membership !== "join") {
|
||||
// we're not in the room anymore - delete
|
||||
if (room .roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
|
@ -96,7 +96,7 @@ export default class PersistentApp extends React.Component<{}, IState> {
|
|||
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile";
|
|||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
/**
|
||||
* This number is based on the previous behavior - if we have message of height
|
||||
* over 60px then we want to show button that will allow to expand it.
|
||||
*/
|
||||
const SHOW_EXPAND_QUOTE_PIXELS = 60;
|
||||
|
||||
interface IProps {
|
||||
// the latest event in this chain of replies
|
||||
parentEv?: MatrixEvent;
|
||||
|
@ -45,6 +53,9 @@ interface IProps {
|
|||
layout?: Layout;
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps?: boolean;
|
||||
forExport?: boolean;
|
||||
isQuoteExpanded?: boolean;
|
||||
setQuoteExpanded: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -66,6 +77,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
static contextType = MatrixClientContext;
|
||||
private unmounted = false;
|
||||
private room: Room;
|
||||
private blockquoteRef = React.createRef<HTMLElement>();
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -80,7 +92,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
}
|
||||
|
||||
public static getParentEventId(ev: MatrixEvent): string {
|
||||
public static getParentEventId(ev: MatrixEvent): string | undefined {
|
||||
if (!ev || ev.isRedacted()) return;
|
||||
|
||||
// XXX: For newer relations (annotations, replacements, etc.), we now
|
||||
|
@ -137,7 +149,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
public static getNestedReplyText(
|
||||
ev: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): { body: string, html: string } {
|
||||
): { body: string, html: string } | null {
|
||||
if (!ev) return null;
|
||||
|
||||
let { body, formatted_body: html } = ev.getContent();
|
||||
|
@ -237,37 +249,38 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
return replyMixin;
|
||||
}
|
||||
|
||||
public static makeThread(
|
||||
parentEv: MatrixEvent,
|
||||
onHeightChanged: () => void,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
ref: React.RefObject<ReplyThread>,
|
||||
layout: Layout,
|
||||
alwaysShowTimestamps: boolean,
|
||||
): JSX.Element {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) return null;
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
onHeightChanged={onHeightChanged}
|
||||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
layout={layout}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
/>;
|
||||
public static hasThreadReply(event: MatrixEvent) {
|
||||
return Boolean(ReplyThread.getParentEventId(event));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialize();
|
||||
this.trySetExpandableQuotes();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.onHeightChanged();
|
||||
this.trySetExpandableQuotes();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private trySetExpandableQuotes() {
|
||||
if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
|
||||
const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
|
||||
if (el) {
|
||||
const code: HTMLElement | null = el.querySelector('code');
|
||||
const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
|
||||
const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
|
||||
if (isElipsisShown) {
|
||||
this.props.setQuoteExpanded(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
const { parentEv } = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
|
@ -321,7 +334,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
this.initialize();
|
||||
};
|
||||
|
||||
private onQuoteClick = async (): Promise<void> => {
|
||||
private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
|
||||
const events = [this.state.loadedEv, ...this.state.events];
|
||||
|
||||
let loadedEv = null;
|
||||
|
@ -369,18 +382,41 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
})
|
||||
}
|
||||
</blockquote>;
|
||||
} else if (this.props.forExport) {
|
||||
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
|
||||
header = <p className="mx_ReplyThread_Export">
|
||||
{ _t("In reply to <a>this message</a>",
|
||||
{},
|
||||
{ a: (sub) => (
|
||||
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
|
||||
),
|
||||
})
|
||||
}
|
||||
</p>;
|
||||
} else if (this.state.loading) {
|
||||
header = <Spinner w={16} h={16} />;
|
||||
}
|
||||
|
||||
const { isQuoteExpanded } = this.props;
|
||||
const evTiles = this.state.events.map((ev) => {
|
||||
return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}>
|
||||
<ReplyTile
|
||||
mxEvent={ev}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
</blockquote>;
|
||||
const classname = classNames({
|
||||
'mx_ReplyThread': true,
|
||||
[this.getReplyThreadColorClass(ev)]: true,
|
||||
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
|
||||
'mx_ReplyThread--expanded': isQuoteExpanded === true,
|
||||
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
|
||||
'mx_ReplyThread--collapsed': isQuoteExpanded === false,
|
||||
});
|
||||
return (
|
||||
<blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}>
|
||||
<ReplyTile
|
||||
mxEvent={ev}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
toggleExpandedQuote={() => this.props.setQuoteExpanded(!this.props.isQuoteExpanded)}
|
||||
/>
|
||||
</blockquote>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="mx_ReplyThread_wrapper">
|
||||
|
|
|
@ -35,12 +35,17 @@ function getDaysArray(): string[] {
|
|||
|
||||
interface IProps {
|
||||
ts: number;
|
||||
forExport?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DateSeparator")
|
||||
export default class DateSeparator extends React.Component<IProps> {
|
||||
private getLabel() {
|
||||
const date = new Date(this.props.ts);
|
||||
|
||||
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
|
||||
if (this.props.forExport) return formatFullDateNoTime(date);
|
||||
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const days = getDaysArray();
|
||||
|
|
|
@ -15,107 +15,112 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import { formatTime } from '../../../DateUtils';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import classNames from 'classnames';
|
||||
import RedactedBody from "./RedactedBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
|
||||
import ViewSource from "../../structures/ViewSource";
|
||||
|
||||
function getReplacedContent(event) {
|
||||
const originalContent = event.getOriginalContent();
|
||||
return originalContent["m.new_content"] || originalContent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.EditHistoryMessage")
|
||||
export default class EditHistoryMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
previousEdit: PropTypes.instanceOf(MatrixEvent),
|
||||
isBaseEvent: PropTypes.bool,
|
||||
};
|
||||
interface IProps {
|
||||
// the message event being edited
|
||||
mxEvent: MatrixEvent;
|
||||
previousEdit?: MatrixEvent;
|
||||
isBaseEvent?: boolean;
|
||||
isTwelveHour?: boolean;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
canRedact: boolean;
|
||||
sendStatus: EventStatus;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.EditHistoryMessage")
|
||||
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
|
||||
private content = createRef<HTMLDivElement>();
|
||||
private pills: Element[] = [];
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const { userId } = cli.credentials;
|
||||
const event = this.props.mxEvent;
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged);
|
||||
event.localRedactionEvent().on("status", this.onAssociatedStatusChanged);
|
||||
}
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
|
||||
this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
|
||||
|
||||
this._content = createRef();
|
||||
this._pills = [];
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
private onAssociatedStatusChanged = (): void => {
|
||||
this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() });
|
||||
};
|
||||
|
||||
_onRedactClick = async () => {
|
||||
private onRedactClick = async (): Promise<void> => {
|
||||
const event = this.props.mxEvent;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
|
||||
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
|
||||
redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
};
|
||||
|
||||
_onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
private onViewSourceClick = (): void => {
|
||||
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
};
|
||||
|
||||
pillifyLinks() {
|
||||
private pillifyLinks(): void {
|
||||
// not present for redacted events
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
|
||||
if (this.content.current) {
|
||||
pillifyLinks(this.content.current.children, this.props.mxEvent, this.pills);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unmountPills(this._pills);
|
||||
public componentWillUnmount(): void {
|
||||
unmountPills(this.pills);
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
event.localRedactionEvent().off("status", this.onAssociatedStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
public componentDidUpdate(): void {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
_renderActionBar() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
private renderActionBar(): JSX.Element {
|
||||
// hide the button when already redacted
|
||||
let redactButton;
|
||||
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<AccessibleButton onClick={this._onRedactClick}>
|
||||
<AccessibleButton onClick={this.onRedactClick}>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
const viewSourceButton = (
|
||||
<AccessibleButton onClick={this._onViewSourceClick}>
|
||||
<AccessibleButton onClick={this.onViewSourceClick}>
|
||||
{ _t("View Source") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -128,7 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { mxEvent } = this.props;
|
||||
const content = getReplacedContent(mxEvent);
|
||||
let contentContainer;
|
||||
|
@ -139,18 +144,22 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
if (this.props.previousEdit) {
|
||||
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
|
||||
} else {
|
||||
contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true });
|
||||
contentElements = HtmlUtils.bodyToHtml(
|
||||
content,
|
||||
null,
|
||||
{ stripReplyFallback: true, returnString: false },
|
||||
);
|
||||
}
|
||||
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
contentContainer = (
|
||||
<div className="mx_EventTile_content" ref={this._content}>*
|
||||
<div className="mx_EventTile_content" ref={this.content}>*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{ contentElements }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>;
|
||||
contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
<div className="mx_EventTile_line">
|
||||
<span className="mx_MessageTimestamp">{ timestamp }</span>
|
||||
{ contentContainer }
|
||||
{ this._renderActionBar() }
|
||||
{ this.renderActionBar() }
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -33,6 +33,7 @@ export interface IBodyProps {
|
|||
onHeightChanged: () => void;
|
||||
|
||||
showUrlPreview?: boolean;
|
||||
forExport?: boolean;
|
||||
tileShape: TileShape;
|
||||
maxImageHeight?: number;
|
||||
replacingEventId?: string;
|
||||
|
|
|
@ -90,6 +90,17 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.forExport) {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
const contentUrl = content.file?.url || content.url;
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<audio src={contentUrl} controls />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.playback) {
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
|
|
|
@ -123,6 +123,11 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
private getContentUrl(): string | null {
|
||||
if (this.props.forExport) return null;
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
private get content(): IMediaEventContent {
|
||||
return this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
}
|
||||
|
@ -149,11 +154,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private getContentUrl(): string {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
|
||||
this.props.onHeightChanged();
|
||||
|
@ -213,6 +213,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.forExport) {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
return <span className="mx_MFileBody">
|
||||
<a href={content.file?.url || content.url}>
|
||||
{ placeholder }
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
const showDownloadLink = this.props.tileShape || !this.props.showGenericPlaceholder;
|
||||
|
||||
if (isEncrypted) {
|
||||
|
|
|
@ -179,6 +179,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
};
|
||||
|
||||
protected getContentUrl(): string {
|
||||
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
if (this.props.forExport) return content.url || content.file?.url;
|
||||
if (this.media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
|
@ -372,7 +375,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
let placeholder = null;
|
||||
let gifLabel = null;
|
||||
|
||||
if (!this.state.imgLoaded) {
|
||||
if (!this.props.forExport && !this.state.imgLoaded) {
|
||||
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
|
@ -462,7 +465,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
// Overidden by MStickerBody
|
||||
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
return <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
|
||||
{ children }
|
||||
</a>;
|
||||
}
|
||||
|
@ -490,6 +493,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
// Overidden by MStickerBody
|
||||
protected getFileBody(): string | JSX.Element {
|
||||
if (this.props.forExport) return null;
|
||||
// We only ever need the download bar if we're appearing outside of the timeline
|
||||
if (this.props.tileShape) {
|
||||
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||
|
@ -510,7 +514,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
const contentUrl = this.getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this.isGif() && SettingsStore.getValue("autoplayGifs")) {
|
||||
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this.getThumbUrl();
|
||||
|
|
|
@ -16,44 +16,50 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { getNameForEventRoom, userLabelForEventRoom }
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationConclusion")
|
||||
export default class MKeyVerificationConclusion extends React.Component {
|
||||
constructor(props) {
|
||||
export default class MKeyVerificationConclusion extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
request.on("change", this.onRequestChanged);
|
||||
}
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this._onTrustChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onTrustChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
request.off("change", this.onRequestChanged);
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("userTrustStatusChanged", this._onTrustChanged);
|
||||
cli.removeListener("userTrustStatusChanged", this.onTrustChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestChanged = () => {
|
||||
private onRequestChanged = (): void => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_onTrustChanged = (userId, status) => {
|
||||
private onTrustChanged = (userId: string): void => {
|
||||
const { mxEvent } = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
if (!request || request.otherUserId !== userId) {
|
||||
|
@ -62,17 +68,17 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_shouldRender(mxEvent, request) {
|
||||
public static shouldRender(mxEvent: MatrixEvent, request: VerificationRequest): boolean {
|
||||
// normally should not happen
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
// .cancel event that was sent after the verification finished, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.cancel" && !request.cancelled) {
|
||||
if (mxEvent.getType() === EventType.KeyVerificationCancel && !request.cancelled) {
|
||||
return false;
|
||||
}
|
||||
// .done event that was sent after the verification cancelled, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.done" && !request.done) {
|
||||
if (mxEvent.getType() === EventType.KeyVerificationDone && !request.done) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -89,11 +95,11 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { mxEvent } = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
||||
if (!this._shouldRender(mxEvent, request)) {
|
||||
if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -103,15 +109,18 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
let title;
|
||||
|
||||
if (request.done) {
|
||||
title = _t("You verified %(name)s", { name: getNameForEventRoom(request.otherUserId, mxEvent) });
|
||||
title = _t(
|
||||
"You verified %(name)s",
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) },
|
||||
);
|
||||
} else if (request.cancelled) {
|
||||
const userId = request.cancellingUserId;
|
||||
if (userId === myUserId) {
|
||||
title = _t("You cancelled verifying %(name)s",
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent) });
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) });
|
||||
} else {
|
||||
title = _t("%(name)s cancelled verifying",
|
||||
{ name: getNameForEventRoom(userId, mxEvent) });
|
||||
{ name: getNameForEventRoom(userId, mxEvent.getRoomId()) });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,8 +138,3 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MKeyVerificationConclusion.propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
|
@ -79,7 +79,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
}
|
||||
|
||||
private getContentUrl(): string|null {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
if (this.props.forExport) return content.file?.url || content.url;
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
|
@ -93,6 +96,9 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
}
|
||||
|
||||
private getThumbUrl(): string|null {
|
||||
// there's no need of thumbnail when the content is local
|
||||
if (this.props.forExport) return null;
|
||||
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
const media = mediaFromContent(content);
|
||||
|
||||
|
@ -209,6 +215,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
private getFileBody = () => {
|
||||
if (this.props.forExport) return null;
|
||||
return this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const autoplay = SettingsStore.getValue("autoplayVideo");
|
||||
|
@ -222,8 +233,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
// Important: If we aren't autoplaying and we haven't decrypred it yet, show a video with a poster.
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
|
@ -254,6 +265,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
preload = "none";
|
||||
}
|
||||
}
|
||||
|
||||
const fileBody = this.getFileBody();
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<video
|
||||
|
@ -270,7 +283,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
poster={poster}
|
||||
onPlay={this.videoOnPlay}
|
||||
/>
|
||||
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
|
||||
{ fileBody }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { isVoiceMessage } from "../../../utils/EventUtils";
|
|||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
|
||||
public render() {
|
||||
if (isVoiceMessage(this.props.mxEvent)) {
|
||||
if (!this.props.forExport && isVoiceMessage(this.props.mxEvent)) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
} else {
|
||||
return <MAudioBody {...this.props} />;
|
||||
|
|
|
@ -17,7 +17,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import type { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -26,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
|||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -35,13 +36,17 @@ import Resend from "../../../Resend";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import DownloadActionButton from "./DownloadActionButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import ReplyThread from '../elements/ReplyThread';
|
||||
|
||||
interface IOptionsButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
|
||||
// TODO: Types
|
||||
getTile: () => any | null;
|
||||
getReplyThread: () => ReplyThread;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onFocusChange: (menuDisplayed: boolean) => void;
|
||||
|
@ -57,8 +62,6 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
|
@ -90,7 +93,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
|
||||
interface IReactButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions: any; // TODO: types
|
||||
reactions: Relations;
|
||||
onFocusChange: (menuDisplayed: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -127,12 +130,14 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
|
||||
interface IMessageActionBarProps {
|
||||
mxEvent: MatrixEvent;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: any; // TODO: types
|
||||
reactions?: Relations;
|
||||
// TODO: Types
|
||||
getTile: () => any | null;
|
||||
getReplyThread: () => ReplyThread | undefined;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
|
||||
getReplyThread?: () => ReplyThread;
|
||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||
toggleThreadExpanded: () => void;
|
||||
isQuoteExpanded?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MessageActionBar")
|
||||
|
@ -202,8 +207,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
|
||||
private onEditClick = (ev: React.MouseEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -283,7 +289,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
// Like the resend button, the react and reply buttons need to appear before the edit.
|
||||
// The only catch is we do the reply button first so that we can make sure the react
|
||||
// button is the very first button without having to do length checks for `splice()`.
|
||||
if (this.context.canReply) {
|
||||
if (this.context.canReply && this.context.timelineRenderingType === TimelineRenderingType.Room) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
@ -324,6 +330,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
toolbarOpts.push(cancelSendingButton);
|
||||
}
|
||||
|
||||
if (this.props.isQuoteExpanded !== undefined && ReplyThread.hasThreadReply(this.props.mxEvent)) {
|
||||
const expandClassName = classNames({
|
||||
'mx_MessageActionBar_maskButton': true,
|
||||
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
|
||||
'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded,
|
||||
});
|
||||
toolbarOpts.push(<RovingAccessibleTooltipButton
|
||||
className={expandClassName}
|
||||
title={this.props.isQuoteExpanded ? _t("Collapse quotes │ ⇧+click") : _t("Expand quotes │ ⇧+click")}
|
||||
onClick={this.props.toggleThreadExpanded}
|
||||
key="expand"
|
||||
/>);
|
||||
}
|
||||
|
||||
// The menu button should be last, so dump it there.
|
||||
toolbarOpts.push(<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
|
|
@ -136,6 +136,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
forExport={this.props.forExport}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
|
|
|
@ -15,22 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onMessageAllowed: () => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MjolnirBody")
|
||||
export default class MjolnirBody extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onMessageAllowed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
_onAllowClick = (e) => {
|
||||
export default class MjolnirBody extends React.Component<IProps> {
|
||||
private onAllowClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -39,11 +35,11 @@ export default class MjolnirBody extends React.Component {
|
|||
this.props.onMessageAllowed();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className='mx_MjolnirBody'><i>{ _t(
|
||||
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
|
||||
{}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> },
|
||||
{}, { a: (sub) => <a href="#" onClick={this.onAllowClick}>{ sub }</a> },
|
||||
) }</i></div>
|
||||
);
|
||||
}
|
|
@ -16,15 +16,19 @@ limitations under the License.
|
|||
|
||||
import React, { useContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
|
||||
const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
|
||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||
let text = _t("Message deleted");
|
||||
const unsigned = mxEvent.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
|
||||
|
|
|
@ -17,23 +17,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import ImageView from "../elements/ImageView";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.RoomAvatarEvent")
|
||||
export default class RoomAvatarEvent extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
onAvatarClick = () => {
|
||||
export default class RoomAvatarEvent extends React.Component<IProps> {
|
||||
private onAvatarClick = (): void => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
|
||||
|
@ -44,7 +45,6 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
roomName: room ? room.name : '',
|
||||
});
|
||||
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: text,
|
||||
|
@ -52,10 +52,9 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const ev = this.props.mxEvent;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
|
||||
return (
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
|
@ -24,15 +23,16 @@ import { _t } from '../../../languageHandler';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.RoomCreate")
|
||||
export default class RoomCreate extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
_onLinkClicked = e => {
|
||||
export default class RoomCreate extends React.Component<IProps> {
|
||||
private onLinkClicked = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
|
@ -45,7 +45,7 @@ export default class RoomCreate extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instantiated in this case
|
||||
|
@ -55,7 +55,7 @@ export default class RoomCreate extends React.Component {
|
|||
permalinkCreator.load();
|
||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||
const link = (
|
||||
<a href={predecessorPermalink} onClick={this._onLinkClicked}>
|
||||
<a href={predecessorPermalink} onClick={this.onLinkClicked}>
|
||||
{ _t("Click here to see older messages.") }
|
||||
</a>
|
||||
);
|
|
@ -138,6 +138,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
// If it's less than 30% we don't add the expansion button.
|
||||
// We also round the number as it sometimes can be 29.99...
|
||||
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||
// TODO: additionally show the button if it's an expanded quoted message
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -47,6 +47,7 @@ import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
|||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -240,6 +241,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onRoomExportClick = async () => {
|
||||
Modal.createTrackedDialog('export room dialog', '', ExportDialog, {
|
||||
room,
|
||||
});
|
||||
};
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
|
@ -280,6 +287,9 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ _t("Show files") }
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ _t("Export chat") }
|
||||
</Button>
|
||||
{ SettingsStore.getValue("feature_thread") && (
|
||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
||||
{ _t("Show threads") }
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
|
@ -35,16 +34,6 @@ interface IProps {
|
|||
room: Room;
|
||||
userId: string;
|
||||
showApps: boolean; // Render apps
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number;
|
||||
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: () => void;
|
||||
fullHeight: boolean;
|
||||
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
|
@ -92,13 +81,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// most changes are likely to cause a resize
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
private rateLimitedUpdate = throttle(() => {
|
||||
this.setState({ counters: this.computeCounters() });
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
@ -138,7 +120,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
const callView = (
|
||||
<CallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
maxVideoHeight={this.props.maxHeight}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
|
@ -148,7 +129,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
appsDrawer = <AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
|
@ -204,21 +184,12 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_RoomView_auxPanel": true,
|
||||
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
|
||||
});
|
||||
const style: React.CSSProperties = {};
|
||||
if (!this.props.fullHeight) {
|
||||
style.maxHeight = this.props.maxHeight;
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHideScrollbar className={classes} style={style}>
|
||||
<AutoHideScrollbar className="mx_RoomView_auxPanel">
|
||||
{ stateViews }
|
||||
{ this.props.children }
|
||||
{ appsDrawer }
|
||||
{ callView }
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
|
|||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
@ -36,15 +35,18 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -65,7 +67,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
|||
return "";
|
||||
}
|
||||
|
||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
||||
function createEditContent(
|
||||
model: EditorModel,
|
||||
editedEvent: MatrixEvent,
|
||||
renderingContext?: TimelineRenderingType,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
|
@ -98,41 +104,49 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
|||
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
const relation = {
|
||||
"m.new_content": newContent,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": editedEvent.getId(),
|
||||
},
|
||||
}, contentBody);
|
||||
};
|
||||
|
||||
if (renderingContext === TimelineRenderingType.Thread) {
|
||||
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
|
||||
}
|
||||
|
||||
return Object.assign(relation, contentBody);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IEditMessageComposerProps extends MatrixClientProps {
|
||||
editState: EditorStateTransfer;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
saveDisabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private readonly dispatcherRef: string;
|
||||
private model: EditorModel = null;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
const isRestored = this.createEditorModel();
|
||||
const ev = this.props.editState.getEvent();
|
||||
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, ev, renderingContext);
|
||||
this.state = {
|
||||
saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]),
|
||||
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
|
@ -140,7 +154,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private getRoom(): Room {
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -161,10 +175,17 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
const previousEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: false,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (previousEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: previousEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
@ -173,12 +194,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
|
||||
const nextEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: true,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (nextEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: nextEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
} else {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: 'edit_event', event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -188,16 +221,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}`;
|
||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
||||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
private get events(): MatrixEvent[] {
|
||||
const liveTimelineEvents = this.context.liveTimeline.getEvents();
|
||||
const pendingEvents = this.getRoom().getPendingEvents();
|
||||
const isInThread = Boolean(this.props.editState.getEvent().getThread());
|
||||
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
||||
}
|
||||
|
||||
private cancelEdit = (): void => {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -325,12 +369,20 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, editedEvent, renderingContext);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
if (newContent?.body === '') {
|
||||
this.cancelPreviousPendingEdit();
|
||||
createRedactEventDialog({
|
||||
mxEvent: editedEvent,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (this.isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
|
@ -372,7 +424,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
if (shouldSend) {
|
||||
this.cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
|
@ -380,7 +432,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -391,7 +447,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
this.props.mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,7 +475,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private createEditorModel(): boolean {
|
||||
const { editState } = this.props;
|
||||
const room = this.getRoom();
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||
|
||||
let parts;
|
||||
let isRestored = false;
|
||||
|
@ -484,3 +540,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||
export default EditMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow';
|
|||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -144,8 +145,7 @@ export function getHandlerTile(ev) {
|
|||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -192,6 +192,7 @@ export enum TileShape {
|
|||
Notif = "notif",
|
||||
FileGrid = "file_grid",
|
||||
Pinned = "pinned",
|
||||
Thread = "thread",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -263,6 +264,8 @@ interface IProps {
|
|||
// for now.
|
||||
tileShape?: TileShape;
|
||||
|
||||
forExport?: boolean;
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour?: boolean;
|
||||
|
||||
|
@ -322,7 +325,7 @@ interface IState {
|
|||
reactions: Relations;
|
||||
|
||||
hover: boolean;
|
||||
|
||||
isQuoteExpanded?: boolean;
|
||||
thread?: Thread;
|
||||
}
|
||||
|
||||
|
@ -330,7 +333,8 @@ interface IState {
|
|||
export default class EventTile extends React.Component<IProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
private isListeningForReceipts: boolean;
|
||||
private tile = React.createRef();
|
||||
// TODO: Types
|
||||
private tile = React.createRef<unknown>();
|
||||
private replyThread = React.createRef<ReplyThread>();
|
||||
|
||||
public readonly ref = createRef<HTMLElement>();
|
||||
|
@ -338,6 +342,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
static defaultProps = {
|
||||
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
||||
onHeightChanged: function() {},
|
||||
forExport: false,
|
||||
layout: Layout.Group,
|
||||
};
|
||||
|
||||
|
@ -380,7 +385,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
* or 'sent' receipt, for example.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private get isEligibleForSpecialReceipt() {
|
||||
private get isEligibleForSpecialReceipt(): boolean {
|
||||
// First, if there are other read receipts then just short-circuit this.
|
||||
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
|
||||
if (!this.props.mxEvent) return false;
|
||||
|
@ -451,16 +456,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
componentDidMount() {
|
||||
this.suppressReadReceiptAnimation = false;
|
||||
const client = this.context;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
|
||||
}
|
||||
if (!this.props.forExport) {
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
|
||||
}
|
||||
|
||||
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
||||
client.on("Room.receipt", this.onRoomReceipt);
|
||||
this.isListeningForReceipts = true;
|
||||
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
||||
client.on("Room.receipt", this.onRoomReceipt);
|
||||
this.isListeningForReceipts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
|
@ -696,6 +703,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
shouldHighlight() {
|
||||
if (this.props.forExport) return false;
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
|
@ -888,8 +896,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
actionBarFocused: focused,
|
||||
});
|
||||
};
|
||||
|
||||
getTile = () => this.tile.current;
|
||||
// TODO: Types
|
||||
getTile: () => any | null = () => this.tile.current;
|
||||
|
||||
getReplyThread = () => this.replyThread.current;
|
||||
|
||||
|
@ -914,6 +922,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private setQuoteExpanded = (expanded: boolean) => {
|
||||
this.setState({
|
||||
isQuoteExpanded: expanded,
|
||||
});
|
||||
};
|
||||
render() {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const eventType = this.props.mxEvent.getType() as EventType;
|
||||
|
@ -923,6 +936,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
isInfoMessage,
|
||||
isLeftAlignedBubbleMessage,
|
||||
} = getEventDisplayInfo(this.props.mxEvent);
|
||||
const { isQuoteExpanded } = this.state;
|
||||
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
|
@ -935,6 +949,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const EventTileType = sdk.getComponent(tileHandler);
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
|
@ -1047,13 +1062,16 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const actionBar = !isEditing ? <MessageActionBar
|
||||
const showMessageActionBar = !isEditing && !this.props.forExport;
|
||||
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||
/> : undefined;
|
||||
|
||||
const showTimestamp = this.props.mxEvent.getTs()
|
||||
|
@ -1156,10 +1174,47 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
case TileShape.Thread: {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
/>
|
||||
{ actionBar }
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
case TileShape.FileGrid: {
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
|
@ -1175,6 +1230,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
|
@ -1192,20 +1248,19 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
default: {
|
||||
let thread;
|
||||
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||
// avoid showing replies for hidden events (events without tiles)
|
||||
if (haveTileForEvent(this.props.mxEvent)) {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
this.props.layout,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
|
||||
const thread = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyThread.hasThreadReply(this.props.mxEvent) ? (
|
||||
<ReplyThread
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyThread}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
/>) : null;
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
|
@ -1233,6 +1288,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
forExport={this.props.forExport}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
highlights={this.props.highlights}
|
||||
|
@ -1258,7 +1314,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||
function isMessageEvent(ev) {
|
||||
function isMessageEvent(ev: MatrixEvent): boolean {
|
||||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||
import { IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LinkPreviewWidget from "./LinkPreviewWidget";
|
||||
|
@ -40,13 +40,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
|
|||
|
||||
const ts = mxEvent.getTs();
|
||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
|
||||
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
|
||||
try {
|
||||
return [link, await cli.getUrlPreview(link, ts)];
|
||||
} catch (error) {
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
}
|
||||
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
|
||||
return fetchPreviews(cli, links, ts);
|
||||
}, [links, ts], []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -89,4 +83,18 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
|
|||
</div>;
|
||||
};
|
||||
|
||||
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
|
||||
Promise<[string, IPreviewUrlResponse][]> => {
|
||||
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
|
||||
try {
|
||||
const preview = await cli.getUrlPreview(link, ts);
|
||||
if (preview && Object.keys(preview).length > 0) {
|
||||
return [link, preview];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
}
|
||||
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
|
||||
};
|
||||
|
||||
export default LinkPreviewGroup;
|
||||
|
|
|
@ -45,7 +45,7 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
|||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import SendMessageComposer from "./SendMessageComposer";
|
||||
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
|
@ -219,8 +219,8 @@ interface IState {
|
|||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
|
@ -378,14 +378,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton) {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
await this.voiceRecordingButton.send();
|
||||
await this.voiceRecordingButton.current?.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageComposerInput.sendMessage();
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel) => {
|
||||
|
@ -460,7 +460,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
|
@ -521,7 +521,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
|
@ -535,7 +535,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room} />);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
|
|
@ -35,6 +35,7 @@ interface IProps {
|
|||
highlights?: string[];
|
||||
highlightLink?: string;
|
||||
onHeightChanged?(): void;
|
||||
toggleExpandedQuote?: () => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.ReplyTile")
|
||||
|
@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
// Expand thread on shift key
|
||||
if (this.props.toggleExpandedQuote && e.shiftKey) {
|
||||
this.props.toggleExpandedQuote();
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ import InviteReason from "../elements/InviteReason";
|
|||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
|
@ -339,8 +341,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
title = _t("Join the conversation with an account");
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
}
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
if (this.props.previewLoading) {
|
||||
|
|
|
@ -29,10 +29,9 @@ import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextM
|
|||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { Volume } from "../../../RoomNotifsTypes";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import RoomListActions from "../../../actions/RoomListActions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
@ -364,7 +363,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
};
|
||||
|
||||
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||
private async saveNotifState(ev: ButtonEvent, newState: RoomNotifState) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
@ -378,10 +377,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
||||
private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD);
|
||||
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
|
||||
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
||||
private onClickAllNotifs = ev => this.saveNotifState(ev, RoomNotifState.AllMessages);
|
||||
private onClickAlertMe = ev => this.saveNotifState(ev, RoomNotifState.AllMessagesLoud);
|
||||
private onClickMentions = ev => this.saveNotifState(ev, RoomNotifState.MentionsOnly);
|
||||
private onClickMute = ev => this.saveNotifState(ev, RoomNotifState.Mute);
|
||||
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||
|
@ -404,25 +403,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Use default")}
|
||||
active={state === ALL_MESSAGES}
|
||||
active={state === RoomNotifState.AllMessages}
|
||||
iconClassName="mx_RoomTile_iconBell"
|
||||
onClick={this.onClickAllNotifs}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("All messages")}
|
||||
active={state === ALL_MESSAGES_LOUD}
|
||||
active={state === RoomNotifState.AllMessagesLoud}
|
||||
iconClassName="mx_RoomTile_iconBellDot"
|
||||
onClick={this.onClickAlertMe}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Mentions & Keywords")}
|
||||
active={state === MENTIONS_ONLY}
|
||||
active={state === RoomNotifState.MentionsOnly}
|
||||
iconClassName="mx_RoomTile_iconBellMentions"
|
||||
onClick={this.onClickMentions}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("None")}
|
||||
active={state === MUTE}
|
||||
active={state === RoomNotifState.Mute}
|
||||
iconClassName="mx_RoomTile_iconBellCrossed"
|
||||
onClick={this.onClickMute}
|
||||
/>
|
||||
|
@ -432,14 +431,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const classes = classNames("mx_RoomTile_notificationsButton", {
|
||||
// Show bell icon for the default case too.
|
||||
mx_RoomTile_iconBell: state === ALL_MESSAGES,
|
||||
mx_RoomTile_iconBellDot: state === ALL_MESSAGES_LOUD,
|
||||
mx_RoomTile_iconBellMentions: state === MENTIONS_ONLY,
|
||||
mx_RoomTile_iconBellCrossed: state === MUTE,
|
||||
mx_RoomTile_iconBell: state === RoomNotifState.AllMessages,
|
||||
mx_RoomTile_iconBellDot: state === RoomNotifState.AllMessagesLoud,
|
||||
mx_RoomTile_iconBellMentions: state === RoomNotifState.MentionsOnly,
|
||||
mx_RoomTile_iconBellCrossed: state === RoomNotifState.Mute,
|
||||
|
||||
// Only show the icon by default if the room is overridden to muted.
|
||||
// TODO: [FTUE Notifications] Probably need to detect global mute state
|
||||
mx_RoomTile_notificationsButton_show: state === MUTE,
|
||||
mx_RoomTile_notificationsButton_show: state === RoomNotifState.Mute,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,6 +19,7 @@ import EMOJI_REGEX from 'emojibase-regex';
|
|||
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { DebouncedFunc, throttle } from 'lodash';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
|
@ -40,7 +41,7 @@ import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
|||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { containsEmoji } from "../../../effects/utils";
|
||||
import { CHAT_EFFECTS } from '../../../effects';
|
||||
|
@ -55,8 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -130,7 +130,7 @@ export function isQuickReaction(model: EditorModel): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface ISendMessageComposerProps extends MatrixClientProps {
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
|
@ -141,10 +141,8 @@ interface IProps {
|
|||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||
export default class SendMessageComposer extends React.Component<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
|
||||
static contextType = RoomContext;
|
||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private model: EditorModel = null;
|
||||
|
@ -152,26 +150,25 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
private dispatcherRef: string;
|
||||
private sendHistoryManager: SendHistoryManager;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
|
||||
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this.prepareToEncrypt = throttle(() => {
|
||||
this.context.prepareToEncrypt(this.props.room);
|
||||
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||
}, 60000, { leading: true, trailing: false });
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
|
||||
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
|
||||
if (replyToEventChanged) {
|
||||
this.model.reset([]);
|
||||
}
|
||||
|
||||
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model.reset(parts);
|
||||
this.editorRef.current?.focus();
|
||||
|
@ -202,13 +199,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
case MessageComposerAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
const events =
|
||||
this.context.liveTimeline.getEvents()
|
||||
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
|
||||
const editEvent = findEditableEvent({
|
||||
events,
|
||||
isForward: false,
|
||||
});
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: editEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +279,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.props.room.getLiveTimeline();
|
||||
const timeline = this.context.liveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
|
@ -448,7 +452,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -465,7 +469,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(this.context, roomId, resp.event_id);
|
||||
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
|
@ -490,7 +494,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -577,7 +581,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
// it puts the filename in as text/plain which we want to ignore.
|
||||
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.props.mxClient,
|
||||
);
|
||||
return true; // to skip internal onPaste handler
|
||||
}
|
||||
|
@ -608,3 +612,6 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SendMessageComposerWithMatrixClient = withMatrixClientHOC(SendMessageComposer);
|
||||
export default SendMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -97,7 +97,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
|||
if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) {
|
||||
let upgradeRequiredPill;
|
||||
if (preferredRestrictionVersion) {
|
||||
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
|
||||
upgradeRequiredPill = <span className="mx_JoinRuleSettings_upgradeRequired">
|
||||
{ _t("Upgrade required") }
|
||||
</span>;
|
||||
}
|
||||
|
@ -159,13 +159,14 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
|||
disabled={disabled}
|
||||
onClick={onEditRestrictedClick}
|
||||
kind="link"
|
||||
className="mx_JoinRuleSettings_linkButton"
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
</span>
|
||||
|
||||
<div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
|
||||
<div className="mx_JoinRuleSettings_spacesWithAccess">
|
||||
<h4>{ _t("Spaces with access") }</h4>
|
||||
{ shownSpaces.map(room => {
|
||||
return <span key={room.roomId}>
|
||||
|
@ -286,6 +287,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
|||
onChange={onChange}
|
||||
definitions={definitions}
|
||||
disabled={disabled}
|
||||
className="mx_JoinRuleSettings_radioButton"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,9 +27,6 @@ interface IProps {
|
|||
// What room we should display the call for
|
||||
roomId: string;
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
|
@ -99,14 +96,12 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
|
||||
public render() {
|
||||
if (!this.state.call) return null;
|
||||
// We subtract 8 as it the margin-bottom of the mx_CallViewForRoom_ResizeWrapper
|
||||
const maxHeight = this.props.maxVideoHeight - 8;
|
||||
|
||||
return (
|
||||
<div className="mx_CallViewForRoom">
|
||||
<Resizable
|
||||
minHeight={380}
|
||||
maxHeight={maxHeight}
|
||||
maxHeight="80vh"
|
||||
enable={{
|
||||
top: false,
|
||||
right: false,
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 { createContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||
MatrixClientContext.displayName = "MatrixClientContext";
|
||||
export default MatrixClientContext;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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, { ComponentClass, createContext, forwardRef, useContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||
MatrixClientContext.displayName = "MatrixClientContext";
|
||||
export default MatrixClientContext;
|
||||
|
||||
export interface MatrixClientProps {
|
||||
mxClient: MatrixClient;
|
||||
}
|
||||
|
||||
const matrixHOC = <ComposedComponentProps extends {}>(
|
||||
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
||||
) => {
|
||||
type ComposedComponentInstance = InstanceType<typeof ComposedComponent>;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
|
||||
const TypedComponent = ComposedComponent;
|
||||
|
||||
return forwardRef<ComposedComponentInstance, Omit<ComposedComponentProps, 'mxClient'>>(
|
||||
(props, ref) => {
|
||||
const client = useContext(MatrixClientContext);
|
||||
|
||||
// @ts-ignore
|
||||
return <TypedComponent ref={ref} {...props} mxClient={client} />;
|
||||
},
|
||||
);
|
||||
};
|
||||
export const withMatrixClientHOC = matrixHOC;
|
|
@ -16,10 +16,15 @@ limitations under the License.
|
|||
|
||||
import { createContext } from "react";
|
||||
|
||||
import { IState } from "../components/structures/RoomView";
|
||||
import { IRoomState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/Layout";
|
||||
|
||||
const RoomContext = createContext<IState>({
|
||||
export enum TimelineRenderingType {
|
||||
Room,
|
||||
Thread
|
||||
}
|
||||
|
||||
const RoomContext = createContext<IRoomState>({
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
|
@ -53,6 +58,8 @@ const RoomContext = createContext<IState>({
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: false,
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
|
|
|
@ -128,7 +128,7 @@ export enum Action {
|
|||
* Start a call transfer to a phone number
|
||||
* payload: TransferCallPayload
|
||||
*/
|
||||
TransferCallToPhoneNumber = "transfer_call_to_phone_number",
|
||||
TransferCallToPhoneNumber = "transfer_call_to_phone_number",
|
||||
|
||||
/**
|
||||
* Fired when CallHandler has checked for PSTN protocol support
|
||||
|
@ -205,4 +205,9 @@ export enum Action {
|
|||
* Should be used with SettingUpdatedPayload.
|
||||
*/
|
||||
SettingUpdated = "setting_updated",
|
||||
|
||||
/**
|
||||
* Fires when a user starts to edit event (e.g. up arrow in compositor)
|
||||
*/
|
||||
EditEvent = "edit_event",
|
||||
}
|
||||
|
|
|
@ -185,7 +185,7 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t
|
|||
const firstPart = model.parts[0];
|
||||
// part type will be "plain" while editing,
|
||||
// and "command" while composing a message.
|
||||
let text = firstPart && firstPart.text;
|
||||
let text = firstPart?.text || '';
|
||||
if (!caseSensitive) {
|
||||
prefix = prefix.toLowerCase();
|
||||
text = text.toLowerCase();
|
||||
|
|
|
@ -2901,7 +2901,7 @@
|
|||
"e.g. my-space": "např. můj-prostor",
|
||||
"Silence call": "Ztlumit zvonění",
|
||||
"Sound on": "Zvuk zapnutý",
|
||||
"Show all rooms in Home": "Zobrazit všechny místnosti na domácí obrazovce",
|
||||
"Show all rooms in Home": "Zobrazit všechny místnosti na úvodní obrazovce",
|
||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Prototyp Nahlášování moderátorům. V místnostech, které podporují moderování, vám tlačítko `nahlásit` umožní nahlásit zneužití moderátorům místnosti",
|
||||
"%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s změnil(a) <a>připnuté zprávy</a> v místnosti.",
|
||||
"%(senderName)s kicked %(targetName)s": "%(senderName)s vykopl(a) uživatele %(targetName)s",
|
||||
|
@ -3056,7 +3056,7 @@
|
|||
"Want to add a new space instead?": "Chcete místo toho přidat nový prostor?",
|
||||
"Decrypting": "Dešifrování",
|
||||
"Show all rooms": "Zobrazit všechny místnosti",
|
||||
"All rooms you're in will appear in Home.": "Všechny místnosti, ve kterých se nacházíte, se zobrazí na domovské obrazovce.",
|
||||
"All rooms you're in will appear in Home.": "Všechny místnosti, ve kterých se nacházíte, se zobrazí na úvodní obrazovce.",
|
||||
"Send pseudonymous analytics data": "Odeslat pseudonymní analytická data",
|
||||
"Missed call": "Zmeškaný hovor",
|
||||
"Call declined": "Hovor odmítnut",
|
||||
|
@ -3158,9 +3158,51 @@
|
|||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Tato místnost se nachází v některých prostorech, jejichž nejste správcem. V těchto prostorech bude stará místnost stále zobrazena, ale lidé budou vyzváni, aby se připojili k nové místnosti.",
|
||||
"Before you upgrade": "Než provedete aktualizaci",
|
||||
"To join a space you'll need an invite.": "Pro připojení k prostoru potřebujete pozvánku.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Můžete také vytvořit prostory ze <a>skupin</a>.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Prostory můžete vytvořit také ze <a>skupin</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Dočasně zobrazit skupiny místo prostorů pro tuto relaci. Podpora bude v blízké budoucnosti odstraněna. Toto provede přenačtení Elementu.",
|
||||
"Display Communities instead of Spaces": "Zobrazit skupiny místo prostorů",
|
||||
"Joining space …": "Připojování k prostoru…",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagoval(a) na %(content)s"
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagoval(a) na %(content)s",
|
||||
"Would you like to leave the rooms in this space?": "Chcete odejít z místností v tomto prostoru?",
|
||||
"You are about to leave <spaceName/>.": "Odcházíte z <spaceName/>.",
|
||||
"Leave some rooms": "Odejít z některých místností",
|
||||
"Don't leave any rooms": "Neodcházet z žádné místnosti",
|
||||
"Leave all rooms": "Odejít ze všech místností",
|
||||
"Expand quotes │ ⇧+click": "Rozbalit uvozovky │ ⇧+kliknutí",
|
||||
"Collapse quotes │ ⇧+click": "Sbalit uvozovky │ ⇧+kliknutí",
|
||||
"Include Attachments": "Zahrnout přílohy",
|
||||
"Size Limit": "Omezení velikosti",
|
||||
"Format": "Formát",
|
||||
"Select from the options below to export chats from your timeline": "Vyberte jednu z níže uvedených možností pro export chatů z časové osy",
|
||||
"Export Chat": "Exportovat chat",
|
||||
"Exporting your data": "Exportování dat",
|
||||
"Stop": "Zastavit",
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Opravdu chcete ukončit export dat? Pokud ano, budete muset začít znovu.",
|
||||
"Your export was successful. Find it in your Downloads folder.": "Váš export proběhl úspěšně. Najdete jej ve složce pro stažené soubory.",
|
||||
"The export was cancelled successfully": "Export byl úspěšně zrušen",
|
||||
"Export Successful": "Export proběhl úspěšně",
|
||||
"MB": "MB",
|
||||
"Number of messages": "Počet zpráv",
|
||||
"Number of messages can only be a number between %(min)s and %(max)s": "Počet zpráv může být pouze číslo mezi %(min)s a %(max)s",
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB": "Velikost může být pouze číslo mezi %(min)s MB a %(max)s MB",
|
||||
"Enter a number between %(min)s and %(max)s": "Zadejte číslo mezi %(min)s a %(max)s",
|
||||
"In reply to <a>this message</a>": "V odpovědi na <a>tuto zprávu</a>",
|
||||
"Export chat": "Exportovat chat",
|
||||
"File Attached": "Přiložený soubor",
|
||||
"Error fetching file": "Chyba při načítání souboru",
|
||||
"Topic: %(topic)s": "Téma: %(topic)s",
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "Toto je začátek exportu <roomName/>. Exportováno pomocí <exporterDetails/> v %(exportDate)s.",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s vytvořil(a) tuto místnost.",
|
||||
"Media omitted - file size limit exceeded": "Vynechaná média - překročen limit velikosti souboru",
|
||||
"Media omitted": "Vynechaná média",
|
||||
"Current Timeline": "Aktuální časová osa",
|
||||
"Specify a number of messages": "Zadejte počet zpráv",
|
||||
"From the beginning": "Od začátku",
|
||||
"Plain Text": "Prostý text",
|
||||
"JSON": "JSON",
|
||||
"HTML": "HTML",
|
||||
"Are you sure you want to exit during this export?": "Opravdu chcete skončit během tohoto exportu?",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s poslal(a) nálepku.",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s změnil(a) avatar místnosti.",
|
||||
"%(date)s at %(time)s": "%(date)s v %(time)s"
|
||||
}
|
||||
|
|
|
@ -3134,5 +3134,26 @@
|
|||
"Joining space …": "Space beitreten…",
|
||||
"To join a space you'll need an invite.": "Um einem Space beizutreten brauchst du eine Einladung.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "In dieser Sitzung temporär Communities statt Spaces anzeigen. Unterstützung hierfür wird in naher Zukunft entfernt. Dies wird Element neu laden.",
|
||||
"Display Communities instead of Spaces": "Communities statt Spaces anzeigen"
|
||||
"Display Communities instead of Spaces": "Communities statt Spaces anzeigen",
|
||||
"To join this Space, hide communities in your <a>preferences</a>": "<a>Deaktiviere Communities in den Einstellungen</a>, um diesen Space beizutreten.",
|
||||
"To view this Space, hide communities in your <a>preferences</a>": "<a>Deaktiviere Communities in den Einstellungen</a>, um diesen Space anzuzeigen.",
|
||||
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "Um %(communityName)s beizutreten, <a>aktiviere Communities in den Einstellungen</a>",
|
||||
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "Um %(communityName)s anzuzeigen, <a>aktiviere Communities in den Einstellungen</a>",
|
||||
"Private community": "Private Community",
|
||||
"Public community": "Öffentliche Community",
|
||||
"You are about to leave <spaceName/>.": "Du bist dabei, <spaceName/> zu verlassen.",
|
||||
"Leave some rooms": "Zu verlassende Räume auswählen",
|
||||
"Leave all rooms": "Alle Räume verlassen",
|
||||
"Don't leave any rooms": "Räume nicht verlassen",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s hat mit %(content)s reagiert",
|
||||
"Some encryption parameters have been changed.": "Einige Verschlüsselungsoptionen wurden geändert.",
|
||||
"Message": "Nachricht",
|
||||
"Message didn't send. Click for info.": "Nachricht nicht gesendet. Klicke für Details.",
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "<a>Erstelle einen neuen Raum für deine Konversation</a>, um diese Probleme zu umgehen.",
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Es ist nicht sinnvoll, verschlüsselte Räume öffentlich zu machen.</b> Da jeder den Raum betreten kann, kann auch jeder Nachrichten lesen, was die Verschlüsselung sinnlos macht. Außerdem wird das Senden und Empfangen von Nachrichten langsamer werden.",
|
||||
"Select the roles required to change various parts of the space": "Wähle, von wem folgende Aktionen ausgeführt werden können",
|
||||
"Upgrade anyway": "Trotzdem upgraden",
|
||||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Dieser Raum ist in einigen Spaces, in denen du nicht Admin bist. Daher wird dort noch der alte Raum angezeigt, die Leute werden aber auf den neuen Raum hingewiesen.",
|
||||
"Before you upgrade": "Bevor du upgradest",
|
||||
"You can also make Spaces from <a>communities</a>.": "Du kannst Spaces auch aus <a>Communities</a> erstellen."
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
|
||||
"%(date)s at %(time)s": "%(date)s at %(time)s",
|
||||
"Who would you like to add to this community?": "Who would you like to add to this community?",
|
||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
|
||||
"Invite new community members": "Invite new community members",
|
||||
|
@ -509,6 +510,7 @@
|
|||
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s",
|
||||
"%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s",
|
||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s changed the room avatar.",
|
||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
||||
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
|
||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.",
|
||||
|
@ -525,7 +527,10 @@
|
|||
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s set the server ACLs for this room.",
|
||||
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s changed the server ACLs for this room.",
|
||||
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 All servers are banned from participating! This room can no longer be used.",
|
||||
"Message deleted": "Message deleted",
|
||||
"Message deleted by %(name)s": "Message deleted by %(name)s",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s sent a sticker.",
|
||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
||||
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.",
|
||||
|
@ -723,6 +728,20 @@
|
|||
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
|
||||
"Share your public space": "Share your public space",
|
||||
"Unknown App": "Unknown App",
|
||||
"Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?",
|
||||
"HTML": "HTML",
|
||||
"JSON": "JSON",
|
||||
"Plain Text": "Plain Text",
|
||||
"From the beginning": "From the beginning",
|
||||
"Specify a number of messages": "Specify a number of messages",
|
||||
"Current Timeline": "Current Timeline",
|
||||
"Media omitted": "Media omitted",
|
||||
"Media omitted - file size limit exceeded": "Media omitted - file size limit exceeded",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s created this room.",
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.",
|
||||
"Topic: %(topic)s": "Topic: %(topic)s",
|
||||
"Error fetching file": "Error fetching file",
|
||||
"File Attached": "File Attached",
|
||||
"Help us improve %(brand)s": "Help us improve %(brand)s",
|
||||
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
||||
"Yes": "Yes",
|
||||
|
@ -773,16 +792,6 @@
|
|||
"The person who invited you already left the room.": "The person who invited you already left the room.",
|
||||
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"New in the Spaces beta": "New in the Spaces beta",
|
||||
"Help people in spaces to find and join private rooms": "Help people in spaces to find and join private rooms",
|
||||
"Learn more": "Learn more",
|
||||
"Help space members find private rooms": "Help space members find private rooms",
|
||||
"To help space members find and join a private room, go to that room's Security & Privacy settings.": "To help space members find and join a private room, go to that room's Security & Privacy settings.",
|
||||
"General": "General",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Roles & Permissions": "Roles & Permissions",
|
||||
"This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.",
|
||||
"Skip": "Skip",
|
||||
"You joined the call": "You joined the call",
|
||||
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
||||
"Call in progress": "Call in progress",
|
||||
|
@ -1056,6 +1065,7 @@
|
|||
"Invite people": "Invite people",
|
||||
"Invite with email or username": "Invite with email or username",
|
||||
"Failed to save space settings.": "Failed to save space settings.",
|
||||
"General": "General",
|
||||
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
||||
"Saving...": "Saving...",
|
||||
"Save Changes": "Save Changes",
|
||||
|
@ -1465,6 +1475,7 @@
|
|||
"Muted Users": "Muted Users",
|
||||
"Banned users": "Banned users",
|
||||
"Send %(eventType)s events": "Send %(eventType)s events",
|
||||
"Roles & Permissions": "Roles & Permissions",
|
||||
"Permissions": "Permissions",
|
||||
"Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space",
|
||||
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
|
||||
|
@ -1487,6 +1498,7 @@
|
|||
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
|
||||
"People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.",
|
||||
"Who can read history?": "Who can read history?",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||
"Encrypted": "Encrypted",
|
||||
"Access": "Access",
|
||||
|
@ -1823,6 +1835,7 @@
|
|||
"%(count)s people|other": "%(count)s people",
|
||||
"%(count)s people|one": "%(count)s person",
|
||||
"Show files": "Show files",
|
||||
"Export chat": "Export chat",
|
||||
"Show threads": "Show threads",
|
||||
"Share room": "Share room",
|
||||
"Room settings": "Room settings",
|
||||
|
@ -1944,6 +1957,8 @@
|
|||
"Edit": "Edit",
|
||||
"Reply": "Reply",
|
||||
"Thread": "Thread",
|
||||
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
|
||||
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
|
||||
"Message Actions": "Message Actions",
|
||||
"Download %(text)s": "Download %(text)s",
|
||||
"Error decrypting attachment": "Error decrypting attachment",
|
||||
|
@ -1979,8 +1994,6 @@
|
|||
"Reactions": "Reactions",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||
"Message deleted": "Message deleted",
|
||||
"Message deleted by %(name)s": "Message deleted by %(name)s",
|
||||
"Message deleted on %(date)s": "Message deleted on %(date)s",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||
|
@ -2131,6 +2144,7 @@
|
|||
"QR Code": "QR Code",
|
||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||
"In reply to <a>this message</a>": "In reply to <a>this message</a>",
|
||||
"Room address": "Room address",
|
||||
"e.g. my-room": "e.g. my-room",
|
||||
"Some characters not allowed": "Some characters not allowed",
|
||||
|
@ -2209,6 +2223,7 @@
|
|||
"People you know on %(brand)s": "People you know on %(brand)s",
|
||||
"Hide": "Hide",
|
||||
"Show": "Show",
|
||||
"Skip": "Skip",
|
||||
"Send %(count)s invites|other": "Send %(count)s invites",
|
||||
"Send %(count)s invites|one": "Send %(count)s invite",
|
||||
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
|
||||
|
@ -2336,6 +2351,23 @@
|
|||
"There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
|
||||
"Update community": "Update community",
|
||||
"An error has occurred.": "An error has occurred.",
|
||||
"Enter a number between %(min)s and %(max)s": "Enter a number between %(min)s and %(max)s",
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB": "Size can only be a number between %(min)s MB and %(max)s MB",
|
||||
"Number of messages can only be a number between %(min)s and %(max)s": "Number of messages can only be a number between %(min)s and %(max)s",
|
||||
"Number of messages": "Number of messages",
|
||||
"MB": "MB",
|
||||
"Export Successful": "Export Successful",
|
||||
"The export was cancelled successfully": "The export was cancelled successfully",
|
||||
"Your export was successful. Find it in your Downloads folder.": "Your export was successful. Find it in your Downloads folder.",
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Are you sure you want to stop exporting your data? If you do, you'll need to start over.",
|
||||
"Stop": "Stop",
|
||||
"Exporting your data": "Exporting your data",
|
||||
"Export Chat": "Export Chat",
|
||||
"Select from the options below to export chats from your timeline": "Select from the options below to export chats from your timeline",
|
||||
"Format": "Format",
|
||||
"Size Limit": "Size Limit",
|
||||
"Include Attachments": "Include Attachments",
|
||||
"Export": "Export",
|
||||
"Feedback sent": "Feedback sent",
|
||||
"Rate %(brand)s": "Rate %(brand)s",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Tell us below how you feel about %(brand)s so far.",
|
||||
|
@ -2530,6 +2562,7 @@
|
|||
"We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
|
||||
"Other homeserver": "Other homeserver",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
|
||||
"Learn more": "Learn more",
|
||||
"About homeservers": "About homeservers",
|
||||
"Reset event store?": "Reset event store?",
|
||||
"You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
|
||||
|
@ -3098,7 +3131,6 @@
|
|||
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.",
|
||||
"Enter passphrase": "Enter passphrase",
|
||||
"Confirm passphrase": "Confirm passphrase",
|
||||
"Export": "Export",
|
||||
"Import room keys": "Import room keys",
|
||||
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.",
|
||||
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
|
||||
|
|
|
@ -3099,5 +3099,62 @@
|
|||
"What kind of Space do you want to create?": "Kian aron volas vi krei?",
|
||||
"All rooms you're in will appear in Home.": "Ĉiuj ĉambroj, kie vi estas, aperos en la ĉefpaĝo.",
|
||||
"Show all rooms in Home": "Montri ĉiujn ĉambrojn en ĉefpaĝo",
|
||||
"%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksis <a>mesaĝon</a> al ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>."
|
||||
"%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksis <a>mesaĝon</a> al ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>.",
|
||||
"To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.": "Por eviti tiujn problemojn, kreu <a>novan ĉifritan ĉambron</a> por la planata interparolo.",
|
||||
"<b>It's not recommended to add encryption to public rooms.</b>Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Ne rekomendate estas aldoni ĉifradon al publikaj ĉambroj.</b> Ĉiu ajn povas trovi publikajn ĉambrojn kaj aliĝi, do ĉiu ajn povas legi ties mesaĝojn. Vi havos neniujn avantaĝojn de ĉifrado, kaj vi ne povos ĝin malŝalti pli poste. Ĉifrado en publikaj ĉambroj malrapidigos ricevadon kaj sendadon de mesaĝoj.",
|
||||
"Are you sure you want to add encryption to this public room?": "Ĉu vi certas, ke vi volas aldoni ĉifradon al ĉi tiu publika ĉambro?",
|
||||
"Select the roles required to change various parts of the space": "Elekti rolojn bezonatajn por ŝanĝado de diversaj partoj de la aro",
|
||||
"Change description": "Ŝanĝi priskribon",
|
||||
"Change main address for the space": "Ŝanĝi ĉefadreson de aro",
|
||||
"Change space name": "Ŝanĝi nomon de aro",
|
||||
"Change space avatar": "Ŝanĝi bildon de aro",
|
||||
"Upgrade anyway": "Tamen gradaltigi",
|
||||
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Ĉi tiu ĉambro estas en iuj aroj, kiujn vi ne administras. En tiuj aroj, la malnova ĉambro aperos, sed tie oni ricevos avizon aliĝi al la nova.",
|
||||
"Before you upgrade": "Antaŭ ol vi gradaltigos",
|
||||
"Anyone in <spaceName/> can find and join. You can select other spaces too.": "Ĉiu en <spaceName/> povas trovi kaj aliĝi. Vi povas elekti ankaŭ aliajn arojn.",
|
||||
"Currently, %(count)s spaces have access|one": "Nun, aro povas aliri",
|
||||
"& %(count)s more|one": "kaj %(count)s pli",
|
||||
"To join a space you'll need an invite.": "Por aliĝi al aro, vi bezonas inviton.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Vi ankaŭ povas krei Arojn el <a>komunumoj</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Provizore montri komunumojn anstataŭ arojn por tiu ĉi salutaĵo. Subteno de tio ĉi baldaŭ malaperos. Ĉi tio re-enlegos Elementon.",
|
||||
"Display Communities instead of Spaces": "Montri komunumojn anstataŭ arojn",
|
||||
"Autoplay videos": "Memage ludi filmojn",
|
||||
"Autoplay GIFs": "Memage ludi GIF-ojn",
|
||||
"Multiple integration managers (requires manual setup)": "Pluraj kunigiloj (bezonas permanan agordon)",
|
||||
"Threaded messaging": "Mesaĝaj fadenoj",
|
||||
"%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s malfiksis mesaĝon de ĉi tiu ĉambro. Vidu ĉiujn fiksitajn mesaĝojn.",
|
||||
"%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s malfiksis <a>mesaĝon</a> de ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>.",
|
||||
"%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fiksis mesaĝon al ĉi tiu ĉambro. Vidu ĉiujn fiksitajn mesaĝojn.",
|
||||
"To join this Space, hide communities in your <a>preferences</a>": "Por aliĝi al ĉi tiu aro, kaŝu komunumojn per viaj <a>agordoj</a>",
|
||||
"To view this Space, hide communities in your <a>preferences</a>": "Por vidi ĉi tiun aron, kaŝu komunumojn per viaj <a>agordoj</a>",
|
||||
"Rooms and spaces": "Ĉambroj kaj aroj",
|
||||
"Results": "Rezultoj",
|
||||
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "Por aliĝi al %(communityName)s, ŝaltu komunumojn en viaj <a>agordoj</a>",
|
||||
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "Por vidi komunumon %(communityName)s, ŝaltu komunumojn en viaj <a>agordoj</a>",
|
||||
"Private community": "Privata komunumo",
|
||||
"Public community": "Publika komunumo",
|
||||
"Forward": "Plusendi",
|
||||
"Would you like to leave the rooms in this space?": "Ĉu vi volus foriri de la ĉambroj en ĉi tiu aro?",
|
||||
"You are about to leave <spaceName/>.": "Vi foriros de <spaceName/>.",
|
||||
"Leave some rooms": "Foriri de iuj ĉambroj",
|
||||
"Leave all rooms": "Foriri de ĉiuj ĉambroj",
|
||||
"Don't leave any rooms": "Foriru de neniuj ĉambroj",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s reagis per %(content)s",
|
||||
"Thread": "Fadeno",
|
||||
"Some encryption parameters have been changed.": "Ŝanĝiĝis iuj parametroj de ĉifrado.",
|
||||
"Role in <RoomName/>": "Rolo en <RoomName/>",
|
||||
"Message": "Mesaĝo",
|
||||
"Show threads": "Montri fadenojn",
|
||||
"Joining space …": "Aliĝante al aro…",
|
||||
"Explore %(spaceName)s": "Esplori aron %(spaceName)s",
|
||||
"Message didn't send. Click for info.": "Mesaĝo ne sendiĝis. Klaku por akiri informojn.",
|
||||
"Send a sticker": "Sendi glumarkon",
|
||||
"Reply to thread…": "Respondi al fadeno…",
|
||||
"Reply to encrypted thread…": "Respondi al ĉifrita fadeno…",
|
||||
"Add emoji": "Aldoni bildosignon",
|
||||
"To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "Por eviti ĉi tiujn problemojn, kreu <a>novan publikan ĉambron</a> por la dezirata interparolo.",
|
||||
"<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Publikigo de ĉifrataj ĉambroj estas malrekomendata.</b> Ĝi implicas, ke ĉiu povos trovi la ĉambron kaj aliĝi al ĝi, kaj ĉiu do povos legi mesaĝojn. Vi havos neniujn avantaĝojn de ĉifrado. Ĉifrado de mesaĝoj en publika ĉambro malrapidigos iliajn ricevadon kaj sendadon.",
|
||||
"Are you sure you want to make this encrypted room public?": "Ĉu vi certas, ke vi volas publikigi ĉi tiun ĉifratan ĉambron?",
|
||||
"Unknown failure": "Nekonata malsukceso",
|
||||
"Failed to update the join rules": "Malsukcesis ĝisdatigi regulojn pri aliĝo"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2705,7 +2705,7 @@
|
|||
"Invite by username": "Kutsu kasutajanime alusel",
|
||||
"What projects are you working on?": "Mis ettevõtmistega sa tegeled?",
|
||||
"Decrypted event source": "Sündmuse dekrüptitud lähtekood",
|
||||
"Original event source": "Algse sündmuse lähtekood",
|
||||
"Original event source": "Sündmuse töötlemata lähtekood",
|
||||
"Failed to remove some rooms. Try again later": "Mõnede jututubade eemaldamine ei õnnestunud. Proovi hiljem uuesti",
|
||||
"Removing...": "Eemaldan...",
|
||||
"Mark as not suggested": "Eemalda soovitus",
|
||||
|
@ -3158,5 +3158,47 @@
|
|||
"Before you upgrade": "Enne uuendamist",
|
||||
"To join a space you'll need an invite.": "Kogukonnakeskusega liitumiseks vajad kutset.",
|
||||
"%(reactors)s reacted with %(content)s": "%(reactors)s kasutajat reageeris järgnevalt: %(content)s",
|
||||
"Joining space …": "Liitun kohukonnakeskusega…"
|
||||
"Joining space …": "Liitun kohukonnakeskusega…",
|
||||
"Would you like to leave the rooms in this space?": "Kas sa soovid lahkuda ka selle kogukonna jututubadest?",
|
||||
"You are about to leave <spaceName/>.": "Sa oled lahkumas <spaceName/> kogukonnast.",
|
||||
"Leave some rooms": "Lahku mõnedest jututubadest",
|
||||
"Leave all rooms": "Lahku kõikidest jututubadest",
|
||||
"Don't leave any rooms": "Ära lahku ühestki jututoast",
|
||||
"Expand quotes │ ⇧+click": "Näita tsitaate │ ⇧+click",
|
||||
"Collapse quotes │ ⇧+click": "Ahenda tsitaadid │ ⇧+click",
|
||||
"Media omitted": "Osa meediat jäi eksportimata",
|
||||
"Media omitted - file size limit exceeded": "Osa meediat jäi vahele failisuuruse piirangu tõttu",
|
||||
"Include Attachments": "Kaasa manused",
|
||||
"Size Limit": "Andmemahu piir",
|
||||
"Format": "Vorming",
|
||||
"Select from the options below to export chats from your timeline": "Kui soovid oma ajajoonelt mõnda vestlust eksportida, siis vali tingimused alljärgnevalt",
|
||||
"Export Chat": "Ekspordi vestlus",
|
||||
"Exporting your data": "Ekspordin sinu andmeid",
|
||||
"Stop": "Peata",
|
||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Kas sa oled kindel, et soovid oma andmete eksporti katkestada? Kui nii toimid, siis pead hiljem uuesti alustama.",
|
||||
"Your export was successful. Find it in your Downloads folder.": "Sinu andmete eksport õnnestus. Faili leiad tavapärasest allalaadimiste kaustast.",
|
||||
"The export was cancelled successfully": "Ekspordi tühistamine õnnestus",
|
||||
"Export Successful": "Eksport õnnestus",
|
||||
"MB": "MB",
|
||||
"Number of messages": "Sõnumite arv",
|
||||
"Number of messages can only be a number between %(min)s and %(max)s": "Sõnumite arv saab olla ainult number%(min)s ja %(max)s vahemikust",
|
||||
"Size can only be a number between %(min)s MB and %(max)s MB": "Suurus saab olla number %(min)s MB ja %(max)s MB vahemikust",
|
||||
"Enter a number between %(min)s and %(max)s": "Sisesta number %(min)s ja %(max)s vahemikust",
|
||||
"In reply to <a>this message</a>": "Vastuseks <a>sellele sõnumile</a>",
|
||||
"Export chat": "Ekspordi vestlus",
|
||||
"File Attached": "Fail on manustatud",
|
||||
"Error fetching file": "Viga faili laadimisel",
|
||||
"Topic: %(topic)s": "Teema: %(topic)s",
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.": "See on <roomName/> jututoast eksporditud andmekogu. Viited: <exporterDetails/>, %(exportDate)s.",
|
||||
"%(creatorName)s created this room.": "%(creatorName)s lõi selle jututoa.",
|
||||
"Current Timeline": "Praegune ajajoon",
|
||||
"Specify a number of messages": "Määra sõnumite arv",
|
||||
"From the beginning": "Algusest alates",
|
||||
"Plain Text": "Vormindamata tekst",
|
||||
"JSON": "JSON",
|
||||
"HTML": "HTML",
|
||||
"Are you sure you want to exit during this export?": "Kas sa oled kindel, et soovid lõpetada tegevuse selle ekspordi ajal?",
|
||||
"%(senderDisplayName)s sent a sticker.": "%(senderDisplayName)s saatis kleepsu.",
|
||||
"%(senderDisplayName)s changed the room avatar.": "%(senderDisplayName)s muutis jututoa tunnuspilti.",
|
||||
"%(date)s at %(time)s": "%(date)s %(time)s"
|
||||
}
|
||||
|
|
|
@ -3162,5 +3162,12 @@
|
|||
"To join a space you'll need an invite.": "Vous avez besoin d’une invitation pour rejoindre un espace.",
|
||||
"You can also make Spaces from <a>communities</a>.": "Vous pouvez également créer des espaces à partir de <a>communautés</a>.",
|
||||
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Montre temporairement les communautés au lieu des espaces pour cette session. Il ne sera plus possible de le faire dans un futur proche. Cela va recharger Element.",
|
||||
"Display Communities instead of Spaces": "Afficher les communautés au lieu des espaces"
|
||||
"Display Communities instead of Spaces": "Afficher les communautés au lieu des espaces",
|
||||
"Would you like to leave the rooms in this space?": "Voulez-vous quitter les salons de cet espace ?",
|
||||
"You are about to leave <spaceName/>.": "Vous êtes sur le point de quitter <spaceName/>.",
|
||||
"Leave some rooms": "Quitter certains salons",
|
||||
"Leave all rooms": "Quitter tous les salons",
|
||||
"Don't leave any rooms": "Ne quitter aucun salon",
|
||||
"Expand quotes │ ⇧+click": "Développer les citations │ ⇧+clic",
|
||||
"Collapse quotes │ ⇧+click": "Réduire les citations │ ⇧+clic"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue