mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
Merge branch 'develop' into Bubble-bericht
This commit is contained in:
parent
f367d617c5
commit
c843387043
@ -15,7 +15,6 @@ module.exports = {
|
|||||||
"prefer-promise-reject-errors": "off",
|
"prefer-promise-reject-errors": "off",
|
||||||
"no-async-promise-executor": "off",
|
"no-async-promise-executor": "off",
|
||||||
"quotes": "off",
|
"quotes": "off",
|
||||||
"indent": "off",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [{
|
overrides: [{
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
/*.log
|
/*.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
/coverage
|
||||||
/node_modules
|
/node_modules
|
||||||
/lib
|
/lib
|
||||||
|
|
||||||
|
118
CHANGELOG.md
118
CHANGELOG.md
@ -1,3 +1,121 @@
|
|||||||
|
Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 10.0.0
|
||||||
|
* [Release] Dynamic max and min zoom in the new ImageView
|
||||||
|
[\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927)
|
||||||
|
* [Release] Add a WheelEvent normalization function
|
||||||
|
[\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911)
|
||||||
|
* Add a WheelEvent normalization function
|
||||||
|
[\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904)
|
||||||
|
* [Release] Use floats for image background opacity
|
||||||
|
[\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907)
|
||||||
|
|
||||||
|
Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 10.0.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896)
|
||||||
|
* Fix sticky tags header in room list
|
||||||
|
[\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895)
|
||||||
|
* Fix spaces filtering sometimes lagging behind or behaving oddly
|
||||||
|
[\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893)
|
||||||
|
* Fix issue with spaces context switching looping and breaking
|
||||||
|
[\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894)
|
||||||
|
* Improve RoomList render time when filtering
|
||||||
|
[\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874)
|
||||||
|
* Avoid being stuck in a space
|
||||||
|
[\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891)
|
||||||
|
* [Spaces] Context switching
|
||||||
|
[\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795)
|
||||||
|
* Warn when you attempt to leave room that you are the only member of
|
||||||
|
[\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415)
|
||||||
|
* Ensure PersistedElement are unmounted on application logout
|
||||||
|
[\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884)
|
||||||
|
* Add missing space in seshat dialog and the corresponding string
|
||||||
|
[\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866)
|
||||||
|
* A tiny change to make the Add existing rooms dialog a little nicer
|
||||||
|
[\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885)
|
||||||
|
* Remove weird margin from the file panel
|
||||||
|
[\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889)
|
||||||
|
* Trigger lazy loading when filtering using spaces
|
||||||
|
[\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882)
|
||||||
|
* Fix typo in method call in add existing to space dialog
|
||||||
|
[\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883)
|
||||||
|
* New Image View fixes/improvements
|
||||||
|
[\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872)
|
||||||
|
* Limit voice recording length
|
||||||
|
[\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871)
|
||||||
|
* Clean up add existing to space dialog and include DMs in it too
|
||||||
|
[\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881)
|
||||||
|
* Fix unknown slash command error exploding
|
||||||
|
[\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853)
|
||||||
|
* Switch to a spec conforming email validation Regexp
|
||||||
|
[\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852)
|
||||||
|
* Cleanup unused state in MessageComposer
|
||||||
|
[\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877)
|
||||||
|
* Pulse animation for voice messages recording state
|
||||||
|
[\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869)
|
||||||
|
* Don't include invisible rooms in notify summary
|
||||||
|
[\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875)
|
||||||
|
* Properly disable composer access when recording a voice message
|
||||||
|
[\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870)
|
||||||
|
* Stabilise starting a DM with multiple people flow
|
||||||
|
[\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862)
|
||||||
|
* Render msgOption only if showReadReceipts is enabled
|
||||||
|
[\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864)
|
||||||
|
* Labs: Add quick/cheap "do not disturb" flag
|
||||||
|
[\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873)
|
||||||
|
* Fix ReadReceipts animations
|
||||||
|
[\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836)
|
||||||
|
* Add tooltips to message previews
|
||||||
|
[\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859)
|
||||||
|
* IRC Layout fix layout spacing in replies
|
||||||
|
[\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855)
|
||||||
|
* Move user to welcome_page if continuing with previous session
|
||||||
|
[\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849)
|
||||||
|
* Improve image view
|
||||||
|
[\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521)
|
||||||
|
* Add a button to reset personal encryption state during login
|
||||||
|
[\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819)
|
||||||
|
* Fix js-sdk import in SlashCommands
|
||||||
|
[\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850)
|
||||||
|
* Fix useRoomPowerLevels hook
|
||||||
|
[\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854)
|
||||||
|
* Prevent state events being rendered with invalid state keys
|
||||||
|
[\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851)
|
||||||
|
* Give server ACLs a name in 'roles & permissions' tab
|
||||||
|
[\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838)
|
||||||
|
* Don't hide notification badge on the home space button as it has no menu
|
||||||
|
[\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845)
|
||||||
|
* User Info hide disambiguation as we always show MXID anyway
|
||||||
|
[\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843)
|
||||||
|
* Improve kick state to not show if the target was not joined to begin with
|
||||||
|
[\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846)
|
||||||
|
* Fix space store wrongly switching to a non-space filter
|
||||||
|
[\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844)
|
||||||
|
* Tweak appearance of invite reason
|
||||||
|
[\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847)
|
||||||
|
* Update Inter font to v3.18
|
||||||
|
[\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840)
|
||||||
|
* Enable sharing historical keys on invite
|
||||||
|
[\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839)
|
||||||
|
* Add ability to hide post-login encryption setup with customisation point
|
||||||
|
[\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834)
|
||||||
|
* Use LaTeX and TeX delimiters by default
|
||||||
|
[\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515)
|
||||||
|
* Clone author's deps fork for Netlify previews
|
||||||
|
[\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837)
|
||||||
|
* Show drop file UI only if dragging a file
|
||||||
|
[\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827)
|
||||||
|
* Ignore punctuation when filtering rooms
|
||||||
|
[\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824)
|
||||||
|
* Resizable CallView
|
||||||
|
[\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710)
|
||||||
|
|
||||||
Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12)
|
Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0)
|
||||||
|
@ -28,7 +28,7 @@ Platform Targets:
|
|||||||
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||||
* Mobile Web is not currently a target platform - instead please use the native
|
* Mobile Web is not currently a target platform - instead please use the native
|
||||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||||
(https://github.com/matrix-org/matrix-android-sdk) SDKs.
|
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||||
|
|
||||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||||
**Please file PRs against `develop`!!**
|
**Please file PRs against `develop`!!**
|
||||||
|
19
package.json
19
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.18.0",
|
"version": "3.19.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -23,9 +23,7 @@
|
|||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"reskindex": "scripts/reskindex.js",
|
"reskindex": "scripts/reskindex.js"
|
||||||
"matrix-gen-i18n": "scripts/gen-i18n.js",
|
|
||||||
"matrix-prune-i18n": "scripts/prune-i18n.js"
|
|
||||||
},
|
},
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"matrix_src_main": "./src/index.js",
|
"matrix_src_main": "./src/index.js",
|
||||||
@ -35,7 +33,7 @@
|
|||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
"i18n": "matrix-gen-i18n",
|
"i18n": "matrix-gen-i18n",
|
||||||
"prunei18n": "matrix-prune-i18n",
|
"prunei18n": "matrix-prune-i18n",
|
||||||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||||
"reskindex": "node scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
"rethemendex": "res/css/rethemendex.sh",
|
"rethemendex": "res/css/rethemendex.sh",
|
||||||
@ -51,7 +49,8 @@
|
|||||||
"lint:types": "tsc --noEmit --jsx react",
|
"lint:types": "tsc --noEmit --jsx react",
|
||||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080"
|
"test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080",
|
||||||
|
"coverage": "yarn test --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
@ -133,6 +132,7 @@
|
|||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^16.9",
|
||||||
"@types/react-dom": "^16.9.10",
|
"@types/react-dom": "^16.9.10",
|
||||||
@ -160,6 +160,7 @@
|
|||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.2",
|
"matrix-react-test-utils": "^0.2.2",
|
||||||
|
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
@ -189,6 +190,12 @@
|
|||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!matrix-js-sdk).+$"
|
"/node_modules/(?!matrix-js-sdk).+$"
|
||||||
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"<rootDir>/src/**/*.{js,ts,tsx}"
|
||||||
|
],
|
||||||
|
"coverageReporters": [
|
||||||
|
"text"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,7 @@
|
|||||||
@import "./views/messages/_MStickerBody.scss";
|
@import "./views/messages/_MStickerBody.scss";
|
||||||
@import "./views/messages/_MTextBody.scss";
|
@import "./views/messages/_MTextBody.scss";
|
||||||
@import "./views/messages/_MVideoBody.scss";
|
@import "./views/messages/_MVideoBody.scss";
|
||||||
|
@import "./views/messages/_MVoiceMessageBody.scss";
|
||||||
@import "./views/messages/_MessageActionBar.scss";
|
@import "./views/messages/_MessageActionBar.scss";
|
||||||
@import "./views/messages/_MessageTimestamp.scss";
|
@import "./views/messages/_MessageTimestamp.scss";
|
||||||
@import "./views/messages/_MjolnirBody.scss";
|
@import "./views/messages/_MjolnirBody.scss";
|
||||||
@ -248,6 +249,8 @@
|
|||||||
@import "./views/toasts/_AnalyticsToast.scss";
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
|
@import "./views/voice_messages/_PlayPauseButton.scss";
|
||||||
|
@import "./views/voice_messages/_PlaybackContainer.scss";
|
||||||
@import "./views/voice_messages/_Waveform.scss";
|
@import "./views/voice_messages/_Waveform.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomStatusBar {
|
.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
|
||||||
margin-left: 65px;
|
margin-left: 65px;
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
@ -68,6 +68,99 @@ limitations under the License.
|
|||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomStatusBar_unsentMessages {
|
||||||
|
> div[role="alert"] {
|
||||||
|
// cheat some basic alignment
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 70px;
|
||||||
|
margin: 12px;
|
||||||
|
padding-left: 16px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomStatusBar_unsentBadge {
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
// Override sizing from the default badge
|
||||||
|
width: 24px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
border-radius: 24px !important;
|
||||||
|
|
||||||
|
.mx_NotificationBadge_count {
|
||||||
|
font-size: $font-16px !important; // override default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomStatusBar_unsentTitle {
|
||||||
|
color: $warning-color;
|
||||||
|
font-size: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomStatusBar_unsentDescription {
|
||||||
|
font-size: $font-12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomStatusBar_unsentButtonBar {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 22px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
padding: 5px 10px;
|
||||||
|
padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
border-left: 1px solid $resend-button-divider-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10px; // inset for regular button padding
|
||||||
|
background-color: $muted-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomStatusBar_unsentCancelAllBtn::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
|
width: 12px;
|
||||||
|
height: 16px;
|
||||||
|
top: calc(50% - 8px); // text sizes are dynamic
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomStatusBar_unsentResendAllBtn {
|
||||||
|
padding-left: 34px; // 28px from above, but +6px to account for the wider icon
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
top: calc(50% - 9px); // text sizes are dynamic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InlineSpinner {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 5px;
|
||||||
|
top: 1px; // just to help the vertical alignment be slightly better
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
margin-right: 10px; // same margin/padding as the rightmost button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomStatusBar_connectionLostBar img {
|
.mx_RoomStatusBar_connectionLostBar img {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
@ -103,7 +196,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat_useCompactLayout {
|
.mx_MatrixChat_useCompactLayout {
|
||||||
.mx_RoomStatusBar {
|
.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color;
|
|||||||
.mx_SpaceItem {
|
.mx_SpaceItem {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-flow: wrap;
|
flex-flow: wrap;
|
||||||
|
|
||||||
|
&.mx_SpaceItem_narrow {
|
||||||
|
align-self: baseline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceItem.collapsed {
|
.mx_SpaceItem.collapsed {
|
||||||
@ -233,7 +237,6 @@ $activeBorderColor: $secondary-fg-color;
|
|||||||
|
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
// Create a flexbox to make aligning dot badges easier
|
// Create a flexbox to make aligning dot badges easier
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -245,23 +248,37 @@ $activeBorderColor: $secondary-fg-color;
|
|||||||
|
|
||||||
.mx_NotificationBadge_dot {
|
.mx_NotificationBadge_dot {
|
||||||
// make the smaller dot occupy the same width for centering
|
// make the smaller dot occupy the same width for centering
|
||||||
margin-left: 7px;
|
margin: 0 7px;
|
||||||
margin-right: 7px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
.mx_SpaceButton {
|
.mx_SpaceButton {
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
right: -3px;
|
right: 0;
|
||||||
top: -3px;
|
top: 0;
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge_dot {
|
||||||
|
margin: 0 -1px 0 0;
|
||||||
|
border: 3px solid $groupFilterPanel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge_2char,
|
||||||
|
.mx_NotificationBadge_3char {
|
||||||
|
margin: -5px -5px 0 0;
|
||||||
|
border: 2px solid $groupFilterPanel-bg-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
|
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
|
||||||
// when we draw the selection border we move the relative bounds of our parent
|
// when we draw the selection border we move the relative bounds of our parent
|
||||||
// so update our position within the bounds of the parent to maintain position overall
|
// so update our position within the bounds of the parent to maintain position overall
|
||||||
right: -6px;
|
right: -3px;
|
||||||
top: -6px;
|
top: -3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,7 +292,7 @@ $activeBorderColor: $secondary-fg-color;
|
|||||||
.mx_SpaceButton:hover,
|
.mx_SpaceButton:hover,
|
||||||
.mx_SpaceButton:focus-within,
|
.mx_SpaceButton:focus-within,
|
||||||
.mx_SpaceButton_hasMenuOpen {
|
.mx_SpaceButton_hasMenuOpen {
|
||||||
&:not(.mx_SpaceButton_home) {
|
&:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) {
|
||||||
// Hide the badge container on hover because it'll be a menu button
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -26,7 +26,10 @@ limitations under the License.
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomDirectory,
|
||||||
|
.mx_SpaceRoomView_landing {
|
||||||
.mx_Dialog_title {
|
.mx_Dialog_title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@ -56,7 +59,6 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_content {
|
|
||||||
.mx_AccessibleButton_kind_link {
|
.mx_AccessibleButton_kind_link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -84,7 +86,7 @@ limitations under the License.
|
|||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 2px 8px;
|
padding: 4px 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
& + .mx_AccessibleButton {
|
& + .mx_AccessibleButton {
|
||||||
@ -92,6 +94,11 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_danger_outline,
|
||||||
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
|
padding: 3px 12px; // to account for the 1px border
|
||||||
|
}
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
@ -117,7 +124,6 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_list {
|
.mx_SpaceRoomDirectory_list {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@ -245,11 +251,17 @@ limitations under the License.
|
|||||||
grid-row: 1/3;
|
grid-row: 1/3;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 8px 18px;
|
line-height: $font-24px;
|
||||||
|
padding: 4px 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_danger_outline,
|
||||||
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
|
padding: 3px 16px; // to account for the 1px border
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Checkbox {
|
.mx_Checkbox {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -81,6 +81,16 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||||||
color: $secondary-fg-color;
|
color: $secondary-fg-color;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
max-width: $SpaceRoomViewInnerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace {
|
||||||
|
max-width: $SpaceRoomViewInnerWidth;
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_content {
|
||||||
|
height: calc(100vh - 360px);
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_buttons {
|
.mx_SpaceRoomView_buttons {
|
||||||
@ -228,7 +238,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||||||
|
|
||||||
.mx_SpaceRoomView_landing_inviteButton {
|
.mx_SpaceRoomView_landing_inviteButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 40px;
|
padding: 4px 18px 4px 40px;
|
||||||
|
line-height: $font-24px;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -244,6 +255,27 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_landing_settingsButton {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 16px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background: $tertiary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_topic {
|
.mx_SpaceRoomView_landing_topic {
|
||||||
@ -258,80 +290,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_adminButtons {
|
|
||||||
margin-top: 24px;
|
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
|
||||||
position: relative;
|
|
||||||
width: 160px;
|
|
||||||
height: 124px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 72px 16px 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid $input-border-color;
|
|
||||||
margin-right: 28px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: $font-14px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(141, 151, 165, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before, &::after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
left: 16px;
|
|
||||||
top: 16px;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 30px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
background: #ffffff; // white icon fill
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_SpaceRoomView_landing_addButton {
|
|
||||||
&::before {
|
|
||||||
background-color: #ac3ba8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_SpaceRoomView_landing_createButton {
|
|
||||||
&::before {
|
|
||||||
background-color: #368bd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_SpaceRoomView_landing_settingsButton {
|
|
||||||
&::before {
|
|
||||||
background-color: #5c56f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBox {
|
.mx_SearchBox {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,66 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace {
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_section {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_entry {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_entry_name {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Checkbox {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_section_spaces {
|
||||||
|
.mx_BaseAvatar_image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog {
|
.mx_AddExistingToSpaceDialog {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
@ -41,7 +101,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_BaseAvatar {
|
.mx_BaseAvatar {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin: 5px 16px 5px 5px;
|
margin: auto 16px auto 5px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,85 +160,32 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SearchBox {
|
.mx_AddExistingToSpace {
|
||||||
// To match the space around the title
|
display: contents;
|
||||||
margin: 0 0 15px 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_errorText {
|
|
||||||
font-weight: $font-semi-bold;
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
color: $notice-primary-color;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_content {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_noResults {
|
|
||||||
display: block;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_section {
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
font-size: $font-12px;
|
|
||||||
font-weight: $font-semi-bold;
|
|
||||||
line-height: $font-15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_entry {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_entry_name {
|
|
||||||
font-size: $font-15px;
|
|
||||||
line-height: 30px;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_section_spaces {
|
|
||||||
.mx_BaseAvatar_image {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_footer {
|
.mx_AddExistingToSpaceDialog_footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 32px;
|
margin-top: 20px;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: $font-14px;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
font-weight: $font-semi-bold;
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_ProgressBar {
|
||||||
font-size: inherit;
|
height: 8px;
|
||||||
display: inline-block;
|
width: 100%;
|
||||||
|
|
||||||
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_progressText {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@ -186,8 +193,54 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_error {
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_errorHeading {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_errorCaption {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary {
|
||||||
|
padding: 8px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_retryButton {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_link {
|
.mx_AccessibleButton_kind_link {
|
||||||
|
@ -76,12 +76,16 @@ limitations under the License.
|
|||||||
border: 1px solid $button-danger-bg-color;
|
border: 1px solid $button-danger-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
|
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
|
||||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
|
||||||
color: $button-danger-disabled-fg-color;
|
color: $button-danger-disabled-fg-color;
|
||||||
background-color: $button-danger-disabled-bg-color;
|
background-color: $button-danger-disabled-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
||||||
|
color: $button-danger-disabled-bg-color;
|
||||||
|
border-color: $button-danger-disabled-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
|
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
color: $button-danger-fg-color;
|
color: $button-danger-fg-color;
|
||||||
|
@ -31,8 +31,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_ImageView_image {
|
.mx_ImageView_image {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
max-width: 95%;
|
flex-shrink: 0;
|
||||||
max-height: 95%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_panel {
|
.mx_ImageView_panel {
|
||||||
|
@ -21,7 +21,7 @@ progress.mx_ProgressBar {
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
@mixin ProgressBarBorderRadius "6px";
|
@mixin ProgressBarBorderRadius 6px;
|
||||||
@mixin ProgressBarColour $progressbar-fg-color;
|
@mixin ProgressBarColour $progressbar-fg-color;
|
||||||
@mixin ProgressBarBgColour $progressbar-bg-color;
|
@mixin ProgressBarBgColour $progressbar-bg-color;
|
||||||
::-webkit-progress-value {
|
::-webkit-progress-value {
|
||||||
|
@ -61,9 +61,9 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_MFileBody_info {
|
.mx_MFileBody_info {
|
||||||
background-color: $message-body-panel-bg-color;
|
background-color: $message-body-panel-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
width: 270px;
|
width: 243px; // same width as a playable voice message, accounting for padding
|
||||||
padding: 8px;
|
padding: 6px 12px;
|
||||||
color: $message-body-panel-fg-color;
|
color: $message-body-panel-fg-color;
|
||||||
|
|
||||||
.mx_MFileBody_info_icon {
|
.mx_MFileBody_info_icon {
|
||||||
@ -82,7 +82,7 @@ limitations under the License.
|
|||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||||
background-color: $message-body-panel-fg-color;
|
background-color: $message-body-panel-icon-fg-color;
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
||||||
|
19
res/css/views/messages/_MVoiceMessageBody.scss
Normal file
19
res/css/views/messages/_MVoiceMessageBody.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
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_MVoiceMessageBody {
|
||||||
|
display: inline-block; // makes the playback controls magically line up
|
||||||
|
}
|
@ -105,3 +105,11 @@ limitations under the License.
|
|||||||
.mx_MessageActionBar_optionsButton::after {
|
.mx_MessageActionBar_optionsButton::after {
|
||||||
mask-image: url('$(res)/img/element-icons/context-menu.svg');
|
mask-image: url('$(res)/img/element-icons/context-menu.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_resendButton::after {
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_cancelButton::after {
|
||||||
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
|
}
|
||||||
|
@ -34,6 +34,10 @@ limitations under the License.
|
|||||||
border-color: $reaction-row-button-selected-border-color;
|
border-color: $reaction-row-button-selected-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ReactionsRowButton_content {
|
.mx_ReactionsRowButton_content {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -219,10 +219,6 @@ $left-gutter: 64px;
|
|||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_notSent {
|
|
||||||
color: $event-notsent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_receiptSent,
|
.mx_EventTile_receiptSent,
|
||||||
.mx_EventTile_receiptSending {
|
.mx_EventTile_receiptSending {
|
||||||
// We don't use `position: relative` on the element because then it won't line
|
// We don't use `position: relative` on the element because then it won't line
|
||||||
|
@ -35,22 +35,29 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceRecordComposerTile_waveformContainer {
|
.mx_VoiceRecordComposerTile_delete {
|
||||||
padding: 5px;
|
width: 14px; // w&h are size of icon
|
||||||
padding-right: 4px; // there's 1px from the waveform itself, so account for that
|
height: 18px;
|
||||||
padding-left: 15px; // +10px for the live circle, +5px for regular padding
|
vertical-align: middle;
|
||||||
background-color: $voice-record-waveform-bg-color;
|
margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
|
||||||
border-radius: 12px;
|
background-color: $voice-record-icon-color;
|
||||||
margin-right: 12px; // isolate from stop button
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
|
}
|
||||||
|
|
||||||
// Cheat at alignment a bit
|
.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer {
|
||||||
display: flex;
|
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||||
align-items: center;
|
|
||||||
|
margin: 6px; // force the composer area to put a gutter around us
|
||||||
|
margin-right: 12px; // isolate from stop button
|
||||||
|
|
||||||
position: relative; // important for the live circle
|
position: relative; // important for the live circle
|
||||||
|
|
||||||
color: $voice-record-waveform-fg-color;
|
&.mx_VoiceRecordComposerTile_recording {
|
||||||
font-size: $font-14px;
|
// We are putting the circle in this padding, so we need +10px from the regular
|
||||||
|
// padding on the left side.
|
||||||
|
padding-left: 22px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
animation: recording-pulse 2s infinite;
|
animation: recording-pulse 2s infinite;
|
||||||
@ -60,19 +67,10 @@ limitations under the License.
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px;
|
left: 12px; // 12px from the left edge for container padding
|
||||||
top: 16px; // vertically center
|
top: 18px; // vertically center (middle align with clock)
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Waveform_bar {
|
|
||||||
background-color: $voice-record-waveform-fg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Clock {
|
|
||||||
padding-right: 8px; // isolate from waveform
|
|
||||||
padding-left: 10px; // isolate from live circle
|
|
||||||
width: 42px; // we're not using a monospace font, so fake it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,3 +22,34 @@ limitations under the License.
|
|||||||
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
|
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab code {
|
||||||
|
word-break: break-all;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: solid 1px $light-fg-color;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken_copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
||||||
|
mask-image: url($copy-button-url);
|
||||||
|
background-color: $message-action-bar-fg-color;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
51
res/css/views/voice_messages/_PlayPauseButton.scss
Normal file
51
res/css/views/voice_messages/_PlayPauseButton.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_PlayPauseButton {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 32px;
|
||||||
|
background-color: $voice-playback-button-bg-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute; // sizing varies by icon
|
||||||
|
background-color: $voice-playback-button-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_PlayPauseButton_disabled::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_PlayPauseButton_play::before {
|
||||||
|
width: 13px;
|
||||||
|
height: 16px;
|
||||||
|
top: 8px; // center
|
||||||
|
left: 12px; // center
|
||||||
|
mask-image: url('$(res)/img/element-icons/play.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_PlayPauseButton_pause::before {
|
||||||
|
width: 10px;
|
||||||
|
height: 12px;
|
||||||
|
top: 10px; // center
|
||||||
|
left: 11px; // center
|
||||||
|
mask-image: url('$(res)/img/element-icons/pause.svg');
|
||||||
|
}
|
||||||
|
}
|
53
res/css/views/voice_messages/_PlaybackContainer.scss
Normal file
53
res/css/views/voice_messages/_PlaybackContainer.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Dev note: there's no actual component called <PlaybackContainer />. These classes
|
||||||
|
// are shared amongst multiple voice message components.
|
||||||
|
|
||||||
|
// Container for live recording and playback controls
|
||||||
|
.mx_VoiceMessagePrimaryContainer {
|
||||||
|
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||||
|
// has a 1px padding on it that we want to account for.
|
||||||
|
padding: 7px 12px 7px 11px;
|
||||||
|
background-color: $voice-record-waveform-bg-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
// Cheat at alignment a bit
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
color: $voice-record-waveform-fg-color;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
.mx_Waveform {
|
||||||
|
.mx_Waveform_bar {
|
||||||
|
background-color: $voice-record-waveform-incomplete-fg-color;
|
||||||
|
|
||||||
|
&.mx_Waveform_bar_100pct {
|
||||||
|
// Small animation to remove the mechanical feel of progress
|
||||||
|
transition: background-color 250ms ease;
|
||||||
|
background-color: $voice-record-waveform-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Clock {
|
||||||
|
width: 42px; // we're not using a monospace font, so fake it
|
||||||
|
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
||||||
|
padding-left: 8px; // isolate from recording circle / play control
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_CallView {
|
.mx_CallView {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: $voipcall-plinth-color;
|
background-color: $dark-panel-bg-color;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
||||||
@ -40,7 +40,8 @@ limitations under the License.
|
|||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
|
background-color: $voipcall-plinth-color;
|
||||||
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.mx_CallView_voice {
|
.mx_CallView_voice {
|
||||||
@ -64,14 +65,17 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_voice {
|
.mx_CallView_content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_voice_avatarsContainer {
|
.mx_CallView_voice_avatarsContainer {
|
||||||
@ -108,9 +112,7 @@ limitations under the License.
|
|||||||
.mx_CallView_video {
|
.mx_CallView_video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,21 +14,37 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_VideoFeed_voice {
|
||||||
|
// We don't want to collide with the call controls that have 52px of height
|
||||||
|
padding-bottom: 52px;
|
||||||
|
background-color: $inverted-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.mx_VideoFeed_remote {
|
.mx_VideoFeed_remote {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.mx_VideoFeed_video {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
z-index: 50;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_local {
|
.mx_VideoFeed_local {
|
||||||
width: 25%;
|
max-width: 25%;
|
||||||
height: 25%;
|
max-height: 25%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.mx_VideoFeed_video {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_mirror {
|
.mx_VideoFeed_mirror {
|
||||||
|
4
res/img/element-icons/pause.svg
Normal file
4
res/img/element-icons/pause.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 1C0 0.447715 0.447715 0 1 0H2C2.55228 0 3 0.447715 3 1V11C3 11.5523 2.55228 12 2 12H1C0.447715 12 0 11.5523 0 11V1Z" fill="#737D8C"/>
|
||||||
|
<path d="M7 1C7 0.447715 7.44772 0 8 0H9C9.55228 0 10 0.447715 10 1V11C10 11.5523 9.55228 12 9 12H8C7.44772 12 7 11.5523 7 11V1Z" fill="#737D8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 396 B |
3
res/img/element-icons/play.svg
Normal file
3
res/img/element-icons/play.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 14.2104V1.78956C0 1.00724 0.857827 0.527894 1.5241 0.937906L11.6161 7.14834C12.2506 7.53883 12.2506 8.46117 11.6161 8.85166L1.5241 15.0621C0.857828 15.4721 0 14.9928 0 14.2104Z" fill="#737D8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 310 B |
3
res/img/element-icons/retry.svg
Normal file
3
res/img/element-icons/retry.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58365 3.90848C5.79757 2.94852 7.33285 2.375 9 2.375C12.6817 2.375 15.7112 5.1675 16.086 8.75H17.6314C17.9253 8.75 18.1006 9.07792 17.9376 9.32274L15.6812 12.711C15.5355 12.9297 15.2145 12.9297 15.0688 12.711L12.8124 9.32274C12.6494 9.07792 12.8247 8.75 13.1186 8.75H14.5754C14.2088 5.99798 11.8523 3.875 9 3.875C7.68247 3.875 6.4726 4.32705 5.51407 5.08504C5.45221 5.13396 5.39899 5.17326 5.36001 5.20114C5.34047 5.21513 5.32433 5.22637 5.31229 5.23463L5.29733 5.24482L5.29227 5.24821L5.29037 5.24948L5.28958 5.25L5.28923 5.25023L5.28906 5.25034L5.28898 5.2504L4.875 4.625L5.2889 5.25045C4.94347 5.47904 4.47814 5.38433 4.24955 5.0389C4.02136 4.69408 4.11534 4.22977 4.45929 4.00075L4.4633 3.99802C4.46789 3.99487 4.47605 3.9892 4.48719 3.98123C4.5096 3.9652 4.5433 3.94038 4.58365 3.90848ZM3.42456 10.25H4.88138C5.1753 10.25 5.35061 9.92208 5.18758 9.67726L2.93119 6.28905C2.78553 6.07032 2.46447 6.07032 2.31881 6.28905L0.0624241 9.67726C-0.100613 9.92207 0.0746987 10.25 0.368618 10.25H1.914C2.28878 13.8325 5.31828 16.625 9 16.625C10.7415 16.625 12.3388 15.9992 13.5764 14.9611C13.8938 14.6949 13.9353 14.2219 13.6691 13.9045C13.4029 13.5872 12.9298 13.5457 12.6125 13.8119C11.6349 14.6319 10.376 15.125 9 15.125C6.14769 15.125 3.79123 13.002 3.42456 10.25Z" fill="#737D8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
3
res/img/element-icons/trashcan.svg
Normal file
3
res/img/element-icons/trashcan.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="12" height="17" viewBox="0 0 12 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.857143 14.5C0.857143 15.4491 1.62857 16.5 2.57143 16.5H9.42857C10.3714 16.5 11.1429 15.2542 11.1429 14.3051V5.67692C11.1429 4.72781 10.3714 3.95128 9.42857 3.95128H2.57143C1.62857 3.95128 0.857143 4.72781 0.857143 5.67692V14.5ZM11.1429 1.36282H9L8.39143 0.750218C8.23714 0.59491 8.01429 0.5 7.79143 0.5H4.20857C3.98571 0.5 3.76286 0.59491 3.60857 0.750218L3 1.36282H0.857143C0.385714 1.36282 0 1.75109 0 2.22564C0 2.70019 0.385714 3.08846 0.857143 3.08846H11.1429C11.6143 3.08846 12 2.70019 12 2.22564C12 1.75109 11.6143 1.36282 11.1429 1.36282Z" fill="#737D8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 679 B |
@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
|
|||||||
$header-panel-text-secondary-color: #c8c8cd;
|
$header-panel-text-secondary-color: #c8c8cd;
|
||||||
$text-primary-color: #ffffff;
|
$text-primary-color: #ffffff;
|
||||||
$text-secondary-color: #B9BEC6;
|
$text-secondary-color: #B9BEC6;
|
||||||
|
$quaternary-fg-color: #6F7882;
|
||||||
$search-bg-color: #181b21;
|
$search-bg-color: #181b21;
|
||||||
$search-placeholder-color: #61708b;
|
$search-placeholder-color: #61708b;
|
||||||
$room-highlight-color: #343a46;
|
$room-highlight-color: #343a46;
|
||||||
@ -63,6 +64,8 @@ $input-invalid-border-color: $warning-color;
|
|||||||
|
|
||||||
$field-focused-label-bg-color: $bg-color;
|
$field-focused-label-bg-color: $bg-color;
|
||||||
|
|
||||||
|
$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity.
|
||||||
|
|
||||||
// scrollbars
|
// scrollbars
|
||||||
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
||||||
$scrollbar-track-color: transparent;
|
$scrollbar-track-color: transparent;
|
||||||
@ -110,7 +113,7 @@ $header-divider-color: $header-panel-text-primary-color;
|
|||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
// this probably shouldn't have it's own colour
|
||||||
$voipcall-plinth-color: #21262c;
|
$voipcall-plinth-color: #394049;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
@ -203,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #21262c82;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #8e99a4;
|
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||||
$message-body-panel-fg-color: $primary-fg-color;
|
$message-body-panel-icon-fg-color: #21262C; // "Separator"
|
||||||
|
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
||||||
|
|
||||||
|
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||||
|
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||||
|
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||||
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
|
$voice-record-icon-color: $quaternary-fg-color;
|
||||||
|
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||||
|
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||||
|
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color;
|
|||||||
|
|
||||||
$field-focused-label-bg-color: $bg-color;
|
$field-focused-label-bg-color: $bg-color;
|
||||||
|
|
||||||
|
$resend-button-divider-color: $muted-fg-color;
|
||||||
|
|
||||||
// scrollbars
|
// scrollbars
|
||||||
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
||||||
$scrollbar-track-color: transparent;
|
$scrollbar-track-color: transparent;
|
||||||
@ -107,7 +109,7 @@ $header-divider-color: $header-panel-text-primary-color;
|
|||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
// this probably shouldn't have it's own colour
|
||||||
$voipcall-plinth-color: #f2f5f8;
|
$voipcall-plinth-color: #394049;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
@ -198,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #21262c82;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #8e99a4;
|
$message-body-panel-bg-color: #394049;
|
||||||
$message-body-panel-fg-color: $primary-fg-color;
|
$message-body-panel-icon-fg-color: $primary-bg-color;
|
||||||
|
$message-body-panel-icon-bg-color: $secondary-fg-color;
|
||||||
|
|
||||||
|
// See non-legacy dark for variable information
|
||||||
|
$voice-record-stop-border-color: #6F7882;
|
||||||
|
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||||
|
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||||
|
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||||
|
$voice-record-icon-color: #6F7882;
|
||||||
|
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||||
|
$voice-playback-button-fg-color: #21262C;
|
||||||
|
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color;
|
|||||||
|
|
||||||
$field-focused-label-bg-color: #ffffff;
|
$field-focused-label-bg-color: #ffffff;
|
||||||
|
|
||||||
|
$resend-button-divider-color: $input-darker-bg-color;
|
||||||
|
|
||||||
$button-bg-color: $accent-color;
|
$button-bg-color: $accent-color;
|
||||||
$button-fg-color: white;
|
$button-fg-color: white;
|
||||||
|
|
||||||
@ -174,7 +176,7 @@ $composer-e2e-icon-color: #91a1c0;
|
|||||||
$header-divider-color: #91a1c0;
|
$header-divider-color: #91a1c0;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
// this probably shouldn't have it's own colour
|
||||||
$voipcall-plinth-color: #f2f5f8;
|
$voipcall-plinth-color: #F4F6FA;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
@ -189,13 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
|||||||
|
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
// See non-legacy _light for variable information
|
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
|
||||||
$voice-record-waveform-bg-color: #E3E8F0;
|
|
||||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
|
||||||
$voice-record-live-circle-color: #ff4b55;
|
|
||||||
|
|
||||||
$roomtile-preview-color: #9e9e9e;
|
$roomtile-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
$roomtile-selected-bg-color: #fff;
|
$roomtile-selected-bg-color: #fff;
|
||||||
@ -328,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #e3e8f082;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #ffffff;
|
$message-body-panel-bg-color: #E3E8F0;
|
||||||
$message-body-panel-fg-color: $muted-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
|
$message-body-panel-icon-bg-color: $primary-bg-color;
|
||||||
|
|
||||||
|
// 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-bg-color: $message-body-panel-bg-color;
|
||||||
|
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||||
|
$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;
|
||||||
|
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
|||||||
$primary-fg-color: #2e2f32;
|
$primary-fg-color: #2e2f32;
|
||||||
$secondary-fg-color: #737D8C;
|
$secondary-fg-color: #737D8C;
|
||||||
$tertiary-fg-color: #8D99A5;
|
$tertiary-fg-color: #8D99A5;
|
||||||
|
$quaternary-fg-color: #C1C6CD;
|
||||||
$header-panel-bg-color: #f3f8fd;
|
$header-panel-bg-color: #f3f8fd;
|
||||||
|
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
@ -91,6 +92,8 @@ $field-focused-label-bg-color: #ffffff;
|
|||||||
$button-bg-color: $accent-color;
|
$button-bg-color: $accent-color;
|
||||||
$button-fg-color: white;
|
$button-fg-color: white;
|
||||||
|
|
||||||
|
$resend-button-divider-color: $input-darker-bg-color;
|
||||||
|
|
||||||
// apart from login forms, which have stronger border
|
// apart from login forms, which have stronger border
|
||||||
$strong-input-border-color: #c7c7c7;
|
$strong-input-border-color: #c7c7c7;
|
||||||
|
|
||||||
@ -165,7 +168,7 @@ $composer-e2e-icon-color: #91A1C0;
|
|||||||
$header-divider-color: #91A1C0;
|
$header-divider-color: #91A1C0;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
// this probably shouldn't have it's own colour
|
||||||
$voipcall-plinth-color: #f2f5f8;
|
$voipcall-plinth-color: #F4F6FA;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
@ -180,12 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
|||||||
|
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
|
||||||
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
|
||||||
$voice-record-waveform-bg-color: #E3E8F0;
|
|
||||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
|
||||||
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
|
||||||
|
|
||||||
$roomtile-preview-color: $secondary-fg-color;
|
$roomtile-preview-color: $secondary-fg-color;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
$roomtile-selected-bg-color: #FFF;
|
$roomtile-selected-bg-color: #FFF;
|
||||||
@ -325,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #e3e8f082;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #ffffff;
|
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||||
$message-body-panel-fg-color: $muted-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
|
$message-body-panel-icon-bg-color: $primary-bg-color;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
|
|
||||||
|
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||||
|
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||||
|
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||||
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
|
$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;
|
||||||
|
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
if (process.argv.length < 4) throw new Error("Missing source and target file arguments");
|
|
||||||
|
|
||||||
const sourceFile = fs.readFileSync(process.argv[2], 'utf8');
|
|
||||||
const targetFile = fs.readFileSync(process.argv[3], 'utf8');
|
|
||||||
|
|
||||||
if (sourceFile !== targetFile) {
|
|
||||||
throw new Error("Files do not match");
|
|
||||||
}
|
|
@ -1,304 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerates the translations en_EN file by walking the source tree and
|
|
||||||
* parsing each file with the appropriate parser. Emits a JSON file with the
|
|
||||||
* translatable strings mapped to themselves in the order they appeared
|
|
||||||
* in the files and grouped by the file they appeared in.
|
|
||||||
*
|
|
||||||
* Usage: node scripts/gen-i18n.js
|
|
||||||
*/
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const walk = require('walk');
|
|
||||||
|
|
||||||
const parser = require("@babel/parser");
|
|
||||||
const traverse = require("@babel/traverse");
|
|
||||||
|
|
||||||
const TRANSLATIONS_FUNCS = ['_t', '_td'];
|
|
||||||
|
|
||||||
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
|
|
||||||
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
|
|
||||||
|
|
||||||
// NB. The sync version of walk is broken for single files so we walk
|
|
||||||
// all of res rather than just res/home.html.
|
|
||||||
// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it,
|
|
||||||
// or if we get bored waiting for it to be merged, we could switch
|
|
||||||
// to a project that's actively maintained.
|
|
||||||
const SEARCH_PATHS = ['src', 'res'];
|
|
||||||
|
|
||||||
function getObjectValue(obj, key) {
|
|
||||||
for (const prop of obj.properties) {
|
|
||||||
if (prop.key.type === 'Identifier' && prop.key.name === key) {
|
|
||||||
return prop.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTKey(arg) {
|
|
||||||
if (arg.type === 'Literal' || arg.type === "StringLiteral") {
|
|
||||||
return arg.value;
|
|
||||||
} else if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
||||||
return getTKey(arg.left) + getTKey(arg.right);
|
|
||||||
} else if (arg.type === 'TemplateLiteral') {
|
|
||||||
return arg.quasis.map((q) => {
|
|
||||||
return q.value.raw;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormatStrings(str) {
|
|
||||||
// Match anything that starts with %
|
|
||||||
// We could make a regex that matched the full placeholder, but this
|
|
||||||
// would just not match invalid placeholders and so wouldn't help us
|
|
||||||
// detect the invalid ones.
|
|
||||||
// Also note that for simplicity, this just matches a % character and then
|
|
||||||
// anything up to the next % character (or a single %, or end of string).
|
|
||||||
const formatStringRe = /%([^%]+|%|$)/g;
|
|
||||||
const formatStrings = new Set();
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ( (match = formatStringRe.exec(str)) !== null ) {
|
|
||||||
const placeholder = match[1]; // Minus the leading '%'
|
|
||||||
if (placeholder === '%') continue; // Literal % is %%
|
|
||||||
|
|
||||||
const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/);
|
|
||||||
if (placeholderMatch === null) {
|
|
||||||
throw new Error("Invalid format specifier: '"+match[0]+"'");
|
|
||||||
}
|
|
||||||
if (placeholderMatch.length < 3) {
|
|
||||||
throw new Error("Malformed format specifier");
|
|
||||||
}
|
|
||||||
const placeholderName = placeholderMatch[1];
|
|
||||||
const placeholderFormat = placeholderMatch[2];
|
|
||||||
|
|
||||||
if (placeholderFormat !== 's') {
|
|
||||||
throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatStrings.add(placeholderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatStrings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTranslationsJs(file) {
|
|
||||||
const contents = fs.readFileSync(file, { encoding: 'utf8' });
|
|
||||||
|
|
||||||
const trs = new Set();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const plugins = [
|
|
||||||
// https://babeljs.io/docs/en/babel-parser#plugins
|
|
||||||
"classProperties",
|
|
||||||
"objectRestSpread",
|
|
||||||
"throwExpressions",
|
|
||||||
"exportDefaultFrom",
|
|
||||||
"decorators-legacy",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (file.endsWith(".js") || file.endsWith(".jsx")) {
|
|
||||||
// all JS is assumed to be flow or react
|
|
||||||
plugins.push("flow", "jsx");
|
|
||||||
} else if (file.endsWith(".ts")) {
|
|
||||||
// TS can't use JSX unless it's a TSX file (otherwise angle casts fail)
|
|
||||||
plugins.push("typescript");
|
|
||||||
} else if (file.endsWith(".tsx")) {
|
|
||||||
// When the file is a TSX file though, enable JSX parsing
|
|
||||||
plugins.push("typescript", "jsx");
|
|
||||||
}
|
|
||||||
|
|
||||||
const babelParsed = parser.parse(contents, {
|
|
||||||
allowImportExportEverywhere: true,
|
|
||||||
errorRecovery: true,
|
|
||||||
sourceFilename: file,
|
|
||||||
tokens: true,
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
traverse.default(babelParsed, {
|
|
||||||
enter: (p) => {
|
|
||||||
const node = p.node;
|
|
||||||
if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) {
|
|
||||||
const tKey = getTKey(node.arguments[0]);
|
|
||||||
|
|
||||||
// This happens whenever we call _t with non-literals (ie. whenever we've
|
|
||||||
// had to use a _td to compensate) so is expected.
|
|
||||||
if (tKey === null) return;
|
|
||||||
|
|
||||||
// check the format string against the args
|
|
||||||
// We only check _t: _td has no args
|
|
||||||
if (node.callee.name === '_t') {
|
|
||||||
try {
|
|
||||||
const placeholders = getFormatStrings(tKey);
|
|
||||||
for (const placeholder of placeholders) {
|
|
||||||
if (node.arguments.length < 2) {
|
|
||||||
throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`);
|
|
||||||
}
|
|
||||||
const value = getObjectValue(node.arguments[1], placeholder);
|
|
||||||
if (value === null) {
|
|
||||||
throw new Error(`No value found for placeholder '${placeholder}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate tag replacements
|
|
||||||
if (node.arguments.length > 2) {
|
|
||||||
const tagMap = node.arguments[2];
|
|
||||||
for (const prop of tagMap.properties || []) {
|
|
||||||
if (prop.key.type === 'Literal') {
|
|
||||||
const tag = prop.key.value;
|
|
||||||
// RegExp same as in src/languageHandler.js
|
|
||||||
const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
|
|
||||||
if (!tKey.match(regexp)) {
|
|
||||||
throw new Error(`No match for ${regexp} in ${tKey}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log();
|
|
||||||
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isPlural = false;
|
|
||||||
if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') {
|
|
||||||
const countVal = getObjectValue(node.arguments[1], 'count');
|
|
||||||
if (countVal) {
|
|
||||||
isPlural = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlural) {
|
|
||||||
trs.add(tKey + "|other");
|
|
||||||
const plurals = enPlurals[tKey];
|
|
||||||
if (plurals) {
|
|
||||||
for (const pluralType of Object.keys(plurals)) {
|
|
||||||
trs.add(tKey + "|" + pluralType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trs.add(tKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTranslationsOther(file) {
|
|
||||||
const contents = fs.readFileSync(file, { encoding: 'utf8' });
|
|
||||||
|
|
||||||
const trs = new Set();
|
|
||||||
|
|
||||||
// Taken from element-web src/components/structures/HomePage.js
|
|
||||||
const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg;
|
|
||||||
let matches;
|
|
||||||
while (matches = translationsRegex.exec(contents)) {
|
|
||||||
trs.add(matches[1]);
|
|
||||||
}
|
|
||||||
return trs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// gather en_EN plural strings from the input translations file:
|
|
||||||
// the en_EN strings are all in the source with the exception of
|
|
||||||
// pluralised strings, which we need to pull in from elsewhere.
|
|
||||||
const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' }));
|
|
||||||
const enPlurals = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(inputTranslationsRaw)) {
|
|
||||||
const parts = key.split("|");
|
|
||||||
if (parts.length > 1) {
|
|
||||||
const plurals = enPlurals[parts[0]] || {};
|
|
||||||
plurals[parts[1]] = inputTranslationsRaw[key];
|
|
||||||
enPlurals[parts[0]] = plurals;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const translatables = new Set();
|
|
||||||
|
|
||||||
const walkOpts = {
|
|
||||||
listeners: {
|
|
||||||
names: function(root, nodeNamesArray) {
|
|
||||||
// Sort the names case insensitively and alphabetically to
|
|
||||||
// maintain some sense of order between the different strings.
|
|
||||||
nodeNamesArray.sort((a, b) => {
|
|
||||||
a = a.toLowerCase();
|
|
||||||
b = b.toLowerCase();
|
|
||||||
if (a > b) return 1;
|
|
||||||
if (a < b) return -1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
file: function(root, fileStats, next) {
|
|
||||||
const fullPath = path.join(root, fileStats.name);
|
|
||||||
|
|
||||||
let trs;
|
|
||||||
if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) {
|
|
||||||
trs = getTranslationsJs(fullPath);
|
|
||||||
} else if (fileStats.name.endsWith('.html')) {
|
|
||||||
trs = getTranslationsOther(fullPath);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`${fullPath} (${trs.size} strings)`);
|
|
||||||
for (const tr of trs.values()) {
|
|
||||||
// Convert DOS line endings to unix
|
|
||||||
translatables.add(tr.replace(/\r\n/g, "\n"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const path of SEARCH_PATHS) {
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
walk.walkSync(path, walkOpts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const trObj = {};
|
|
||||||
for (const tr of translatables) {
|
|
||||||
if (tr.includes("|")) {
|
|
||||||
if (inputTranslationsRaw[tr]) {
|
|
||||||
trObj[tr] = inputTranslationsRaw[tr];
|
|
||||||
} else {
|
|
||||||
trObj[tr] = tr.split("|")[0];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trObj[tr] = tr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
OUTPUT_FILE,
|
|
||||||
JSON.stringify(trObj, translatables.values(), 4) + "\n"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`);
|
|
@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Looks through all the translation files and removes any strings
|
|
||||||
* which don't appear in en_EN.json.
|
|
||||||
* Use this if you remove a translation, but merge any outstanding changes
|
|
||||||
* from weblate first or you'll need to resolve the conflict in weblate.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const I18NDIR = 'src/i18n/strings';
|
|
||||||
|
|
||||||
const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json')));
|
|
||||||
|
|
||||||
const enStrings = new Set();
|
|
||||||
for (const str of Object.keys(enStringsRaw)) {
|
|
||||||
const parts = str.split('|');
|
|
||||||
if (parts.length > 1) {
|
|
||||||
enStrings.add(parts[0]);
|
|
||||||
} else {
|
|
||||||
enStrings.add(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filename of fs.readdirSync(I18NDIR)) {
|
|
||||||
if (filename === 'en_EN.json') continue;
|
|
||||||
if (filename === 'basefile.json') continue;
|
|
||||||
if (!filename.endsWith('.json')) continue;
|
|
||||||
|
|
||||||
const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename)));
|
|
||||||
const oldLen = Object.keys(trs).length;
|
|
||||||
for (const tr of Object.keys(trs)) {
|
|
||||||
const parts = tr.split('|');
|
|
||||||
const trKey = parts.length > 1 ? parts[0] : tr;
|
|
||||||
if (!enStrings.has(trKey)) {
|
|
||||||
delete trs[tr];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removed = oldLen - Object.keys(trs).length;
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(`${filename}: removed ${removed} translations`);
|
|
||||||
// XXX: This is totally relying on the impl serialising the JSON object in the
|
|
||||||
// same order as they were parsed from the file. JSON.stringify() has a specific argument
|
|
||||||
// that can be used to control the order, but JSON.parse() lacks any kind of equivalent.
|
|
||||||
// Empirically this does maintain the order on my system, so I'm going to leave it like
|
|
||||||
// this for now.
|
|
||||||
fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n");
|
|
||||||
}
|
|
||||||
}
|
|
22
src/@types/global.d.ts
vendored
22
src/@types/global.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -39,7 +39,9 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
|||||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
import VoipUserMapper from "../VoipUserMapper";
|
import VoipUserMapper from "../VoipUserMapper";
|
||||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||||
import {VoiceRecording} from "../voice/VoiceRecording";
|
import TypingStore from "../stores/TypingStore";
|
||||||
|
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||||
|
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -71,12 +73,16 @@ declare global {
|
|||||||
mxModalWidgetStore: ModalWidgetStore;
|
mxModalWidgetStore: ModalWidgetStore;
|
||||||
mxVoipUserMapper: VoipUserMapper;
|
mxVoipUserMapper: VoipUserMapper;
|
||||||
mxSpaceStore: SpaceStoreClass;
|
mxSpaceStore: SpaceStoreClass;
|
||||||
mxVoiceRecorder: typeof VoiceRecording;
|
mxVoiceRecordingStore: VoiceRecordingStore;
|
||||||
|
mxTypingStore: TypingStore;
|
||||||
|
mxEventIndexPeg: EventIndexPeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
||||||
hasStorageAccess?: () => Promise<boolean>;
|
hasStorageAccess?: () => Promise<boolean>;
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess
|
||||||
|
requestStorageAccess?: () => Promise<undefined>;
|
||||||
|
|
||||||
// Safari & IE11 only have this prefixed: we used prefixed versions
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
// previously so let's continue to support them for now
|
// previously so let's continue to support them for now
|
||||||
@ -112,6 +118,16 @@ declare global {
|
|||||||
|
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
type?: string;
|
type?: string;
|
||||||
|
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||||
|
sinkId: string;
|
||||||
|
setSinkId(outputId: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HTMLVideoElement {
|
||||||
|
type?: string;
|
||||||
|
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||||
|
sinkId: string;
|
||||||
|
setSinkId(outputId: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Element {
|
interface Element {
|
||||||
|
@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
|||||||
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import {mediaFromMxc} from "./customisations/Media";
|
import {mediaFromMxc} from "./customisations/Media";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
export type ResizeMethod = "crop" | "scale";
|
||||||
|
|
||||||
@ -27,11 +28,7 @@ export type ResizeMethod = "crop" | "scale";
|
|||||||
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (member?.getMxcAvatarUrl()) {
|
if (member?.getMxcAvatarUrl()) {
|
||||||
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
Math.floor(width * window.devicePixelRatio),
|
|
||||||
Math.floor(height * window.devicePixelRatio),
|
|
||||||
resizeMethod,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!url) {
|
if (!url) {
|
||||||
// member can be null here currently since on invites, the JS SDK
|
// member can be null here currently since on invites, the JS SDK
|
||||||
@ -44,11 +41,7 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
|
|||||||
|
|
||||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
if (!user.avatarUrl) return null;
|
if (!user.avatarUrl) return null;
|
||||||
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
|
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
Math.floor(width * window.devicePixelRatio),
|
|
||||||
Math.floor(height * window.devicePixelRatio),
|
|
||||||
resizeMethod,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidHexColor(color: string): boolean {
|
function isValidHexColor(color: string): boolean {
|
||||||
@ -151,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// space rooms cannot be DMs so skip the rest
|
// space rooms cannot be DMs so skip the rest
|
||||||
if (room.isSpaceRoom()) return null;
|
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null;
|
||||||
|
|
||||||
let otherMember = null;
|
let otherMember = null;
|
||||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
@ -258,7 +258,7 @@ export default abstract class BasePlatform {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLanguage(preferredLangs: string[]) {}
|
async setLanguage(preferredLangs: string[]) {}
|
||||||
|
|
||||||
setSpellCheckLanguages(preferredLangs: string[]) {}
|
setSpellCheckLanguages(preferredLangs: string[]) {}
|
||||||
|
|
||||||
|
@ -59,7 +59,6 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
@ -86,6 +85,9 @@ import { Action } from './dispatcher/actions';
|
|||||||
import VoipUserMapper from './VoipUserMapper';
|
import VoipUserMapper from './VoipUserMapper';
|
||||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||||
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
|
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
import SdkConfig from './SdkConfig';
|
||||||
|
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||||
|
|
||||||
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
||||||
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
||||||
@ -137,22 +139,12 @@ export enum PlaceCallType {
|
|||||||
ScreenSharing = 'screensharing',
|
ScreenSharing = 'screensharing',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteAudioElement(): HTMLAudioElement {
|
export enum CallHandlerEvent {
|
||||||
// this needs to be somewhere at the top of the DOM which
|
CallsChanged = "calls_changed",
|
||||||
// always exists to avoid audio interruptions.
|
CallChangeRoom = "call_change_room",
|
||||||
// Might as well just use DOM.
|
|
||||||
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
|
|
||||||
if (!remoteAudioElement) {
|
|
||||||
console.error(
|
|
||||||
"Failed to find remoteAudio element - cannot play audio!" +
|
|
||||||
"You need to add an <audio/> to the DOM.",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return remoteAudioElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler extends EventEmitter {
|
||||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||||
// call with a different party to this one.
|
// call with a different party to this one.
|
||||||
@ -167,6 +159,11 @@ export default class CallHandler {
|
|||||||
private invitedRoomsAreVirtual = new Map<string, boolean>();
|
private invitedRoomsAreVirtual = new Map<string, boolean>();
|
||||||
private invitedRoomCheckInProgress = false;
|
private invitedRoomCheckInProgress = false;
|
||||||
|
|
||||||
|
// Map of the asserted identity users after we've looked them up using the API.
|
||||||
|
// We need to be be able to determine the mapped room synchronously, so we
|
||||||
|
// do the async lookup when we get new information and then store these mappings here
|
||||||
|
private assertedIdentityNativeUsers = new Map<string, string>();
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxCallHandler) {
|
||||||
window.mxCallHandler = new CallHandler()
|
window.mxCallHandler = new CallHandler()
|
||||||
@ -179,8 +176,19 @@ export default class CallHandler {
|
|||||||
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
|
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
|
||||||
* if a voip_mxid_translate_pattern is set in the config)
|
* if a voip_mxid_translate_pattern is set in the config)
|
||||||
*/
|
*/
|
||||||
public static roomIdForCall(call: MatrixCall): string {
|
public roomIdForCall(call: MatrixCall): string {
|
||||||
if (!call) return null;
|
if (!call) return null;
|
||||||
|
|
||||||
|
const voipConfig = SdkConfig.get()['voip'];
|
||||||
|
|
||||||
|
if (voipConfig && voipConfig.obeyAssertedIdentity) {
|
||||||
|
const nativeUser = this.assertedIdentityNativeUsers[call.callId];
|
||||||
|
if (nativeUser) {
|
||||||
|
const room = findDMForUser(MatrixClientPeg.get(), nativeUser);
|
||||||
|
if (room) return room.roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
|
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,14 +387,14 @@ export default class CallHandler {
|
|||||||
// We don't allow placing more than one call per room, but that doesn't mean there
|
// We don't allow placing more than one call per room, but that doesn't mean there
|
||||||
// can't be more than one, eg. in a glare situation. This checks that the given call
|
// can't be more than one, eg. in a glare situation. This checks that the given call
|
||||||
// is the call we consider 'the' call for its room.
|
// is the call we consider 'the' call for its room.
|
||||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
const mappedRoomId = this.roomIdForCall(call);
|
||||||
|
|
||||||
const callForThisRoom = this.getCallForRoom(mappedRoomId);
|
const callForThisRoom = this.getCallForRoom(mappedRoomId);
|
||||||
return callForThisRoom && call.callId === callForThisRoom.callId;
|
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallListeners(call: MatrixCall) {
|
private setCallListeners(call: MatrixCall) {
|
||||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
|
|
||||||
call.on(CallEvent.Error, (err: CallError) => {
|
call.on(CallEvent.Error, (err: CallError) => {
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
@ -497,9 +505,43 @@ export default class CallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.calls.set(mappedRoomId, newCall);
|
this.calls.set(mappedRoomId, newCall);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(newCall);
|
this.setCallListeners(newCall);
|
||||||
this.setCallState(newCall, newCall.state);
|
this.setCallState(newCall, newCall.state);
|
||||||
});
|
});
|
||||||
|
call.on(CallEvent.AssertedIdentityChanged, async () => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
|
||||||
|
|
||||||
|
const newAssertedIdentity = call.getRemoteAssertedIdentity().id;
|
||||||
|
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||||
|
if (newAssertedIdentity) {
|
||||||
|
const response = await this.sipNativeLookup(newAssertedIdentity);
|
||||||
|
if (response.length) newNativeAssertedIdentity = response[0].userid;
|
||||||
|
}
|
||||||
|
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
||||||
|
|
||||||
|
if (newNativeAssertedIdentity) {
|
||||||
|
this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity;
|
||||||
|
|
||||||
|
// If we don't already have a room with this user, make one. This will be slightly odd
|
||||||
|
// if they called us because we'll be inviting them, but there's not much we can do about
|
||||||
|
// this if we want the actual, native room to exist (which we do). This is why it's
|
||||||
|
// important to only obey asserted identity in trusted environments, since anyone you're
|
||||||
|
// on a call with can cause you to send a room invite to someone.
|
||||||
|
await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity);
|
||||||
|
|
||||||
|
const newMappedRoomId = this.roomIdForCall(call);
|
||||||
|
console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
|
||||||
|
if (newMappedRoomId !== mappedRoomId) {
|
||||||
|
this.removeCallForRoom(mappedRoomId);
|
||||||
|
mappedRoomId = newMappedRoomId;
|
||||||
|
this.calls.set(mappedRoomId, call);
|
||||||
|
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
|
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
|
||||||
@ -545,13 +587,8 @@ export default class CallHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallAudioElement(call: MatrixCall) {
|
|
||||||
const audioElement = getRemoteAudioElement();
|
|
||||||
if (audioElement) call.setRemoteAudioElement(audioElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCallState(call: MatrixCall, status: CallState) {
|
private setCallState(call: MatrixCall, status: CallState) {
|
||||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||||
@ -566,6 +603,7 @@ export default class CallHandler {
|
|||||||
|
|
||||||
private removeCallForRoom(roomId: string) {
|
private removeCallForRoom(roomId: string) {
|
||||||
this.calls.delete(roomId);
|
this.calls.delete(roomId);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showICEFallbackPrompt() {
|
private showICEFallbackPrompt() {
|
||||||
@ -626,11 +664,7 @@ export default class CallHandler {
|
|||||||
}, null, true);
|
}, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async placeCall(
|
private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
|
||||||
roomId: string, type: PlaceCallType,
|
|
||||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
|
||||||
transferee: MatrixCall,
|
|
||||||
) {
|
|
||||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||||
|
|
||||||
@ -639,25 +673,22 @@ export default class CallHandler {
|
|||||||
|
|
||||||
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
|
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
|
||||||
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||||
|
|
||||||
this.calls.set(roomId, call);
|
this.calls.set(roomId, call);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
if (transferee) {
|
if (transferee) {
|
||||||
this.transferees[call.callId] = transferee;
|
this.transferees[call.callId] = transferee;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
this.setCallAudioElement(call);
|
|
||||||
|
|
||||||
this.setActiveCallRoomId(roomId);
|
this.setActiveCallRoomId(roomId);
|
||||||
|
|
||||||
if (type === PlaceCallType.Voice) {
|
if (type === PlaceCallType.Voice) {
|
||||||
call.placeVoiceCall();
|
call.placeVoiceCall();
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
call.placeVideoCall(
|
call.placeVideoCall();
|
||||||
remoteElement,
|
|
||||||
localElement,
|
|
||||||
);
|
|
||||||
} else if (type === PlaceCallType.ScreenSharing) {
|
} else if (type === PlaceCallType.ScreenSharing) {
|
||||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
if (screenCapErrorString) {
|
if (screenCapErrorString) {
|
||||||
@ -671,13 +702,12 @@ export default class CallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
call.placeScreenSharingCall(
|
call.placeScreenSharingCall(
|
||||||
remoteElement,
|
|
||||||
localElement,
|
|
||||||
async (): Promise<DesktopCapturerSource> => {
|
async (): Promise<DesktopCapturerSource> => {
|
||||||
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
return source;
|
return source;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("Unknown conf call type: " + type);
|
console.error("Unknown conf call type: " + type);
|
||||||
}
|
}
|
||||||
@ -734,17 +764,12 @@ export default class CallHandler {
|
|||||||
} else if (members.length === 2) {
|
} else if (members.length === 2) {
|
||||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||||
|
|
||||||
this.placeCall(
|
this.placeCall(payload.room_id, payload.type, payload.transferee);
|
||||||
payload.room_id, payload.type, payload.local_element, payload.remote_element,
|
|
||||||
payload.transferee,
|
|
||||||
);
|
|
||||||
} else { // > 2
|
} else { // > 2
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "place_conference_call",
|
action: "place_conference_call",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
remote_element: payload.remote_element,
|
|
||||||
local_element: payload.local_element,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -772,7 +797,7 @@ export default class CallHandler {
|
|||||||
|
|
||||||
const call = payload.call as MatrixCall;
|
const call = payload.call as MatrixCall;
|
||||||
|
|
||||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
if (this.getCallForRoom(mappedRoomId)) {
|
if (this.getCallForRoom(mappedRoomId)) {
|
||||||
// ignore multiple incoming calls to the same room
|
// ignore multiple incoming calls to the same room
|
||||||
return;
|
return;
|
||||||
@ -780,6 +805,7 @@ export default class CallHandler {
|
|||||||
|
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
this.calls.set(mappedRoomId, call)
|
this.calls.set(mappedRoomId, call)
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
|
||||||
// get ready to send encrypted events in the room, so if the user does answer
|
// get ready to send encrypted events in the room, so if the user does answer
|
||||||
@ -822,7 +848,6 @@ export default class CallHandler {
|
|||||||
|
|
||||||
const call = this.calls.get(payload.room_id);
|
const call = this.calls.get(payload.room_id);
|
||||||
call.answer();
|
call.answer();
|
||||||
this.setCallAudioElement(call);
|
|
||||||
this.setActiveCallRoomId(payload.room_id);
|
this.setActiveCallRoomId(payload.room_id);
|
||||||
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
import {SettingLevel} from "./settings/SettingLevel";
|
||||||
import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
hasAnyLabeledDevices: async function() {
|
hasAnyLabeledDevices: async function() {
|
||||||
@ -50,18 +50,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadDevices: function() {
|
loadDevices: function() {
|
||||||
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
|
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
setMatrixCallAudioOutput(audioOutDeviceId);
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
setMatrixCallAudioInput(audioDeviceId);
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
setMatrixCallVideoInput(videoDeviceId);
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioOutput: function(deviceId) {
|
setAudioOutput: function(deviceId) {
|
||||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallAudioOutput(deviceId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
setAudioInput: function(deviceId) {
|
||||||
|
@ -148,13 +148,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
|||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Failed to add the following room to the group',
|
'Failed to add the following room to the group',
|
||||||
'', ErrorDialog,
|
'',
|
||||||
|
ErrorDialog,
|
||||||
{
|
{
|
||||||
title: _t(
|
title: _t(
|
||||||
"Failed to add the following rooms to %(groupId)s:",
|
"Failed to add the following rooms to %(groupId)s:",
|
||||||
{groupId},
|
{groupId},
|
||||||
),
|
),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -231,8 +231,10 @@ export class KeyBindingsManager {
|
|||||||
/**
|
/**
|
||||||
* Finds a matching KeyAction for a given KeyboardEvent
|
* Finds a matching KeyAction for a given KeyboardEvent
|
||||||
*/
|
*/
|
||||||
private getAction<T extends string>(getters: KeyBindingGetter<T>[], ev: KeyboardEvent | React.KeyboardEvent)
|
private getAction<T extends string>(
|
||||||
: T | undefined {
|
getters: KeyBindingGetter<T>[],
|
||||||
|
ev: KeyboardEvent | React.KeyboardEvent,
|
||||||
|
): T | undefined {
|
||||||
for (const getter of getters) {
|
for (const getter of getters) {
|
||||||
const bindings = getter();
|
const bindings = getter();
|
||||||
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
|
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow;
|
|||||||
// TODO: Move this to JS SDK
|
// TODO: Move this to JS SDK
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface ILoginParams {
|
interface ILoginParams {
|
||||||
identifier?: string;
|
identifier?: object;
|
||||||
password?: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
|
@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
|||||||
|
|
||||||
export default class Resend {
|
export default class Resend {
|
||||||
static resendUnsentEvents(room) {
|
static resendUnsentEvents(room) {
|
||||||
room.getPendingEvents().filter(function(ev) {
|
return Promise.all(room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).forEach(function(event) {
|
}).map(function(event) {
|
||||||
Resend.resend(event);
|
return Resend.resend(event);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelUnsentEvents(room) {
|
static cancelUnsentEvents(room) {
|
||||||
@ -38,7 +38,7 @@ export default class Resend {
|
|||||||
|
|
||||||
static resend(event) {
|
static resend(event) {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event,
|
event: event,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -17,13 +16,14 @@ limitations under the License.
|
|||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import request from "browser-request";
|
import request from "browser-request";
|
||||||
|
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
import {WidgetType} from "./widgets/WidgetType";
|
||||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
// The version of the integration manager API we're intending to work with
|
// The version of the integration manager API we're intending to work with
|
||||||
const imApiVersion = "1.1";
|
const imApiVersion = "1.1";
|
||||||
@ -31,9 +31,11 @@ const imApiVersion = "1.1";
|
|||||||
// TODO: Generify the name of this class and all components within - it's not just for Scalar.
|
// TODO: Generify the name of this class and all components within - it's not just for Scalar.
|
||||||
|
|
||||||
export default class ScalarAuthClient {
|
export default class ScalarAuthClient {
|
||||||
constructor(apiUrl, uiUrl) {
|
private scalarToken: string;
|
||||||
this.apiUrl = apiUrl;
|
private termsInteractionCallback: TermsInteractionCallback;
|
||||||
this.uiUrl = uiUrl;
|
private isDefaultManager: boolean;
|
||||||
|
|
||||||
|
constructor(private apiUrl: string, private uiUrl: string) {
|
||||||
this.scalarToken = null;
|
this.scalarToken = null;
|
||||||
// `undefined` to allow `startTermsFlow` to fallback to a default
|
// `undefined` to allow `startTermsFlow` to fallback to a default
|
||||||
// callback if this is unset.
|
// callback if this is unset.
|
||||||
@ -46,7 +48,7 @@ export default class ScalarAuthClient {
|
|||||||
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
|
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeTokenToStore() {
|
private writeTokenToStore() {
|
||||||
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
|
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
|
||||||
if (this.isDefaultManager) {
|
if (this.isDefaultManager) {
|
||||||
// We remove the old token from storage to migrate upwards. This is safe
|
// We remove the old token from storage to migrate upwards. This is safe
|
||||||
@ -56,7 +58,7 @@ export default class ScalarAuthClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_readTokenFromStore() {
|
private readTokenFromStore(): string {
|
||||||
let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl);
|
let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl);
|
||||||
if (!token && this.isDefaultManager) {
|
if (!token && this.isDefaultManager) {
|
||||||
token = window.localStorage.getItem("mx_scalar_token");
|
token = window.localStorage.getItem("mx_scalar_token");
|
||||||
@ -64,33 +66,33 @@ export default class ScalarAuthClient {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
_readToken() {
|
private readToken(): string {
|
||||||
if (this.scalarToken) return this.scalarToken;
|
if (this.scalarToken) return this.scalarToken;
|
||||||
return this._readTokenFromStore();
|
return this.readTokenFromStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTermsInteractionCallback(callback) {
|
setTermsInteractionCallback(callback) {
|
||||||
this.termsInteractionCallback = callback;
|
this.termsInteractionCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect(): Promise<void> {
|
||||||
return this.getScalarToken().then((tok) => {
|
return this.getScalarToken().then((tok) => {
|
||||||
this.scalarToken = tok;
|
this.scalarToken = tok;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasCredentials() {
|
hasCredentials(): boolean {
|
||||||
return this.scalarToken != null; // undef or null
|
return this.scalarToken != null; // undef or null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that resolves to a scalar_token string
|
// Returns a promise that resolves to a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken(): Promise<string> {
|
||||||
const token = this._readToken();
|
const token = this.readToken();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return this.registerForToken();
|
return this.registerForToken();
|
||||||
} else {
|
} else {
|
||||||
return this._checkToken(token).catch((e) => {
|
return this.checkToken(token).catch((e) => {
|
||||||
if (e instanceof TermsNotSignedError) {
|
if (e instanceof TermsNotSignedError) {
|
||||||
// retrying won't help this
|
// retrying won't help this
|
||||||
throw e;
|
throw e;
|
||||||
@ -100,7 +102,7 @@ export default class ScalarAuthClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getAccountName(token) {
|
private getAccountName(token: string): Promise<string> {
|
||||||
const url = this.apiUrl + "/account";
|
const url = this.apiUrl + "/account";
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
@ -125,8 +127,8 @@ export default class ScalarAuthClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkToken(token) {
|
private checkToken(token: string): Promise<string> {
|
||||||
return this._getAccountName(token).then(userId => {
|
return this.getAccountName(token).then(userId => {
|
||||||
const me = MatrixClientPeg.get().getUserId();
|
const me = MatrixClientPeg.get().getUserId();
|
||||||
if (userId !== me) {
|
if (userId !== me) {
|
||||||
throw new Error("Scalar token is owned by someone else: " + me);
|
throw new Error("Scalar token is owned by someone else: " + me);
|
||||||
@ -154,7 +156,7 @@ export default class ScalarAuthClient {
|
|||||||
parsedImRestUrl.pathname = '';
|
parsedImRestUrl.pathname = '';
|
||||||
return startTermsFlow([new Service(
|
return startTermsFlow([new Service(
|
||||||
SERVICE_TYPES.IM,
|
SERVICE_TYPES.IM,
|
||||||
parsedImRestUrl.format(),
|
url.format(parsedImRestUrl),
|
||||||
token,
|
token,
|
||||||
)], this.termsInteractionCallback).then(() => {
|
)], this.termsInteractionCallback).then(() => {
|
||||||
return token;
|
return token;
|
||||||
@ -165,22 +167,22 @@ export default class ScalarAuthClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerForToken() {
|
registerForToken(): Promise<string> {
|
||||||
// Get openid bearer token from the HS as the first part of our dance
|
// Get openid bearer token from the HS as the first part of our dance
|
||||||
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
|
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
|
||||||
// Now we can send that to scalar and exchange it for a scalar token
|
// Now we can send that to scalar and exchange it for a scalar token
|
||||||
return this.exchangeForScalarToken(tokenObject);
|
return this.exchangeForScalarToken(tokenObject);
|
||||||
}).then((token) => {
|
}).then((token) => {
|
||||||
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
|
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
|
||||||
return this._checkToken(token);
|
return this.checkToken(token);
|
||||||
}).then((token) => {
|
}).then((token) => {
|
||||||
this.scalarToken = token;
|
this.scalarToken = token;
|
||||||
this._writeTokenToStore();
|
this.writeTokenToStore();
|
||||||
return token;
|
return token;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeForScalarToken(openidTokenObject) {
|
exchangeForScalarToken(openidTokenObject: any): Promise<string> {
|
||||||
const scalarRestUrl = this.apiUrl;
|
const scalarRestUrl = this.apiUrl;
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
@ -194,7 +196,7 @@ export default class ScalarAuthClient {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (response.statusCode / 100 !== 2) {
|
} else if (response.statusCode / 100 !== 2) {
|
||||||
reject({statusCode: response.statusCode});
|
reject(new Error(`Scalar request failed: ${response.statusCode}`));
|
||||||
} else if (!body || !body.scalar_token) {
|
} else if (!body || !body.scalar_token) {
|
||||||
reject(new Error("Missing scalar_token in response"));
|
reject(new Error("Missing scalar_token in response"));
|
||||||
} else {
|
} else {
|
||||||
@ -204,7 +206,7 @@ export default class ScalarAuthClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarPageTitle(url) {
|
getScalarPageTitle(url: string): Promise<string> {
|
||||||
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
|
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
|
||||||
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
|
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
|
||||||
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
|
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
|
||||||
@ -218,7 +220,7 @@ export default class ScalarAuthClient {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (response.statusCode / 100 !== 2) {
|
} else if (response.statusCode / 100 !== 2) {
|
||||||
reject({statusCode: response.statusCode});
|
reject(new Error(`Scalar request failed: ${response.statusCode}`));
|
||||||
} else if (!body) {
|
} else if (!body) {
|
||||||
reject(new Error("Missing page title in response"));
|
reject(new Error("Missing page title in response"));
|
||||||
} else {
|
} else {
|
||||||
@ -240,10 +242,10 @@ export default class ScalarAuthClient {
|
|||||||
* @param {string} widgetId The widget ID to disable assets for
|
* @param {string} widgetId The widget ID to disable assets for
|
||||||
* @return {Promise} Resolves on completion
|
* @return {Promise} Resolves on completion
|
||||||
*/
|
*/
|
||||||
disableWidgetAssets(widgetType: WidgetType, widgetId) {
|
disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise<void> {
|
||||||
let url = this.apiUrl + '/widgets/set_assets_state';
|
let url = this.apiUrl + '/widgets/set_assets_state';
|
||||||
url = this.getStarterLink(url);
|
url = this.getStarterLink(url);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
request({
|
request({
|
||||||
method: 'GET', // XXX: Actions shouldn't be GET requests
|
method: 'GET', // XXX: Actions shouldn't be GET requests
|
||||||
uri: url,
|
uri: url,
|
||||||
@ -257,7 +259,7 @@ export default class ScalarAuthClient {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (response.statusCode / 100 !== 2) {
|
} else if (response.statusCode / 100 !== 2) {
|
||||||
reject({statusCode: response.statusCode});
|
reject(new Error(`Scalar request failed: ${response.statusCode}`));
|
||||||
} else if (!body) {
|
} else if (!body) {
|
||||||
reject(new Error("Failed to set widget assets state"));
|
reject(new Error("Failed to set widget assets state"));
|
||||||
} else {
|
} else {
|
||||||
@ -267,7 +269,7 @@ export default class ScalarAuthClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(room, screen, id) {
|
getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string {
|
||||||
const roomId = room.roomId;
|
const roomId = room.roomId;
|
||||||
const roomName = room.name;
|
const roomName = room.name;
|
||||||
let url = this.uiUrl;
|
let url = this.uiUrl;
|
||||||
@ -284,7 +286,7 @@ export default class ScalarAuthClient {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStarterLink(starterLinkUrl) {
|
getStarterLink(starterLinkUrl: string): string {
|
||||||
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -38,7 +38,7 @@ import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
|
|||||||
import {inviteUsersToRoom} from "./RoomInvite";
|
import {inviteUsersToRoom} from "./RoomInvite";
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { parseFragment as parseHtml } from "parse5";
|
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||||
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
||||||
import { ensureDMExists } from "./createRoom";
|
import { ensureDMExists } from "./createRoom";
|
||||||
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
||||||
@ -856,7 +856,7 @@ export const Commands = [
|
|||||||
// some superfast regex over the text so we don't have to.
|
// some superfast regex over the text so we don't have to.
|
||||||
const embed = parseHtml(widgetUrl);
|
const embed = parseHtml(widgetUrl);
|
||||||
if (embed && embed.childNodes && embed.childNodes.length === 1) {
|
if (embed && embed.childNodes && embed.childNodes.length === 1) {
|
||||||
const iframe = embed.childNodes[0];
|
const iframe = embed.childNodes[0] as ChildElement;
|
||||||
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
|
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
|
||||||
const srcAttr = iframe.attrs.find(a => a.name === 'src');
|
const srcAttr = iframe.attrs.find(a => a.name === 'src');
|
||||||
console.log("Pulling URL out of iframe (embed code)");
|
console.log("Pulling URL out of iframe (embed code)");
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import * as sdk from './';
|
import * as sdk from '.';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
export class TermsNotSignedError extends Error {}
|
export class TermsNotSignedError extends Error {}
|
||||||
@ -32,13 +32,30 @@ export class Service {
|
|||||||
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
||||||
* @param {string} accessToken The user's access token for the service
|
* @param {string} accessToken The user's access token for the service
|
||||||
*/
|
*/
|
||||||
constructor(serviceType, baseUrl, accessToken) {
|
constructor(public serviceType: string, public baseUrl: string, public accessToken: string) {
|
||||||
this.serviceType = serviceType;
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.accessToken = accessToken;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Policy {
|
||||||
|
// @ts-ignore: No great way to express indexed types together with other keys
|
||||||
|
version: string;
|
||||||
|
[lang: string]: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
type Policies = {
|
||||||
|
[policy: string]: Policy,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TermsInteractionCallback = (
|
||||||
|
policiesAndServicePairs: {
|
||||||
|
service: Service,
|
||||||
|
policies: Policies,
|
||||||
|
}[],
|
||||||
|
agreedUrls: string[],
|
||||||
|
extraClassNames?: string,
|
||||||
|
) => Promise<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a flow where the user is presented with terms & conditions for some services
|
* Start a flow where the user is presented with terms & conditions for some services
|
||||||
*
|
*
|
||||||
@ -51,8 +68,8 @@ export class Service {
|
|||||||
* if they cancel.
|
* if they cancel.
|
||||||
*/
|
*/
|
||||||
export async function startTermsFlow(
|
export async function startTermsFlow(
|
||||||
services,
|
services: Service[],
|
||||||
interactionCallback = dialogTermsInteractionCallback,
|
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
|
||||||
) {
|
) {
|
||||||
const termsPromises = services.map(
|
const termsPromises = services.map(
|
||||||
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
|
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
|
||||||
@ -77,7 +94,7 @@ export async function startTermsFlow(
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const terms = await Promise.all(termsPromises);
|
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
|
||||||
const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; });
|
const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; });
|
||||||
|
|
||||||
// fetch the set of agreed policy URLs from account data
|
// fetch the set of agreed policy URLs from account data
|
||||||
@ -158,10 +175,13 @@ export async function startTermsFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function dialogTermsInteractionCallback(
|
export function dialogTermsInteractionCallback(
|
||||||
policiesAndServicePairs,
|
policiesAndServicePairs: {
|
||||||
agreedUrls,
|
service: Service,
|
||||||
extraClassNames,
|
policies: { [policy: string]: Policy },
|
||||||
) {
|
}[],
|
||||||
|
agreedUrls: string[],
|
||||||
|
extraClassNames?: string,
|
||||||
|
): Promise<string[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log("Terms that need agreement", policiesAndServicePairs);
|
console.log("Terms that need agreement", policiesAndServicePairs);
|
||||||
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
@ -547,17 +547,23 @@ function textForMjolnirEvent(event) {
|
|||||||
|
|
||||||
// else the entity !== prevEntity - count as a removal & add
|
// else the entity !== prevEntity - count as a removal & add
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
return _t(
|
||||||
|
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
|
);
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
return _t(
|
||||||
|
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
|
);
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
return _t(
|
||||||
|
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
|
@ -45,7 +45,7 @@ export function eventTriggersUnreadCount(ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function doesRoomHaveUnreadMessages(room) {
|
export function doesRoomHaveUnreadMessages(room) {
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
// get the most recent read receipt sent by our account.
|
// get the most recent read receipt sent by our account.
|
||||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||||
|
@ -57,7 +57,11 @@ export default class VoipUserMapper {
|
|||||||
if (!virtualRoom) return null;
|
if (!virtualRoom) return null;
|
||||||
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
|
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
|
||||||
if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
|
if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
|
||||||
return virtualRoomEvent.getContent()['native_room'] || null;
|
const nativeRoomID = virtualRoomEvent.getContent()['native_room'];
|
||||||
|
const nativeRoom = MatrixClientPeg.get().getRoom(nativeRoomID);
|
||||||
|
if (!nativeRoom || nativeRoom.getMyMembership() !== 'join') return null;
|
||||||
|
|
||||||
|
return nativeRoomID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isVirtualRoom(room: Room): boolean {
|
public isVirtualRoom(room: Room): boolean {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import SdkConfig from '../../../../SdkConfig';
|
import SdkConfig from '../../../../SdkConfig';
|
||||||
import SettingsStore from "../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../settings/SettingsStore";
|
||||||
@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils";
|
|||||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||||
import {SettingLevel} from "../../../../settings/SettingLevel";
|
import {SettingLevel} from "../../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (confirmed: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
eventIndexSize: number;
|
||||||
|
eventCount: number;
|
||||||
|
crawlingRoomsCount: number;
|
||||||
|
roomCount: number;
|
||||||
|
currentRoom: string;
|
||||||
|
crawlerSleepTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allows the user to introspect the event index state and disable it.
|
* Allows the user to introspect the event index state and disable it.
|
||||||
*/
|
*/
|
||||||
export default class ManageEventIndexDialog extends React.Component {
|
export default class ManageEventIndexDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount(): void {
|
async componentDidMount(): Promise<void> {
|
||||||
let eventIndexSize = 0;
|
let eventIndexSize = 0;
|
||||||
let crawlingRoomsCount = 0;
|
let crawlingRoomsCount = 0;
|
||||||
let roomCount = 0;
|
let roomCount = 0;
|
||||||
@ -123,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisable = async () => {
|
private onDisable = async () => {
|
||||||
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
||||||
import("./DisableEventIndexDialog"),
|
import("./DisableEventIndexDialog"),
|
||||||
null, null, /* priority = */ false, /* static = */ true,
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCrawlerSleepTimeChange = (e) => {
|
private onCrawlerSleepTimeChange = (e) => {
|
||||||
this.setState({crawlerSleepTime: e.target.value});
|
this.setState({crawlerSleepTime: e.target.value});
|
||||||
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
|
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
|
||||||
};
|
};
|
||||||
@ -169,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component {
|
|||||||
label={_t('Message downloading sleep time(ms)')}
|
label={_t('Message downloading sleep time(ms)')}
|
||||||
type='number'
|
type='number'
|
||||||
value={this.state.crawlerSleepTime}
|
value={this.state.crawlerSleepTime}
|
||||||
onChange={this._onCrawlerSleepTimeChange} />
|
onChange={this.onCrawlerSleepTimeChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -188,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component {
|
|||||||
onPrimaryButtonClick={this.props.onFinished}
|
onPrimaryButtonClick={this.props.onFinished}
|
||||||
primaryButtonClass="primary"
|
primaryButtonClass="primary"
|
||||||
cancelButton={_t("Disable")}
|
cancelButton={_t("Disable")}
|
||||||
onCancel={this._onDisable}
|
onCancel={this.onDisable}
|
||||||
cancelButtonClass="danger"
|
cancelButtonClass="danger"
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Please enter your Security Phrase a second time to confirm.",
|
"Enter your Security Phrase a second time to confirm it.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||||
|
@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Enter your recovery passphrase a second time to confirm it.",
|
"Enter your Security Phrase a second time to confirm it.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<Field
|
<Field
|
||||||
@ -655,7 +655,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||||||
onChange={this._onPassPhraseConfirmChange}
|
onChange={this._onPassPhraseConfirmChange}
|
||||||
value={this.state.passPhraseConfirm}
|
value={this.state.passPhraseConfirm}
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
label={_t("Confirm your recovery passphrase")}
|
label={_t("Confirm your Security Phrase")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
@ -170,7 +170,10 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_Dialog_buttons'>
|
<div className='mx_Dialog_buttons'>
|
||||||
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
|
<input
|
||||||
|
className='mx_Dialog_primary'
|
||||||
|
type='submit'
|
||||||
|
value={_t('Export')}
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
|
@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component {
|
|||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Failed to add the following room to the group summary',
|
'Failed to add the following room to the group summary',
|
||||||
'', ErrorDialog,
|
'',
|
||||||
|
ErrorDialog,
|
||||||
{
|
{
|
||||||
title: _t(
|
title: _t(
|
||||||
"Failed to add the following rooms to the summary of %(groupId)s:",
|
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||||
{groupId: this.props.groupId},
|
{groupId: this.props.groupId},
|
||||||
),
|
),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||||
@ -196,7 +198,8 @@ class FeaturedRoom extends React.Component {
|
|||||||
{groupId: this.props.groupId},
|
{groupId: this.props.groupId},
|
||||||
),
|
),
|
||||||
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -289,7 +292,8 @@ class RoleUserList extends React.Component {
|
|||||||
{groupId: this.props.groupId},
|
{groupId: this.props.groupId},
|
||||||
),
|
),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||||
@ -352,14 +356,16 @@ class FeaturedUser extends React.Component {
|
|||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Failed to remove user from community summary',
|
'Failed to remove user from community summary',
|
||||||
'', ErrorDialog,
|
'',
|
||||||
|
ErrorDialog,
|
||||||
{
|
{
|
||||||
title: _t(
|
title: _t(
|
||||||
"Failed to remove a user from the summary of %(groupId)s",
|
"Failed to remove a user from the summary of %(groupId)s",
|
||||||
{groupId: this.props.groupId},
|
{groupId: this.props.groupId},
|
||||||
),
|
),
|
||||||
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1055,7 +1061,8 @@ export default class GroupView extends React.Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const membershipButtonClasses = classnames([
|
const membershipButtonClasses = classnames(
|
||||||
|
[
|
||||||
'mx_RoomHeader_textButton',
|
'mx_RoomHeader_textButton',
|
||||||
'mx_GroupView_textButton',
|
'mx_GroupView_textButton',
|
||||||
],
|
],
|
||||||
|
@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
if (element) {
|
if (element) {
|
||||||
classes = element.classList;
|
classes = element.classList;
|
||||||
}
|
}
|
||||||
} while (element && !cssClasses.some(c => classes.contains(c)));
|
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
const roomList = <RoomList
|
const roomList = <RoomList
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
resizeNotifier={null}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
|||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||||
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
@ -119,6 +122,7 @@ interface IState {
|
|||||||
usageLimitEventContent?: IUsageLimit;
|
usageLimitEventContent?: IUsageLimit;
|
||||||
usageLimitEventTs?: number;
|
usageLimitEventTs?: number;
|
||||||
useCompactLayout: boolean;
|
useCompactLayout: boolean;
|
||||||
|
activeCalls: Array<MatrixCall>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||||
usageLimitDismissed: false,
|
usageLimitDismissed: false,
|
||||||
|
activeCalls: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
|
|
||||||
this._updateServerNoticeEvents();
|
this._updateServerNoticeEvents();
|
||||||
|
|
||||||
@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
@ -206,6 +213,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onCallsChanged = () => {
|
||||||
|
this.setState({
|
||||||
|
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Child components assume that the client peg will not be null, so give them some
|
// Child components assume that the client peg will not be null, so give them some
|
||||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||||
//
|
//
|
||||||
@ -661,6 +674,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||||
|
return (
|
||||||
|
<AudioFeedArrayForCall call={call} key={call.callId} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
<div
|
<div
|
||||||
@ -685,6 +704,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
<HostSignupContainer />
|
<HostSignupContainer />
|
||||||
|
{audioFeedArraysForCalls}
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1094,7 +1094,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
private leaveRoomWarnings(roomId: string) {
|
private leaveRoomWarnings(roomId: string) {
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const isSpace = roomToLeave?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||||
// Show a warning if there are additional complications.
|
// Show a warning if there are additional complications.
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
@ -1133,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const warnings = this.leaveRoomWarnings(roomId);
|
const warnings = this.leaveRoomWarnings(roomId);
|
||||||
|
|
||||||
const isSpace = roomToLeave?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||||
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
||||||
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
|
@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component {
|
|||||||
// we get a new DOM node (restarting the animation) when the ghost
|
// we get a new DOM node (restarting the animation) when the ghost
|
||||||
// moves to a different event.
|
// moves to a different event.
|
||||||
return (
|
return (
|
||||||
<li key={"_readuptoghost_"+eventId}
|
<li
|
||||||
className="mx_RoomView_myReadMarker_container">
|
key={"_readuptoghost_"+eventId}
|
||||||
|
className="mx_RoomView_myReadMarker_container"
|
||||||
|
>
|
||||||
{ hr }
|
{ hr }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
|
|||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
@replaceableComponent("structures.RightPanel")
|
@replaceableComponent("structures.RightPanel")
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component {
|
||||||
@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
|
|||||||
return RightPanelPhases.GroupMemberList;
|
return RightPanelPhases.GroupMemberList;
|
||||||
}
|
}
|
||||||
return rps.groupPanelPhase;
|
return rps.groupPanelPhase;
|
||||||
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
|
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
|
||||||
|
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
|
||||||
|
) {
|
||||||
return RightPanelPhases.SpaceMemberList;
|
return RightPanelPhases.SpaceMemberList;
|
||||||
} else if (userForPanel) {
|
} else if (userForPanel) {
|
||||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { createRef } from "react";
|
import { createRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
@ -26,7 +28,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
|||||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
@ -40,6 +42,7 @@ interface IProps {
|
|||||||
interface IState {
|
interface IState {
|
||||||
query: string;
|
query: string;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
|
inSpaces: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomSearch")
|
@replaceableComponent("structures.RoomSearch")
|
||||||
@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
query: "",
|
query: "",
|
||||||
focused: false,
|
focused: false,
|
||||||
|
inSpaces: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||||
|
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||||
@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||||
|
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onSpaces = (spaces: Room[]) => {
|
||||||
|
this.setState({
|
||||||
|
inSpaces: spaces.length > 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === 'view_room' && payload.clear_search) {
|
if (payload.action === 'view_room' && payload.clear_search) {
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||||||
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let placeholder = _t("Filter");
|
||||||
|
if (this.state.inSpaces) {
|
||||||
|
placeholder = _t("Filter all spaces");
|
||||||
|
}
|
||||||
|
|
||||||
let icon = (
|
let icon = (
|
||||||
<div className='mx_RoomSearch_icon' />
|
<div className='mx_RoomSearch_icon' />
|
||||||
);
|
);
|
||||||
@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
placeholder={_t("Filter")}
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015-2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -20,16 +20,20 @@ import { _t, _td } from '../../languageHandler';
|
|||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
import {messageForResourceLimitError} from '../../utils/ErrorUtils';
|
||||||
import {Action} from "../../dispatcher/actions";
|
import {Action} from "../../dispatcher/actions";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import {EventStatus} from "matrix-js-sdk/src/models/event";
|
import {EventStatus} from "matrix-js-sdk/src/models/event";
|
||||||
|
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||||
|
import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState";
|
||||||
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
function getUnsentMessages(room) {
|
export function getUnsentMessages(room) {
|
||||||
if (!room) { return []; }
|
if (!room) { return []; }
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
@ -76,6 +80,7 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
||||||
unsentMessages: getUnsentMessages(this.props.room),
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
|
isResending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -109,7 +114,10 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_onResendAllClick = () => {
|
_onResendAllClick = () => {
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room).then(() => {
|
||||||
|
this.setState({isResending: false});
|
||||||
|
});
|
||||||
|
this.setState({isResending: true});
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,9 +128,10 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
|
|
||||||
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
||||||
if (room.roomId !== this.props.room.roomId) return;
|
if (room.roomId !== this.props.room.roomId) return;
|
||||||
|
const messages = getUnsentMessages(this.props.room);
|
||||||
this.setState({
|
this.setState({
|
||||||
unsentMessages: getUnsentMessages(this.props.room),
|
unsentMessages: messages,
|
||||||
|
isResending: messages.length > 0 && this.state.isResending,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,7 +150,7 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
_getSize() {
|
_getSize() {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this._shouldShowConnectionError()) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.state.unsentMessages.length > 0) {
|
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
@ -162,7 +171,6 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
|
|
||||||
_getUnsentMessageContent() {
|
_getUnsentMessageContent() {
|
||||||
const unsentMessages = this.state.unsentMessages;
|
const unsentMessages = this.state.unsentMessages;
|
||||||
if (!unsentMessages.length) return null;
|
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
|
||||||
@ -192,7 +200,8 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
} else if (resourceLimitError) {
|
} else if (resourceLimitError) {
|
||||||
title = messageForResourceLimitError(
|
title = messageForResourceLimitError(
|
||||||
resourceLimitError.data.limit_type,
|
resourceLimitError.data.limit_type,
|
||||||
resourceLimitError.data.admin_contact, {
|
resourceLimitError.data.admin_contact,
|
||||||
|
{
|
||||||
'monthly_active_user': _td(
|
'monthly_active_user': _td(
|
||||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
@ -205,48 +214,60 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
),
|
),
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
unsentMessages.length === 1 &&
|
|
||||||
unsentMessages[0].error &&
|
|
||||||
unsentMessages[0].error.data &&
|
|
||||||
unsentMessages[0].error.data.error
|
|
||||||
) {
|
|
||||||
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
|
|
||||||
} else {
|
|
||||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> " +
|
|
||||||
"now. You can also select individual messages to resend or cancel.",
|
|
||||||
{ count: unsentMessages.length },
|
|
||||||
{
|
|
||||||
'resendText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
|
||||||
'cancelText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
title = _t('Some of your messages have not been sent');
|
||||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
|
||||||
<div>
|
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
|
||||||
{ title }
|
|
||||||
</div>
|
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
|
||||||
{ content }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
let buttonRow = <>
|
||||||
_getContent() {
|
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
|
{_t("Delete all")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||||
|
{_t("Retry all")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
if (this.state.isResending) {
|
||||||
|
buttonRow = <>
|
||||||
|
<InlineSpinner w={20} h={20} />
|
||||||
|
{/* span for css */}
|
||||||
|
<span>{_t("Sending")}</span>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
||||||
|
<div role="alert">
|
||||||
|
<div className="mx_RoomStatusBar_unsentBadge">
|
||||||
|
<NotificationBadge
|
||||||
|
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mx_RoomStatusBar_unsentTitle">
|
||||||
|
{ title }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomStatusBar_unsentDescription">
|
||||||
|
{ _t("You can select all or individual messages to retry or delete") }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||||
|
{buttonRow}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this._shouldShowConnectionError()) {
|
||||||
return (
|
return (
|
||||||
|
<div className="mx_RoomStatusBar">
|
||||||
|
<div role="alert">
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
|
||||||
|
height="24" title="/!\ " alt="/!\ " />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
{_t('Connectivity to the server has been lost.')}
|
{_t('Connectivity to the server has been lost.')}
|
||||||
@ -256,25 +277,15 @@ export default class RoomStatusBar extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.unsentMessages.length > 0) {
|
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return this._getUnsentMessageContent();
|
return this._getUnsentMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const content = this._getContent();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomStatusBar">
|
|
||||||
<div role="alert">
|
|
||||||
{ content }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -190,6 +190,9 @@ export interface IState {
|
|||||||
rejectError?: Error;
|
rejectError?: Error;
|
||||||
hasPinnedWidgets?: boolean;
|
hasPinnedWidgets?: boolean;
|
||||||
dragCounter: number;
|
dragCounter: number;
|
||||||
|
// whether or not a spaces context switch brought us here,
|
||||||
|
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||||
|
wasContextSwitch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomView")
|
@replaceableComponent("structures.RoomView")
|
||||||
@ -326,6 +329,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||||
@ -1746,7 +1750,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const myMembership = this.state.room.getMyMembership();
|
const myMembership = this.state.room.getMyMembership();
|
||||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
|
if (myMembership === "invite"
|
||||||
|
// SpaceRoomView handles invites itself
|
||||||
|
&& (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
|
||||||
|
) {
|
||||||
if (this.state.joining || this.state.rejecting) {
|
if (this.state.joining || this.state.rejecting) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@ -1888,7 +1895,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
{ previewBar }
|
{ previewBar }
|
||||||
@ -2014,6 +2021,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
showReadReceipts={this.state.showReadReceipts}
|
showReadReceipts={this.state.showReadReceipts}
|
||||||
manageReadReceipts={!this.state.isPeeking}
|
manageReadReceipts={!this.state.isPeeking}
|
||||||
|
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||||
manageReadMarkers={!this.state.isPeeking}
|
manageReadMarkers={!this.state.isPeeking}
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
|
@ -525,7 +525,7 @@ export default class ScrollPanel extends React.Component {
|
|||||||
*/
|
*/
|
||||||
scrollRelative = mult => {
|
scrollRelative = mult => {
|
||||||
const scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
const delta = mult * scrollNode.clientHeight * 0.9;
|
||||||
scrollNode.scrollBy(0, delta);
|
scrollNode.scrollBy(0, delta);
|
||||||
this._saveScrollState();
|
this._saveScrollState();
|
||||||
};
|
};
|
||||||
@ -884,9 +884,13 @@ export default class ScrollPanel extends React.Component {
|
|||||||
|
|
||||||
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
|
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
|
||||||
// list-style-type: none; is no longer a list
|
// list-style-type: none; is no longer a list
|
||||||
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
|
return (
|
||||||
|
<AutoHideScrollbar
|
||||||
|
wrappedRef={this._collectScroll}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
className={`mx_ScrollPanel ${this.props.className}`}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
{ this.props.fixedChildren }
|
{ this.props.fixedChildren }
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useMemo, useState} from "react";
|
import React, {ReactNode, useMemo, useState} from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||||
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
|
|||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import SearchBox from "./SearchBox";
|
import SearchBox from "./SearchBox";
|
||||||
@ -39,11 +39,14 @@ import {mediaFromMxc} from "../../customisations/Media";
|
|||||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
|
import {getOrder} from "../../stores/SpaceStore";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
refreshToken?: any;
|
refreshToken?: any;
|
||||||
|
additionalButtons?: ReactNode;
|
||||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +109,16 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
const cliRoom = cli.getRoom(room.room_id);
|
const cliRoom = cli.getRoom(room.room_id);
|
||||||
const myMembership = cliRoom?.getMyMembership();
|
const myMembership = cliRoom?.getMyMembership();
|
||||||
|
|
||||||
const onPreviewClick = () => onViewRoomClick(false);
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
const onJoinClick = () => onViewRoomClick(true);
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
onViewRoomClick(false);
|
||||||
|
}
|
||||||
|
const onJoinClick = (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
onViewRoomClick(true);
|
||||||
|
}
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (myMembership === "join") {
|
if (myMembership === "join") {
|
||||||
@ -136,7 +147,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
if (room.avatar_url) {
|
if (room.avatar_url) {
|
||||||
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio));
|
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = _t("%(count)s members", { count: room.num_joined_members });
|
let description = _t("%(count)s members", { count: room.num_joined_members });
|
||||||
@ -254,7 +265,11 @@ export const HierarchyLevel = ({
|
|||||||
const space = cli.getRoom(spaceId);
|
const space = cli.getRoom(spaceId);
|
||||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||||
|
|
||||||
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
|
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||||
|
const sortedChildren = sortBy(children, ev => {
|
||||||
|
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||||
|
return getOrder(ev.content.order, null, ev.state_key);
|
||||||
|
});
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||||
const roomId = ev.state_key;
|
const roomId = ev.state_key;
|
||||||
if (!rooms.has(roomId)) return result;
|
if (!rooms.has(roomId)) return result;
|
||||||
@ -312,11 +327,12 @@ export const HierarchyLevel = ({
|
|||||||
|
|
||||||
// mutate argument refreshToken to force a reload
|
// mutate argument refreshToken to force a reload
|
||||||
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
|
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
|
||||||
|
null,
|
||||||
ISpaceSummaryRoom[],
|
ISpaceSummaryRoom[],
|
||||||
Map<string, Map<string, ISpaceSummaryEvent>>,
|
Map<string, Map<string, ISpaceSummaryEvent>>?,
|
||||||
Map<string, Set<string>>,
|
Map<string, Set<string>>?,
|
||||||
Map<string, Set<string>>,
|
Map<string, Set<string>>?,
|
||||||
] | [] => {
|
] | [Error] => {
|
||||||
// TODO pagination
|
// TODO pagination
|
||||||
return useAsyncMemo(async () => {
|
return useAsyncMemo(async () => {
|
||||||
try {
|
try {
|
||||||
@ -336,13 +352,12 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations];
|
return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e); // TODO
|
console.error(e); // TODO
|
||||||
|
return [e];
|
||||||
}
|
}
|
||||||
|
}, [space, refreshToken], [undefined]);
|
||||||
return [];
|
|
||||||
}, [space, refreshToken], []);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
@ -350,6 +365,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
initialText = "",
|
initialText = "",
|
||||||
showRoom,
|
showRoom,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
additionalButtons,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
@ -358,7 +374,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
|
|
||||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||||
|
|
||||||
const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
|
const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
|
||||||
|
|
||||||
const roomsMap = useMemo(() => {
|
const roomsMap = useMemo(() => {
|
||||||
if (!rooms) return null;
|
if (!rooms) return null;
|
||||||
@ -397,6 +413,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
const [removing, setRemoving] = useState(false);
|
const [removing, setRemoving] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
if (summaryError) {
|
||||||
|
return <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (roomsMap) {
|
if (roomsMap) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
||||||
@ -411,22 +431,31 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||||
}
|
}
|
||||||
|
|
||||||
let editSection;
|
let manageButtons;
|
||||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||||
});
|
});
|
||||||
|
|
||||||
let buttons;
|
|
||||||
if (selectedRelations.length) {
|
|
||||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabled = removing || saving;
|
const disabled = !selectedRelations.length || removing || saving;
|
||||||
|
|
||||||
buttons = <>
|
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||||
<AccessibleButton
|
let props = {};
|
||||||
|
if (!selectedRelations.length) {
|
||||||
|
Button = AccessibleTooltipButton;
|
||||||
|
props = {
|
||||||
|
tooltip: _t("Select a room below first"),
|
||||||
|
yOffset: -40,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
manageButtons = <>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setRemoving(true);
|
setRemoving(true);
|
||||||
try {
|
try {
|
||||||
@ -444,8 +473,9 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{ removing ? _t("Removing...") : _t("Remove") }
|
{ removing ? _t("Removing...") : _t("Remove") }
|
||||||
</AccessibleButton>
|
</Button>
|
||||||
<AccessibleButton
|
<Button
|
||||||
|
{...props}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
@ -476,15 +506,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
? _t("Saving...")
|
? _t("Saving...")
|
||||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||||
}
|
}
|
||||||
</AccessibleButton>
|
</Button>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
editSection = <span>
|
|
||||||
{ buttons }
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results;
|
let results;
|
||||||
if (roomsMap.size) {
|
if (roomsMap.size) {
|
||||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||||
@ -528,7 +553,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
content = <>
|
content = <>
|
||||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||||
{ countsStr }
|
{ countsStr }
|
||||||
{ editSection }
|
<span>
|
||||||
|
{ additionalButtons }
|
||||||
|
{ manageButtons }
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||||
{ error }
|
{ error }
|
||||||
@ -538,10 +566,8 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
{ children }
|
{ children }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
</>;
|
</>;
|
||||||
} else if (!rooms) {
|
|
||||||
content = <Spinner />;
|
|
||||||
} else {
|
} else {
|
||||||
content = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO loading state/error state
|
// TODO loading state/error state
|
||||||
|
@ -51,6 +51,15 @@ import MemberAvatar from "../views/avatars/MemberAvatar";
|
|||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import FacePile from "../views/elements/FacePile";
|
import FacePile from "../views/elements/FacePile";
|
||||||
|
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||||
|
import {sleep} from "../../utils/promise";
|
||||||
|
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||||
|
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuOption,
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
} from "../views/context_menus/IconizedContextMenu";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
@ -214,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
let contextMenu;
|
||||||
|
if (menuDisplayed) {
|
||||||
|
const rect = handle.current.getBoundingClientRect();
|
||||||
|
contextMenu = <IconizedContextMenu
|
||||||
|
left={rect.left + window.pageXOffset + 0}
|
||||||
|
top={rect.bottom + window.pageYOffset + 8}
|
||||||
|
chevronFace={ChevronFace.None}
|
||||||
|
onFinished={closeMenu}
|
||||||
|
className="mx_RoomTile_contextMenu"
|
||||||
|
compact
|
||||||
|
>
|
||||||
|
<IconizedContextMenuOptionList first>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Create new room")}
|
||||||
|
iconClassName="mx_RoomList_iconPlus"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
if (await showCreateNewRoom(cli, space)) {
|
||||||
|
onNewRoomAdded();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Add existing room")}
|
||||||
|
iconClassName="mx_RoomList_iconHash"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
const [added] = await showAddExistingRooms(cli, space);
|
||||||
|
if (added) {
|
||||||
|
onNewRoomAdded();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<ContextMenuButton
|
||||||
|
kind="primary"
|
||||||
|
inputRef={handle}
|
||||||
|
onClick={openMenu}
|
||||||
|
isExpanded={menuDisplayed}
|
||||||
|
label={_t("Add")}
|
||||||
|
>
|
||||||
|
{ _t("Add") }
|
||||||
|
</ContextMenuButton>
|
||||||
|
{ contextMenu }
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
const SpaceLanding = ({ space }) => {
|
const SpaceLanding = ({ space }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
@ -238,32 +308,20 @@ const SpaceLanding = ({ space }) => {
|
|||||||
|
|
||||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||||
|
|
||||||
let addRoomButtons;
|
let addRoomButton;
|
||||||
if (canAddRooms) {
|
if (canAddRooms) {
|
||||||
addRoomButtons = <React.Fragment>
|
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
|
||||||
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
|
|
||||||
const [added] = await showAddExistingRooms(cli, space);
|
|
||||||
if (added) {
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{ _t("Add existing rooms & spaces") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
|
|
||||||
showCreateNewRoom(cli, space);
|
|
||||||
}}>
|
|
||||||
{ _t("Create a new room") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let settingsButton;
|
let settingsButton;
|
||||||
if (shouldShowSpaceSettings(cli, space)) {
|
if (shouldShowSpaceSettings(cli, space)) {
|
||||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
settingsButton = <AccessibleTooltipButton
|
||||||
|
className="mx_SpaceRoomView_landing_settingsButton"
|
||||||
|
onClick={() => {
|
||||||
showSpaceSettings(cli, space);
|
showSpaceSettings(cli, space);
|
||||||
}}>
|
}}
|
||||||
{ _t("Settings") }
|
title={_t("Settings")}
|
||||||
</AccessibleButton>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMembersClick = () => {
|
const onMembersClick = () => {
|
||||||
@ -290,17 +348,19 @@ const SpaceLanding = ({ space }) => {
|
|||||||
<SpaceInfo space={space} />
|
<SpaceInfo space={space} />
|
||||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
|
{ settingsButton }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomView_landing_topic">
|
<div className="mx_SpaceRoomView_landing_topic">
|
||||||
<RoomTopic room={space} />
|
<RoomTopic room={space} />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
|
||||||
{ addRoomButtons }
|
|
||||||
{ settingsButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
|
<SpaceHierarchy
|
||||||
|
space={space}
|
||||||
|
showRoom={showRoom}
|
||||||
|
refreshToken={refreshToken}
|
||||||
|
additionalButtons={addRoomButton}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -354,7 +414,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||||||
let buttonLabel = _t("Skip for now");
|
let buttonLabel = _t("Skip for now");
|
||||||
if (roomNames.some(name => name.trim())) {
|
if (roomNames.some(name => name.trim())) {
|
||||||
onClick = onNextClick;
|
onClick = onNextClick;
|
||||||
buttonLabel = busy ? _t("Creating rooms...") : _t("Continue")
|
buttonLabel = busy ? _t("Creating rooms...") : _t("Continue");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
@ -376,6 +436,74 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||||
|
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||||
|
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
let onClick = onFinished;
|
||||||
|
let buttonLabel = _t("Skip for now");
|
||||||
|
if (selectedToAdd.size > 0) {
|
||||||
|
onClick = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
|
||||||
|
for (const room of selectedToAdd) {
|
||||||
|
const via = calculateRoomVia(room);
|
||||||
|
try {
|
||||||
|
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||||
|
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
await sleep(e.data.retry_after_ms);
|
||||||
|
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to add rooms to space", e);
|
||||||
|
setError(_t("Failed to add rooms to space"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBusy(false);
|
||||||
|
};
|
||||||
|
buttonLabel = busy ? _t("Adding...") : _t("Add");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<h1>{ _t("What do you want to organise?") }</h1>
|
||||||
|
<div className="mx_SpaceRoomView_description">
|
||||||
|
{ _t("Pick rooms or conversations to add. This is just a space for you, " +
|
||||||
|
"no one will be informed. You can add more later.") }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||||
|
|
||||||
|
<AddExistingToSpace
|
||||||
|
space={space}
|
||||||
|
selected={selectedToAdd}
|
||||||
|
onChange={(checked, room) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedToAdd.add(room);
|
||||||
|
} else {
|
||||||
|
selectedToAdd.delete(room);
|
||||||
|
}
|
||||||
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mx_SpaceRoomView_buttons">
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{ buttonLabel }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const SpaceSetupPublicShare = ({ space, onFinished }) => {
|
const SpaceSetupPublicShare = ({ space, onFinished }) => {
|
||||||
return <div className="mx_SpaceRoomView_publicShare">
|
return <div className="mx_SpaceRoomView_publicShare">
|
||||||
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
|
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
|
||||||
@ -659,7 +787,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||||||
return <SpaceSetupPrivateScope
|
return <SpaceSetupPrivateScope
|
||||||
space={this.props.space}
|
space={this.props.space}
|
||||||
onFinished={(invite: boolean) => {
|
onFinished={(invite: boolean) => {
|
||||||
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
|
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms });
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
case Phase.PrivateInvite:
|
case Phase.PrivateInvite:
|
||||||
@ -675,6 +803,11 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||||||
"You can add more later too, including already existing ones.")}
|
"You can add more later too, including already existing ones.")}
|
||||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||||
/>;
|
/>;
|
||||||
|
case Phase.PrivateExistingRooms:
|
||||||
|
return <SpaceAddExistingRooms
|
||||||
|
space={this.props.space}
|
||||||
|
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ class TimelinePanel extends React.Component {
|
|||||||
showReadReceipts: PropTypes.bool,
|
showReadReceipts: PropTypes.bool,
|
||||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||||
manageReadReceipts: PropTypes.bool,
|
manageReadReceipts: PropTypes.bool,
|
||||||
|
sendReadReceiptOnLoad: PropTypes.bool,
|
||||||
manageReadMarkers: PropTypes.bool,
|
manageReadMarkers: PropTypes.bool,
|
||||||
|
|
||||||
// true to give the component a 'display: none' style.
|
// true to give the component a 'display: none' style.
|
||||||
@ -126,6 +127,7 @@ class TimelinePanel extends React.Component {
|
|||||||
// event tile heights. (See _unpaginateEvents)
|
// event tile heights. (See _unpaginateEvents)
|
||||||
timelineCap: Number.MAX_VALUE,
|
timelineCap: Number.MAX_VALUE,
|
||||||
className: 'mx_RoomView_messagePanel',
|
className: 'mx_RoomView_messagePanel',
|
||||||
|
sendReadReceiptOnLoad: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -785,8 +787,10 @@ class TimelinePanel extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
this._setReadMarker(
|
||||||
lastDisplayedEvent.getTs());
|
lastDisplayedEvent.getId(),
|
||||||
|
lastDisplayedEvent.getTs(),
|
||||||
|
);
|
||||||
|
|
||||||
// the read-marker should become invisible, so that if the user scrolls
|
// the read-marker should become invisible, so that if the user scrolls
|
||||||
// down, they don't see it.
|
// down, they don't see it.
|
||||||
@ -1049,7 +1053,9 @@ class TimelinePanel extends React.Component {
|
|||||||
this._messagePanel.current.scrollToBottom();
|
this._messagePanel.current.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.sendReadReceiptOnLoad) {
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016, 2017, 2018, 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -94,7 +94,7 @@ interface IState {
|
|||||||
// be seeing.
|
// be seeing.
|
||||||
serverIsAlive: boolean;
|
serverIsAlive: boolean;
|
||||||
serverErrorIsFatal: boolean;
|
serverErrorIsFatal: boolean;
|
||||||
serverDeadError: string;
|
serverDeadError?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -95,7 +95,7 @@ interface IState {
|
|||||||
// be seeing.
|
// be seeing.
|
||||||
serverIsAlive: boolean;
|
serverIsAlive: boolean;
|
||||||
serverErrorIsFatal: boolean;
|
serverErrorIsFatal: boolean;
|
||||||
serverDeadError: string;
|
serverDeadError?: ReactNode;
|
||||||
|
|
||||||
// Our matrix client - part of state because we can't render the UI auth
|
// Our matrix client - part of state because we can't render the UI auth
|
||||||
// component without it.
|
// component without it.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -15,14 +15,13 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {_t} from '../../../languageHandler';
|
import {_t} from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {sendLoginRequest} from "../../../Login";
|
import {ISSOFlow, LoginFlow, sendLoginRequest} from "../../../Login";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||||
import SSOButtons from "../../views/elements/SSOButtons";
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
@ -42,26 +41,38 @@ const FLOWS_TO_VIEWS = {
|
|||||||
"m.login.sso": LOGIN_VIEW.SSO,
|
"m.login.sso": LOGIN_VIEW.SSO,
|
||||||
};
|
};
|
||||||
|
|
||||||
@replaceableComponent("structures.auth.SoftLogout")
|
interface IProps {
|
||||||
export default class SoftLogout extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// Query parameters from MatrixChat
|
// Query parameters from MatrixChat
|
||||||
realQueryParams: PropTypes.object, // {loginToken}
|
realQueryParams: {
|
||||||
|
loginToken?: string;
|
||||||
|
};
|
||||||
|
fragmentAfterLogin?: string;
|
||||||
|
|
||||||
// Called when the SSO login completes
|
// Called when the SSO login completes
|
||||||
onTokenLoginCompleted: PropTypes.func,
|
onTokenLoginCompleted: () => void,
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor() {
|
interface IState {
|
||||||
super();
|
loginView: number;
|
||||||
|
keyBackupNeeded: boolean;
|
||||||
|
busy: boolean;
|
||||||
|
password: string;
|
||||||
|
errorText: string;
|
||||||
|
flows: LoginFlow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.auth.SoftLogout")
|
||||||
|
export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loginView: LOGIN_VIEW.LOADING,
|
loginView: LOGIN_VIEW.LOADING,
|
||||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
|
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
|
||||||
|
|
||||||
busy: false,
|
busy: false,
|
||||||
password: "",
|
password: "",
|
||||||
errorText: "",
|
errorText: "",
|
||||||
|
flows: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +83,7 @@ export default class SoftLogout extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._initLogin();
|
this.initLogin();
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli.isCryptoEnabled()) {
|
if (cli.isCryptoEnabled()) {
|
||||||
@ -94,7 +105,7 @@ export default class SoftLogout extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async _initLogin() {
|
private async initLogin() {
|
||||||
const queryParams = this.props.realQueryParams;
|
const queryParams = this.props.realQueryParams;
|
||||||
const hasAllParams = queryParams && queryParams['loginToken'];
|
const hasAllParams = queryParams && queryParams['loginToken'];
|
||||||
if (hasAllParams) {
|
if (hasAllParams) {
|
||||||
@ -189,7 +200,7 @@ export default class SoftLogout extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSignInSection() {
|
private renderSignInSection() {
|
||||||
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
@ -247,7 +258,7 @@ export default class SoftLogout extends React.Component {
|
|||||||
} // else we already have a message and should use it (key backup warning)
|
} // else we already have a message and should use it (key backup warning)
|
||||||
|
|
||||||
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
|
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
|
||||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType);
|
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -289,7 +300,7 @@ export default class SoftLogout extends React.Component {
|
|||||||
|
|
||||||
<h3>{_t("Sign in")}</h3>
|
<h3>{_t("Sign in")}</h3>
|
||||||
<div>
|
<div>
|
||||||
{this._renderSignInSection()}
|
{this.renderSignInSection()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>{_t("Clear personal data")}</h3>
|
<h3>{_t("Clear personal data")}</h3>
|
@ -68,8 +68,8 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
|
|||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
if (props.member.getMxcAvatarUrl()) {
|
if (props.member.getMxcAvatarUrl()) {
|
||||||
imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
props.width,
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
props.height,
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,8 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||||||
let oobAvatar = null;
|
let oobAvatar = null;
|
||||||
if (props.oobData.avatarUrl) {
|
if (props.oobData.avatarUrl) {
|
||||||
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
|
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
props.width,
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
props.height,
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,12 +109,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||||||
private static getRoomAvatarUrl(props: IProps): string {
|
private static getRoomAvatarUrl(props: IProps): string {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
return Avatar.avatarUrlForRoom(
|
return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod);
|
||||||
props.room,
|
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
|
||||||
props.resizeMethod,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomAvatarClick = () => {
|
private onRoomAvatarClick = () => {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -34,7 +32,7 @@ import {MenuItem} from "../../structures/ContextMenu";
|
|||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,21 +96,6 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
onResendClick = () => {
|
|
||||||
Resend.resend(this.props.mxEvent);
|
|
||||||
this.closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
onResendEditClick = () => {
|
|
||||||
Resend.resend(this.props.mxEvent.replacingEvent());
|
|
||||||
this.closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
onResendRedactionClick = () => {
|
|
||||||
Resend.resend(this.props.mxEvent.localRedactionEvent());
|
|
||||||
this.closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
onResendReactionsClick = () => {
|
onResendReactionsClick = () => {
|
||||||
for (const reaction of this._getUnsentReactions()) {
|
for (const reaction of this._getUnsentReactions()) {
|
||||||
Resend.resend(reaction);
|
Resend.resend(reaction);
|
||||||
@ -170,29 +153,6 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancelSendClick = () => {
|
|
||||||
const mxEvent = this.props.mxEvent;
|
|
||||||
const editEvent = mxEvent.replacingEvent();
|
|
||||||
const redactEvent = mxEvent.localRedactionEvent();
|
|
||||||
const pendingReactions = this._getPendingReactions();
|
|
||||||
|
|
||||||
if (editEvent && canCancel(editEvent.status)) {
|
|
||||||
Resend.removeFromQueue(editEvent);
|
|
||||||
}
|
|
||||||
if (redactEvent && canCancel(redactEvent.status)) {
|
|
||||||
Resend.removeFromQueue(redactEvent);
|
|
||||||
}
|
|
||||||
if (pendingReactions.length) {
|
|
||||||
for (const reaction of pendingReactions) {
|
|
||||||
Resend.removeFromQueue(reaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (canCancel(mxEvent.status)) {
|
|
||||||
Resend.removeFromQueue(this.props.mxEvent);
|
|
||||||
}
|
|
||||||
this.closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
onForwardClick = () => {
|
onForwardClick = () => {
|
||||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
@ -285,20 +245,9 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
const me = cli.getUserId();
|
const me = cli.getUserId();
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const eventStatus = mxEvent.status;
|
const eventStatus = mxEvent.status;
|
||||||
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
|
|
||||||
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
|
|
||||||
const unsentReactionsCount = this._getUnsentReactions().length;
|
const unsentReactionsCount = this._getUnsentReactions().length;
|
||||||
const pendingReactionsCount = this._getPendingReactions().length;
|
|
||||||
const allowCancel = canCancel(mxEvent.status) ||
|
|
||||||
canCancel(editStatus) ||
|
|
||||||
canCancel(redactStatus) ||
|
|
||||||
pendingReactionsCount !== 0;
|
|
||||||
let resendButton;
|
|
||||||
let resendEditButton;
|
|
||||||
let resendReactionsButton;
|
let resendReactionsButton;
|
||||||
let resendRedactionButton;
|
|
||||||
let redactButton;
|
let redactButton;
|
||||||
let cancelButton;
|
|
||||||
let forwardButton;
|
let forwardButton;
|
||||||
let pinButton;
|
let pinButton;
|
||||||
let unhidePreviewButton;
|
let unhidePreviewButton;
|
||||||
@ -309,22 +258,6 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
// status is SENT before remote-echo, null after
|
// status is SENT before remote-echo, null after
|
||||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||||
if (!mxEvent.isRedacted()) {
|
if (!mxEvent.isRedacted()) {
|
||||||
if (eventStatus === EventStatus.NOT_SENT) {
|
|
||||||
resendButton = (
|
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
|
||||||
{ _t('Resend') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editStatus === EventStatus.NOT_SENT) {
|
|
||||||
resendEditButton = (
|
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
|
|
||||||
{ _t('Resend edit') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unsentReactionsCount !== 0) {
|
if (unsentReactionsCount !== 0) {
|
||||||
resendReactionsButton = (
|
resendReactionsButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||||
@ -334,14 +267,6 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redactStatus === EventStatus.NOT_SENT) {
|
|
||||||
resendRedactionButton = (
|
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
|
|
||||||
{ _t('Resend removal') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSent && this.state.canRedact) {
|
if (isSent && this.state.canRedact) {
|
||||||
redactButton = (
|
redactButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||||
@ -350,14 +275,6 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowCancel) {
|
|
||||||
cancelButton = (
|
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
|
||||||
{ _t('Cancel Sending') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isContentActionable(mxEvent)) {
|
if (isContentActionable(mxEvent)) {
|
||||||
forwardButton = (
|
forwardButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||||
@ -455,12 +372,8 @@ export default class MessageContextMenu extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageContextMenu">
|
<div className="mx_MessageContextMenu">
|
||||||
{ resendButton }
|
|
||||||
{ resendEditButton }
|
|
||||||
{ resendReactionsButton }
|
{ resendReactionsButton }
|
||||||
{ resendRedactionButton }
|
|
||||||
{ redactButton }
|
{ redactButton }
|
||||||
{ cancelButton }
|
|
||||||
{ forwardButton }
|
{ forwardButton }
|
||||||
{ pinButton }
|
{ pinButton }
|
||||||
{ viewSourceButton }
|
{ viewSourceButton }
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from "react";
|
import React, {useContext, useMemo, useState} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
@ -29,10 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
|||||||
import {getDisplayAliasForRoom} from "../../../Rooms";
|
import {getDisplayAliasForRoom} from "../../../Rooms";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {allSettled} from "../../../utils/promise";
|
import {sleep} from "../../../utils/promise";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
@ -41,45 +44,128 @@ interface IProps extends IDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Entry = ({ room, checked, onChange }) => {
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
return <div className="mx_AddExistingToSpaceDialog_entry">
|
return <label className="mx_AddExistingToSpace_entry">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
<span className="mx_AddExistingToSpaceDialog_entry_name">{ room.name }</span>
|
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||||
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
|
<StyledCheckbox
|
||||||
</div>;
|
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||||
|
checked={checked}
|
||||||
|
disabled={!onChange}
|
||||||
|
/>
|
||||||
|
</label>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
interface IAddExistingToSpaceProps {
|
||||||
|
space: Room;
|
||||||
|
selected: Set<Room>;
|
||||||
|
onChange(checked: boolean, room: Room): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase();
|
||||||
|
|
||||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
|
||||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
|
||||||
|
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
const existingSubspacesSet = new Set(existingSubspaces);
|
const existingSubspacesSet = new Set(existingSubspaces);
|
||||||
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
||||||
|
|
||||||
const joinRule = selectedSpace.getJoinRule();
|
const joinRule = space.getJoinRule();
|
||||||
const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => {
|
const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => {
|
||||||
if (room.getMyMembership() !== "join") return arr;
|
if (room.getMyMembership() !== "join") return arr;
|
||||||
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
||||||
|
|
||||||
if (room.isSpaceRoom()) {
|
if (room.isSpaceRoom()) {
|
||||||
if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) {
|
if (room !== space && !existingSubspacesSet.has(room)) {
|
||||||
arr[0].push(room);
|
arr[0].push(room);
|
||||||
}
|
}
|
||||||
} else if (!existingRoomsSet.has(room) && joinRule !== "public") {
|
} else if (!existingRoomsSet.has(room)) {
|
||||||
|
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||||
|
arr[1].push(room);
|
||||||
|
} else if (joinRule !== "public") {
|
||||||
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
||||||
arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room);
|
arr[2].push(room);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}, [[], [], []]);
|
}, [[], [], []]);
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
return <div className="mx_AddExistingToSpace">
|
||||||
const [error, setError] = useState("");
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={ _t("Filter your rooms and spaces") }
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||||
|
{ rooms.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section">
|
||||||
|
<h3>{ _t("Rooms") }</h3>
|
||||||
|
{ rooms.map(room => {
|
||||||
|
return <Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
checked={selected.has(room)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : undefined }
|
||||||
|
|
||||||
|
{ spaces.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||||
|
<h3>{ _t("Spaces") }</h3>
|
||||||
|
{ spaces.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={selected.has(space)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ dms.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section">
|
||||||
|
<h3>{ _t("Direct Messages") }</h3>
|
||||||
|
{ dms.map(room => {
|
||||||
|
return <Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
checked={selected.has(room)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span> : undefined }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||||
|
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||||
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
|
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||||
|
|
||||||
|
const [progress, setProgress] = useState<number>(null);
|
||||||
|
const [error, setError] = useState<Error>(null);
|
||||||
|
|
||||||
let spaceOptionSection;
|
let spaceOptionSection;
|
||||||
if (existingSubspacesSet.size > 0) {
|
if (existingSubspaces.length > 0) {
|
||||||
const options = [space, ...existingSubspaces].map((space) => {
|
const options = [space, ...existingSubspaces].map((space) => {
|
||||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||||
@ -116,116 +202,106 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
|
const addRooms = async () => {
|
||||||
|
setError(null);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
for (const room of selectedToAdd) {
|
||||||
|
const via = calculateRoomVia(room);
|
||||||
|
try {
|
||||||
|
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||||
|
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
await sleep(e.data.retry_after_ms);
|
||||||
|
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
setProgress(i => i + 1);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to add rooms to space", e);
|
||||||
|
setError(error = e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
onFinished(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const busy = progress !== null;
|
||||||
|
|
||||||
|
let footer;
|
||||||
|
if (error) {
|
||||||
|
footer = <>
|
||||||
|
<img
|
||||||
|
src={require("../../../../res/img/element-icons/warning-badge.svg")}
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="mx_AddExistingToSpaceDialog_error">
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div>
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}>
|
||||||
|
{ _t("Retry") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
} else if (busy) {
|
||||||
|
footer = <span>
|
||||||
|
<ProgressBar value={progress} max={selectedToAdd.size} />
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_progressText">
|
||||||
|
{ _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||||
|
count: selectedToAdd.size,
|
||||||
|
progress,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
footer = <>
|
||||||
|
<span>
|
||||||
|
<div>{ _t("Want to add a new room instead?") }</div>
|
||||||
|
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||||
|
{ _t("Create a new room") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||||
|
{ _t("Add") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
title={title}
|
title={title}
|
||||||
className="mx_AddExistingToSpaceDialog"
|
className="mx_AddExistingToSpaceDialog"
|
||||||
contentId="mx_AddExistingToSpaceDialog"
|
contentId="mx_AddExistingToSpace"
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
|
<MatrixClientContext.Provider value={cli}>
|
||||||
|
<AddExistingToSpace
|
||||||
<SearchBox
|
space={space}
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
selected={selectedToAdd}
|
||||||
placeholder={ _t("Filter your rooms and spaces") }
|
onChange={!busy && !error ? (checked, room) => {
|
||||||
onSearch={setQuery}
|
|
||||||
autoComplete={true}
|
|
||||||
/>
|
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
|
|
||||||
{ rooms.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_section">
|
|
||||||
<h3>{ _t("Rooms") }</h3>
|
|
||||||
{ rooms.map(room => {
|
|
||||||
return <Entry
|
|
||||||
key={room.roomId}
|
|
||||||
room={room}
|
|
||||||
checked={selectedToAdd.has(room)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
selectedToAdd.add(room);
|
selectedToAdd.add(room);
|
||||||
} else {
|
} else {
|
||||||
selectedToAdd.delete(room);
|
selectedToAdd.delete(room);
|
||||||
}
|
}
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
}}
|
} : null}
|
||||||
/>;
|
/>
|
||||||
}) }
|
</MatrixClientContext.Provider>
|
||||||
</div>
|
|
||||||
) : undefined }
|
|
||||||
|
|
||||||
{ spaces.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
|
|
||||||
<h3>{ _t("Spaces") }</h3>
|
|
||||||
{ spaces.map(space => {
|
|
||||||
return <Entry
|
|
||||||
key={space.roomId}
|
|
||||||
room={space}
|
|
||||||
checked={selectedToAdd.has(space)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
if (checked) {
|
|
||||||
selectedToAdd.add(space);
|
|
||||||
} else {
|
|
||||||
selectedToAdd.delete(space);
|
|
||||||
}
|
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
|
||||||
}}
|
|
||||||
/>;
|
|
||||||
}) }
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
|
|
||||||
{ dms.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_section">
|
|
||||||
<h3>{ _t("Direct Messages") }</h3>
|
|
||||||
{ dms.map(space => {
|
|
||||||
return <Entry
|
|
||||||
key={space.roomId}
|
|
||||||
room={space}
|
|
||||||
checked={selectedToAdd.has(space)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
if (checked) {
|
|
||||||
selectedToAdd.add(space);
|
|
||||||
} else {
|
|
||||||
selectedToAdd.delete(space);
|
|
||||||
}
|
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
|
||||||
}}
|
|
||||||
/>;
|
|
||||||
}) }
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
|
|
||||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
|
||||||
{ _t("No results") }
|
|
||||||
</span> : undefined }
|
|
||||||
</AutoHideScrollbar>
|
|
||||||
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||||
<span>
|
{ footer }
|
||||||
<div>{ _t("Don't want to add an existing room?") }</div>
|
|
||||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
|
||||||
{ _t("Create a new room") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
disabled={busy || selectedToAdd.size < 1}
|
|
||||||
onClick={async () => {
|
|
||||||
setBusy(true);
|
|
||||||
try {
|
|
||||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
|
||||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
|
||||||
onFinished(true);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to add rooms to space", e);
|
|
||||||
setError(_t("Failed to add rooms to space"));
|
|
||||||
}
|
|
||||||
setBusy(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ busy ? _t("Adding...") : _t("Add") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
||||||
};
|
};
|
||||||
|
@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
|
|||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
|
<BaseDialog
|
||||||
|
className='mx_ConfirmWipeDeviceDialog'
|
||||||
|
hasCancel={true}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Clear all data in this session?")}>
|
title={_t("Clear all data in this session?")}
|
||||||
|
>
|
||||||
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{_t(
|
||||||
|
@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textInput(id, label) {
|
textInput(id, label) {
|
||||||
return <Field id={id} label={label} size="42" autoFocus={true} type="text" autoComplete="on"
|
return <Field
|
||||||
value={this.state[id]} onChange={this._onChange} />;
|
id={id}
|
||||||
|
label={label}
|
||||||
|
size="42"
|
||||||
|
autoFocus={true}
|
||||||
|
type="text"
|
||||||
|
autoComplete="on"
|
||||||
|
value={this.state[id]}
|
||||||
|
onChange={this._onChange}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ export default class IncomingSasDialog extends React.Component {
|
|||||||
const oppProfile = this.state.opponentProfile;
|
const oppProfile = this.state.opponentProfile;
|
||||||
if (oppProfile) {
|
if (oppProfile) {
|
||||||
const url = oppProfile.avatar_url
|
const url = oppProfile.avatar_url
|
||||||
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio))
|
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48)
|
||||||
: null;
|
: null;
|
||||||
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
||||||
<BaseAvatar name={oppProfile.displayname}
|
<BaseAvatar name={oppProfile.displayname}
|
||||||
|
@ -42,9 +42,12 @@ export default class IntegrationsDisabledDialog extends React.Component {
|
|||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
|
<BaseDialog
|
||||||
|
className='mx_IntegrationsDisabledDialog'
|
||||||
|
hasCancel={true}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Integrations are disabled")}>
|
title={_t("Integrations are disabled")}
|
||||||
|
>
|
||||||
<div className='mx_IntegrationsDisabledDialog_content'>
|
<div className='mx_IntegrationsDisabledDialog_content'>
|
||||||
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
|
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
|||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
|
<BaseDialog
|
||||||
|
className='mx_IntegrationsImpossibleDialog'
|
||||||
|
hasCancel={false}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Integrations not allowed")}>
|
title={_t("Integrations not allowed")}
|
||||||
|
>
|
||||||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{_t(
|
||||||
|
@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||||||
goButtonFn = this._startDm;
|
goButtonFn = this._startDm;
|
||||||
} else if (this.props.kind === KIND_INVITE) {
|
} else if (this.props.kind === KIND_INVITE) {
|
||||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||||
const isSpace = room?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||||
title = isSpace
|
title = isSpace
|
||||||
? _t("Invite to %(spaceName)s", {
|
? _t("Invite to %(spaceName)s", {
|
||||||
spaceName: room.name || _t("Unnamed Space"),
|
spaceName: room.name || _t("Unnamed Space"),
|
||||||
|
@ -164,8 +164,12 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
<BaseDialog
|
||||||
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
className='mx_MessageEditHistoryDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Message edits")}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component {
|
|||||||
|
|
||||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
|
<BaseDialog
|
||||||
onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
|
className='mx_RoomSettingsDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||||
|
>
|
||||||
<div className='mx_SettingsDialog_content'>
|
<div className='mx_SettingsDialog_content'>
|
||||||
<TabbedView tabs={this._getTabs()} />
|
<TabbedView tabs={this._getTabs()} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -110,7 +110,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||||
if (stateForError.isFatalError) {
|
if (stateForError.serverErrorIsFatal) {
|
||||||
let error = _t("Unable to validate homeserver");
|
let error = _t("Unable to validate homeserver");
|
||||||
if (e.translatedMessage) {
|
if (e.translatedMessage) {
|
||||||
error = e.translatedMessage;
|
error = e.translatedMessage;
|
||||||
@ -168,7 +168,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||||||
text = _t("Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.");
|
text = _t("Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaultServerName = this.defaultServer.hsName;
|
let defaultServerName: React.ReactNode = this.defaultServer.hsName;
|
||||||
if (this.defaultServer.hsNameIsDifferent) {
|
if (this.defaultServer.hsNameIsDifferent) {
|
||||||
defaultServerName = (
|
defaultServerName = (
|
||||||
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
|
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
|
||||||
|
@ -45,10 +45,12 @@ export default class StorageEvictedDialog extends React.Component {
|
|||||||
let logRequest;
|
let logRequest;
|
||||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||||
logRequest = _t(
|
logRequest = _t(
|
||||||
"To help us prevent this in future, please <a>send us logs</a>.", {},
|
"To help us prevent this in future, please <a>send us logs</a>.",
|
||||||
|
{},
|
||||||
{
|
{
|
||||||
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
|
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -155,8 +155,12 @@ export default class UserSettingsDialog extends React.Component {
|
|||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
|
<BaseDialog
|
||||||
onFinished={this.props.onFinished} title={_t("Settings")}>
|
className='mx_UserSettingsDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Settings")}
|
||||||
|
>
|
||||||
<div className='mx_SettingsDialog_content'>
|
<div className='mx_SettingsDialog_content'>
|
||||||
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
|
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +52,9 @@ export default class VerificationRequestDialog extends React.Component {
|
|||||||
const title = request && request.isSelfVerification ?
|
const title = request && request.isSelfVerification ?
|
||||||
_t("Verify other login") : _t("Verification Request");
|
_t("Verify other login") : _t("Verification Request");
|
||||||
|
|
||||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
return <BaseDialog
|
||||||
|
className="mx_InfoDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
contentId="mx_Dialog_content"
|
contentId="mx_Dialog_content"
|
||||||
title={title}
|
title={title}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
|
@ -70,9 +70,12 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
|
<BaseDialog
|
||||||
|
className='mx_WidgetOpenIDPermissionsDialog'
|
||||||
|
hasCancel={true}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Allow this widget to verify your identity")}>
|
title={_t("Allow this widget to verify your identity")}
|
||||||
|
>
|
||||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||||
|
@ -43,7 +43,8 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
|||||||
className='mx_ConfirmDestroyCrossSigningDialog'
|
className='mx_ConfirmDestroyCrossSigningDialog'
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Destroy cross-signing keys?")}>
|
title={_t("Destroy cross-signing keys?")}
|
||||||
|
>
|
||||||
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
|
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{_t(
|
||||||
|
@ -373,15 +373,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your Security Phrase you can "+
|
"If you've forgotten your Security Phrase you can "+
|
||||||
"<button1>use your Security Key</button1> or " +
|
"<button1>use your Security Key</button1> or " +
|
||||||
"<button2>set up new recovery options</button2>"
|
"<button2>set up new recovery options</button2>",
|
||||||
, {}, {
|
{},
|
||||||
button1: s => <AccessibleButton className="mx_linkButton"
|
{
|
||||||
|
button1: s => <AccessibleButton
|
||||||
|
className="mx_linkButton"
|
||||||
element="span"
|
element="span"
|
||||||
onClick={this._onUseRecoveryKeyClick}
|
onClick={this._onUseRecoveryKeyClick}
|
||||||
>
|
>
|
||||||
{s}
|
{s}
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
button2: s => <AccessibleButton className="mx_linkButton"
|
button2: s => <AccessibleButton
|
||||||
|
className="mx_linkButton"
|
||||||
element="span"
|
element="span"
|
||||||
onClick={this._onResetRecoveryClick}
|
onClick={this._onResetRecoveryClick}
|
||||||
>
|
>
|
||||||
@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your Security Key you can "+
|
"If you've forgotten your Security Key you can "+
|
||||||
"<button>set up new recovery options</button>"
|
"<button>set up new recovery options</button>",
|
||||||
, {}, {
|
{},
|
||||||
|
{
|
||||||
button: s => <AccessibleButton className="mx_linkButton"
|
button: s => <AccessibleButton className="mx_linkButton"
|
||||||
element="span"
|
element="span"
|
||||||
onClick={this._onResetRecoveryClick}
|
onClick={this._onResetRecoveryClick}
|
||||||
>
|
>
|
||||||
{s}
|
{s}
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,12 +65,18 @@ export class EditableItem extends React.Component {
|
|||||||
<span className="mx_EditableItem_promptText">
|
<span className="mx_EditableItem_promptText">
|
||||||
{_t("Are you sure?")}
|
{_t("Are you sure?")}
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
|
<AccessibleButton
|
||||||
className="mx_EditableItem_confirmBtn">
|
onClick={this._onActuallyRemove}
|
||||||
|
kind="primary_sm"
|
||||||
|
className="mx_EditableItem_confirmBtn"
|
||||||
|
>
|
||||||
{_t("Yes")}
|
{_t("Yes")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
|
<AccessibleButton
|
||||||
className="mx_EditableItem_confirmBtn">
|
onClick={this._onDontRemove}
|
||||||
|
kind="danger_sm"
|
||||||
|
className="mx_EditableItem_confirmBtn"
|
||||||
|
>
|
||||||
{_t("No")}
|
{_t("No")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
@ -121,8 +127,12 @@ export default class EditableItemList extends React.Component {
|
|||||||
|
|
||||||
_renderNewItemField() {
|
_renderNewItemField() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this._onItemAdded} autoComplete="off"
|
<form
|
||||||
noValidate={true} className="mx_EditableItemList_newItem">
|
onSubmit={this._onItemAdded}
|
||||||
|
autoComplete="off"
|
||||||
|
noValidate={true}
|
||||||
|
className="mx_EditableItemList_newItem"
|
||||||
|
>
|
||||||
<Field label={this.props.placeholder} type="text"
|
<Field label={this.props.placeholder} type="text"
|
||||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||||
list={this.props.suggestionsListId} />
|
list={this.props.suggestionsListId} />
|
||||||
|
@ -221,13 +221,15 @@ export default class EditableText extends React.Component {
|
|||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
editableEl = <div ref={this._editable_div}
|
editableEl = <div
|
||||||
|
ref={this._editable_div}
|
||||||
contentEditable={true}
|
contentEditable={true}
|
||||||
className={className}
|
className={className}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onKeyUp={this.onKeyUp}
|
onKeyUp={this.onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur} />;
|
onBlur={this.onBlur}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return editableEl;
|
return editableEl;
|
||||||
|
@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
|||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
||||||
|
|
||||||
const MIN_ZOOM = 100;
|
// Max scale to keep gaps around the image
|
||||||
const MAX_ZOOM = 300;
|
const MAX_SCALE = 0.95;
|
||||||
// This is used for the buttons
|
// This is used for the buttons
|
||||||
const ZOOM_STEP = 10;
|
const ZOOM_STEP = 0.10;
|
||||||
// This is used for mouse wheel events
|
// This is used for mouse wheel events
|
||||||
const ZOOM_COEFFICIENT = 0.5;
|
const ZOOM_COEFFICIENT = 0.0025;
|
||||||
// If we have moved only this much we can zoom
|
// If we have moved only this much we can zoom
|
||||||
const ZOOM_DISTANCE = 10;
|
const ZOOM_DISTANCE = 10;
|
||||||
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string, // the source of the image being displayed
|
src: string, // the source of the image being displayed
|
||||||
name?: string, // the main title ('name') for the image
|
name?: string, // the main title ('name') for the image
|
||||||
@ -62,8 +61,10 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
rotation: number,
|
|
||||||
zoom: number,
|
zoom: number,
|
||||||
|
minZoom: number,
|
||||||
|
maxZoom: number,
|
||||||
|
rotation: number,
|
||||||
translationX: number,
|
translationX: number,
|
||||||
translationY: number,
|
translationY: number,
|
||||||
moving: boolean,
|
moving: boolean,
|
||||||
@ -75,8 +76,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
zoom: 0,
|
||||||
|
minZoom: MAX_SCALE,
|
||||||
|
maxZoom: MAX_SCALE,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
zoom: MIN_ZOOM,
|
|
||||||
translationX: 0,
|
translationX: 0,
|
||||||
translationY: 0,
|
translationY: 0,
|
||||||
moving: false,
|
moving: false,
|
||||||
@ -87,6 +90,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
// XXX: Refs to functional components
|
// XXX: Refs to functional components
|
||||||
private contextMenuButton = createRef<any>();
|
private contextMenuButton = createRef<any>();
|
||||||
private focusLock = createRef<any>();
|
private focusLock = createRef<any>();
|
||||||
|
private imageWrapper = createRef<HTMLDivElement>();
|
||||||
|
private image = createRef<HTMLImageElement>();
|
||||||
|
|
||||||
private initX = 0;
|
private initX = 0;
|
||||||
private initY = 0;
|
private initY = 0;
|
||||||
@ -99,12 +104,89 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
// We have to use addEventListener() because the listener
|
// We have to use addEventListener() because the listener
|
||||||
// needs to be passive in order to work with Chromium
|
// needs to be passive in order to work with Chromium
|
||||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||||
|
// We want to recalculate zoom whenever the window's size changes
|
||||||
|
window.addEventListener("resize", this.calculateZoom);
|
||||||
|
// After the image loads for the first time we want to calculate the zoom
|
||||||
|
this.image.current.addEventListener("load", this.calculateZoom);
|
||||||
|
// Try to precalculate the zoom from width and height props
|
||||||
|
this.calculateZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||||
|
window.removeEventListener("resize", this.calculateZoom);
|
||||||
|
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private calculateZoom = () => {
|
||||||
|
const image = this.image.current;
|
||||||
|
const imageWrapper = this.imageWrapper.current;
|
||||||
|
|
||||||
|
const width = this.props.width || image.naturalWidth;
|
||||||
|
const height = this.props.height || image.naturalHeight;
|
||||||
|
|
||||||
|
const zoomX = imageWrapper.clientWidth / width;
|
||||||
|
const zoomY = imageWrapper.clientHeight / height;
|
||||||
|
|
||||||
|
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||||
|
// display it in its original size
|
||||||
|
if (zoomX >= 1 && zoomY >= 1) {
|
||||||
|
this.setState({
|
||||||
|
zoom: 1,
|
||||||
|
minZoom: 1,
|
||||||
|
maxZoom: 1,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We set minZoom to the min of the zoomX and zoomY to avoid overflow in
|
||||||
|
// any direction. We also multiply by MAX_SCALE to get a gap around the
|
||||||
|
// image by default
|
||||||
|
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
||||||
|
|
||||||
|
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
|
||||||
|
this.setState({
|
||||||
|
minZoom: minZoom,
|
||||||
|
maxZoom: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoom(delta: number) {
|
||||||
|
const newZoom = this.state.zoom + delta;
|
||||||
|
|
||||||
|
if (newZoom <= this.state.minZoom) {
|
||||||
|
this.setState({
|
||||||
|
zoom: this.state.minZoom,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newZoom >= this.state.maxZoom) {
|
||||||
|
this.setState({zoom: this.state.maxZoom});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
zoom: newZoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWheel = (ev: WheelEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const {deltaY} = normalizeWheelEvent(ev);
|
||||||
|
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
||||||
|
};
|
||||||
|
|
||||||
|
private onZoomInClick = () => {
|
||||||
|
this.zoom(ZOOM_STEP);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onZoomOutClick = () => {
|
||||||
|
this.zoom(-ZOOM_STEP);
|
||||||
|
};
|
||||||
|
|
||||||
private onKeyDown = (ev: KeyboardEvent) => {
|
private onKeyDown = (ev: KeyboardEvent) => {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@ -113,31 +195,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onWheel = (ev: WheelEvent) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const {deltaY} = normalizeWheelEvent(ev);
|
|
||||||
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
|
|
||||||
|
|
||||||
if (newZoom <= MIN_ZOOM) {
|
|
||||||
this.setState({
|
|
||||||
zoom: MIN_ZOOM,
|
|
||||||
translationX: 0,
|
|
||||||
translationY: 0,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newZoom >= MAX_ZOOM) {
|
|
||||||
this.setState({zoom: MAX_ZOOM});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
zoom: newZoom,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRotateCounterClockwiseClick = () => {
|
private onRotateCounterClockwiseClick = () => {
|
||||||
const cur = this.state.rotation;
|
const cur = this.state.rotation;
|
||||||
const rotationDegrees = cur - 90;
|
const rotationDegrees = cur - 90;
|
||||||
@ -150,31 +207,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
this.setState({ rotation: rotationDegrees });
|
this.setState({ rotation: rotationDegrees });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onZoomInClick = () => {
|
|
||||||
if (this.state.zoom >= MAX_ZOOM) {
|
|
||||||
this.setState({zoom: MAX_ZOOM});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
zoom: this.state.zoom + ZOOM_STEP,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onZoomOutClick = () => {
|
|
||||||
if (this.state.zoom <= MIN_ZOOM) {
|
|
||||||
this.setState({
|
|
||||||
zoom: MIN_ZOOM,
|
|
||||||
translationX: 0,
|
|
||||||
translationY: 0,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
zoom: this.state.zoom - ZOOM_STEP,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDownloadClick = () => {
|
private onDownloadClick = () => {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = this.props.src;
|
a.href = this.props.src;
|
||||||
@ -217,8 +249,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
if (ev.button !== 0) return;
|
if (ev.button !== 0) return;
|
||||||
|
|
||||||
// Zoom in if we are completely zoomed out
|
// Zoom in if we are completely zoomed out
|
||||||
if (this.state.zoom === MIN_ZOOM) {
|
if (this.state.zoom === this.state.minZoom) {
|
||||||
this.setState({zoom: MAX_ZOOM});
|
this.setState({zoom: this.state.maxZoom});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +283,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
||||||
) {
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
zoom: MIN_ZOOM,
|
zoom: this.state.minZoom,
|
||||||
translationX: 0,
|
translationX: 0,
|
||||||
translationY: 0,
|
translationY: 0,
|
||||||
});
|
});
|
||||||
@ -286,17 +318,20 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const showEventMeta = !!this.props.mxEvent;
|
const showEventMeta = !!this.props.mxEvent;
|
||||||
|
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
|
||||||
|
|
||||||
let cursor;
|
let cursor;
|
||||||
if (this.state.moving) {
|
if (this.state.moving) {
|
||||||
cursor= "grabbing";
|
cursor= "grabbing";
|
||||||
} else if (this.state.zoom === MIN_ZOOM) {
|
} else if (zoomingDisabled) {
|
||||||
|
cursor = "default";
|
||||||
|
} else if (this.state.zoom === this.state.minZoom) {
|
||||||
cursor = "zoom-in";
|
cursor = "zoom-in";
|
||||||
} else {
|
} else {
|
||||||
cursor = "zoom-out";
|
cursor = "zoom-out";
|
||||||
}
|
}
|
||||||
const rotationDegrees = this.state.rotation + "deg";
|
const rotationDegrees = this.state.rotation + "deg";
|
||||||
const zoomPercentage = this.state.zoom/100;
|
const zoom = this.state.zoom;
|
||||||
const translatePixelsX = this.state.translationX + "px";
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
const translatePixelsY = this.state.translationY + "px";
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
// The order of the values is important!
|
// The order of the values is important!
|
||||||
@ -308,7 +343,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
transition: this.state.moving ? null : "transform 200ms ease 0s",
|
transition: this.state.moving ? null : "transform 200ms ease 0s",
|
||||||
transform: `translateX(${translatePixelsX})
|
transform: `translateX(${translatePixelsX})
|
||||||
translateY(${translatePixelsY})
|
translateY(${translatePixelsY})
|
||||||
scale(${zoomPercentage})
|
scale(${zoom})
|
||||||
rotate(${rotationDegrees})`,
|
rotate(${rotationDegrees})`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -380,6 +415,25 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let zoomOutButton;
|
||||||
|
let zoomInButton;
|
||||||
|
if (!zoomingDisabled) {
|
||||||
|
zoomOutButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||||
|
title={_t("Zoom out")}
|
||||||
|
onClick={this.onZoomOutClick}>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
);
|
||||||
|
zoomInButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||||
|
title={_t("Zoom in")}
|
||||||
|
onClick={ this.onZoomInClick }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusLock
|
<FocusLock
|
||||||
returnFocus={true}
|
returnFocus={true}
|
||||||
@ -403,16 +457,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
title={_t("Rotate Left")}
|
title={_t("Rotate Left")}
|
||||||
onClick={ this.onRotateCounterClockwiseClick }>
|
onClick={ this.onRotateCounterClockwiseClick }>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
<AccessibleTooltipButton
|
{zoomOutButton}
|
||||||
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
{zoomInButton}
|
||||||
title={_t("Zoom out")}
|
|
||||||
onClick={ this.onZoomOutClick }>
|
|
||||||
</AccessibleTooltipButton>
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
|
||||||
title={_t("Zoom in")}
|
|
||||||
onClick={ this.onZoomInClick }>
|
|
||||||
</AccessibleTooltipButton>
|
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_download"
|
className="mx_ImageView_button mx_ImageView_button_download"
|
||||||
title={_t("Download")}
|
title={_t("Download")}
|
||||||
@ -427,11 +473,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ImageView_image_wrapper">
|
<div
|
||||||
|
className="mx_ImageView_image_wrapper"
|
||||||
|
ref={this.imageWrapper}>
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={this.props.src}
|
||||||
title={this.props.name}
|
title={this.props.name}
|
||||||
style={style}
|
style={style}
|
||||||
|
ref={this.image}
|
||||||
className="mx_ImageView_image"
|
className="mx_ImageView_image"
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onMouseDown={this.onStartMoving}
|
onMouseDown={this.onStartMoving}
|
||||||
|
@ -46,8 +46,12 @@ export default class LabelledToggleSwitch extends React.Component {
|
|||||||
// This is a minimal version of a SettingsFlag
|
// This is a minimal version of a SettingsFlag
|
||||||
|
|
||||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
||||||
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
|
let secondPart = <ToggleSwitch
|
||||||
onChange={this.props.onChange} aria-label={this.props.label} />;
|
checked={this.props.value}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
aria-label={this.props.label}
|
||||||
|
/>;
|
||||||
|
|
||||||
if (this.props.toggleInFront) {
|
if (this.props.toggleInFront) {
|
||||||
const temp = firstPart;
|
const temp = firstPart;
|
||||||
|
@ -136,8 +136,12 @@ export default class PowerSelector extends React.Component {
|
|||||||
picker = (
|
picker = (
|
||||||
<Field type="number"
|
<Field type="number"
|
||||||
label={label} max={this.props.maxValue}
|
label={label} max={this.props.maxValue}
|
||||||
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
onBlur={this.onCustomBlur}
|
||||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
onKeyDown={this.onCustomKeyDown}
|
||||||
|
onChange={this.onCustomChange}
|
||||||
|
value={String(this.state.customValue)}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Each level must have a definition in this.state.levelRoleMap
|
// Each level must have a definition in this.state.levelRoleMap
|
||||||
@ -155,7 +159,8 @@ export default class PowerSelector extends React.Component {
|
|||||||
picker = (
|
picker = (
|
||||||
<Field element="select"
|
<Field element="select"
|
||||||
label={label} onChange={this.onSelectChange}
|
label={label} onChange={this.onSelectChange}
|
||||||
value={String(this.state.selectValue)} disabled={this.props.disabled}>
|
value={String(this.state.selectValue)} disabled={this.props.disabled}
|
||||||
|
>
|
||||||
{options}
|
{options}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
@ -56,7 +56,8 @@ export default class RoomAliasField extends React.PureComponent {
|
|||||||
placeholder={_t("e.g. my-room")}
|
placeholder={_t("e.g. my-room")}
|
||||||
onChange={this._onChange}
|
onChange={this._onChange}
|
||||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||||
maxLength={maxlength} />
|
maxLength={maxlength}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
|
|||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
|
let serverName: React.ReactNode = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
|
||||||
if (serverConfig.hsNameIsDifferent) {
|
if (serverConfig.hsNameIsDifferent) {
|
||||||
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
|
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
|
||||||
{serverConfig.hsName}
|
{serverConfig.hsName}
|
||||||
|
@ -178,9 +178,15 @@ export default class GroupMemberList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inputBox = (
|
const inputBox = (
|
||||||
<input className="mx_GroupMemberList_query mx_textinput" id="mx_GroupMemberList_query" type="text"
|
<input
|
||||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
className="mx_GroupMemberList_query mx_textinput"
|
||||||
placeholder={_t('Filter community members')} autoComplete="off" />
|
id="mx_GroupMemberList_query"
|
||||||
|
type="text"
|
||||||
|
onChange={this.onSearchQueryChanged}
|
||||||
|
value={this.state.searchQuery}
|
||||||
|
placeholder={_t('Filter community members')}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||||
|
@ -141,9 +141,14 @@ export default class GroupRoomList extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const inputBox = (
|
const inputBox = (
|
||||||
<input className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query" type="text"
|
<input
|
||||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query"
|
||||||
placeholder={_t('Filter community rooms')} autoComplete="off" />
|
type="text"
|
||||||
|
onChange={this.onSearchQueryChanged}
|
||||||
|
value={this.state.searchQuery}
|
||||||
|
placeholder={_t('Filter community rooms')}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||||
|
@ -160,7 +160,6 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||||||
"mx_EventTile": true,
|
"mx_EventTile": true,
|
||||||
// Note: we keep the `sending` state class for tests, not for our styles
|
// Note: we keep the `sending` state class for tests, not for our styles
|
||||||
"mx_EventTile_sending": isSending,
|
"mx_EventTile_sending": isSending,
|
||||||
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
|
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user