Merge branch 'develop' into Bubble-bericht

This commit is contained in:
Ayush PS 2021-05-07 18:32:38 +05:30
parent f367d617c5
commit c843387043
264 changed files with 7383 additions and 3268 deletions

View file

@ -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
View file

@ -2,6 +2,7 @@
/*.log /*.log
package-lock.json package-lock.json
/coverage
/node_modules /node_modules
/lib /lib

View file

@ -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)

View file

@ -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`!!**

View file

@ -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"
] ]
} }
} }

View file

@ -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";

View file

@ -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;
} }

View file

@ -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;

View file

@ -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;

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View 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
}

View file

@ -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');
}

View file

@ -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;

View file

@ -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

View file

@ -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
} }
} }

View file

@ -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;
}

View 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');
}
}

View 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
}
}

View file

@ -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;
} }

View file

@ -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 {

View 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

View 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

View 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

View 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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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");
}

View file

@ -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}`);

View 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");
}
}

View file

@ -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 {

View file

@ -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);

View file

@ -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[]) {}

View file

@ -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({

View file

@ -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) {

View file

@ -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(", "),
}); },
);
}); });
} }

View file

@ -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));

View file

@ -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;

View file

@ -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,

View file

@ -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);
} }
} }

View file

@ -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)");

View file

@ -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");

View file

@ -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.

View file

@ -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"),

View file

@ -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 {

View file

@ -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>

View file

@ -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">

View file

@ -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"
/> />

View file

@ -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}>

View file

@ -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',
], ],

View file

@ -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}

View file

@ -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>
); );
} }

View file

@ -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: (

View file

@ -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>
); );

View file

@ -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

View file

@ -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"
/> />
); );

View file

@ -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>
);
}
} }

View file

@ -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}

View file

@ -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">

View file

@ -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

View file

@ -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 })}
/>;
} }
} }

View file

@ -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();
}
}); });
}; };

View file

@ -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;
} }
/* /*

View file

@ -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.

View file

@ -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>

View file

@ -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,
); );
} }

View file

@ -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 = () => {

View file

@ -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 }

View file

@ -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>;
}; };

View file

@ -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(

View file

@ -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}
/>;
} }
} }

View file

@ -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}

View file

@ -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>

View file

@ -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(

View file

@ -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"),

View file

@ -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>
); );

View file

@ -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>

View file

@ -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 its a good place for many."); text = _t("Matrix.org is the biggest public homeserver in the world, so its 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}>

View file

@ -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 (

View file

@ -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>

View file

@ -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}

View file

@ -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:")}

View file

@ -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(

View file

@ -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>;
} }

View file

@ -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} />

View file

@ -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;

View file

@ -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}

View file

@ -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;

View file

@ -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>
); );

View file

@ -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}
/>
); );
} }

View file

@ -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}

View file

@ -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">

View file

@ -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");

View file

@ -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