Merge branch 'develop' into fix/call-search-areas

This commit is contained in:
panoschal 2021-04-16 15:44:06 +03:00 committed by GitHub
commit b5ed1df249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
194 changed files with 6910 additions and 2328 deletions

View file

@ -1,7 +1,7 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. # autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/Markdown.js src/Markdown.js
src/Velociraptor.js src/NodeAnimator.js
src/components/structures/RoomDirectory.js src/components/structures/RoomDirectory.js
src/components/views/rooms/MemberList.js src/components/views/rooms/MemberList.js
src/ratelimitedfunc.js src/ratelimitedfunc.js

View file

@ -4,6 +4,7 @@ module.exports = {
"stylelint-scss", "stylelint-scss",
], ],
"rules": { "rules": {
"color-hex-case": null,
"indentation": 4, "indentation": 4,
"comment-empty-line-before": null, "comment-empty-line-before": null,
"declaration-empty-line-before": null, "declaration-empty-line-before": null,

View file

@ -1,3 +1,188 @@
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)
* Upgrade to JS SDK 9.11.0
* [Release] Tweak appearance of invite reason
[\#5848](https://github.com/matrix-org/matrix-react-sdk/pull/5848)
Changes in [3.18.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0-rc.1) (2021-04-07)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0...v3.18.0-rc.1)
* Upgrade to JS SDK 9.11.0-rc.1
* Translations update from Weblate
[\#5832](https://github.com/matrix-org/matrix-react-sdk/pull/5832)
* Add fake fallback thumbnail URL for encrypted videos
[\#5826](https://github.com/matrix-org/matrix-react-sdk/pull/5826)
* Fix broken "Go to Home View" shortcut on macOS
[\#5818](https://github.com/matrix-org/matrix-react-sdk/pull/5818)
* Remove status area UI defects when in-call
[\#5828](https://github.com/matrix-org/matrix-react-sdk/pull/5828)
* Fix viewing invitations when the inviter has no avatar set
[\#5829](https://github.com/matrix-org/matrix-react-sdk/pull/5829)
* Restabilize room list ordering with prefiltering on spaces/communities
[\#5825](https://github.com/matrix-org/matrix-react-sdk/pull/5825)
* Show invite reasons
[\#5694](https://github.com/matrix-org/matrix-react-sdk/pull/5694)
* Require strong password in forgot password form
[\#5744](https://github.com/matrix-org/matrix-react-sdk/pull/5744)
* Attended transfer
[\#5798](https://github.com/matrix-org/matrix-react-sdk/pull/5798)
* Make user autocomplete query search beyond prefix
[\#5822](https://github.com/matrix-org/matrix-react-sdk/pull/5822)
* Add reset option for corrupted event index store
[\#5806](https://github.com/matrix-org/matrix-react-sdk/pull/5806)
* Prevent Re-request encryption keys from appearing under redacted messages
[\#5816](https://github.com/matrix-org/matrix-react-sdk/pull/5816)
* Keybindings follow up
[\#5815](https://github.com/matrix-org/matrix-react-sdk/pull/5815)
* Increase default visible tiles for room sublists
[\#5821](https://github.com/matrix-org/matrix-react-sdk/pull/5821)
* Change copy to point to native node modules docs in element desktop
[\#5817](https://github.com/matrix-org/matrix-react-sdk/pull/5817)
* Show waveform and timer in voice messages
[\#5801](https://github.com/matrix-org/matrix-react-sdk/pull/5801)
* Label unlabeled avatar button in event panel
[\#5585](https://github.com/matrix-org/matrix-react-sdk/pull/5585)
* Fix the theme engine breaking with some web theming extensions
[\#5810](https://github.com/matrix-org/matrix-react-sdk/pull/5810)
* Add /spoiler command
[\#5696](https://github.com/matrix-org/matrix-react-sdk/pull/5696)
* Don't specify sample rates for voice messages
[\#5802](https://github.com/matrix-org/matrix-react-sdk/pull/5802)
* Tweak security key error handling
[\#5812](https://github.com/matrix-org/matrix-react-sdk/pull/5812)
* Add user settings for warn before exit
[\#5793](https://github.com/matrix-org/matrix-react-sdk/pull/5793)
* Decouple key bindings from event handling
[\#5720](https://github.com/matrix-org/matrix-react-sdk/pull/5720)
* Fixing spaces papercuts
[\#5792](https://github.com/matrix-org/matrix-react-sdk/pull/5792)
* Share keys for historical messages when inviting users to encrypted rooms
[\#5763](https://github.com/matrix-org/matrix-react-sdk/pull/5763)
* Fix upload bar not populating when starting uploads
[\#5804](https://github.com/matrix-org/matrix-react-sdk/pull/5804)
* Fix crash on login when using social login
[\#5803](https://github.com/matrix-org/matrix-react-sdk/pull/5803)
* Convert AccessSecretStorageDialog to TypeScript
[\#5805](https://github.com/matrix-org/matrix-react-sdk/pull/5805)
* Tweak cross-signing copy
[\#5807](https://github.com/matrix-org/matrix-react-sdk/pull/5807)
* Fix password change popup message
[\#5791](https://github.com/matrix-org/matrix-react-sdk/pull/5791)
* View Source: make Event ID go below Event ID
[\#5790](https://github.com/matrix-org/matrix-react-sdk/pull/5790)
* Fix line numbers when missing trailing newline
[\#5800](https://github.com/matrix-org/matrix-react-sdk/pull/5800)
* Remember reply when switching rooms
[\#5796](https://github.com/matrix-org/matrix-react-sdk/pull/5796)
* Fix edge case with redaction grouper messing up continuations
[\#5797](https://github.com/matrix-org/matrix-react-sdk/pull/5797)
* Only show the ask anyway modal for explicit user lookup failures
[\#5785](https://github.com/matrix-org/matrix-react-sdk/pull/5785)
* Improve error reporting when EventIndex fails on a supported environment
[\#5787](https://github.com/matrix-org/matrix-react-sdk/pull/5787)
* Tweak and fix some space features
[\#5789](https://github.com/matrix-org/matrix-react-sdk/pull/5789)
* Support replying with a message command
[\#5686](https://github.com/matrix-org/matrix-react-sdk/pull/5686)
* Labs feature: Early implementation of voice messages
[\#5769](https://github.com/matrix-org/matrix-react-sdk/pull/5769)
Changes in [3.17.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0) (2021-03-29)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0-rc.1...v3.17.0)
* Upgrade to JS SDK 9.10.0
* [Release] Tweak cross-signing copy
[\#5808](https://github.com/matrix-org/matrix-react-sdk/pull/5808)
* [Release] Fix crash on login when using social login
[\#5809](https://github.com/matrix-org/matrix-react-sdk/pull/5809)
* [Release] Fix edge case with redaction grouper messing up continuations
[\#5799](https://github.com/matrix-org/matrix-react-sdk/pull/5799)
Changes in [3.17.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0-rc.1) (2021-03-25)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0...v3.17.0-rc.1)
* Upgrade to JS SDK 9.10.0-rc.1
* Translations update from Weblate
[\#5788](https://github.com/matrix-org/matrix-react-sdk/pull/5788)
* Track next event [tile] over group boundaries
[\#5784](https://github.com/matrix-org/matrix-react-sdk/pull/5784)
* Fixing the minor UI issues in the email discovery
[\#5780](https://github.com/matrix-org/matrix-react-sdk/pull/5780)
* Don't overwrite callback with undefined if no customization provided
[\#5783](https://github.com/matrix-org/matrix-react-sdk/pull/5783)
* Fix redaction event list summaries breaking sender profiles
[\#5781](https://github.com/matrix-org/matrix-react-sdk/pull/5781)
* Fix CIDER formatting buttons on Safari
[\#5782](https://github.com/matrix-org/matrix-react-sdk/pull/5782)
* Improve discovery of rooms in a space
[\#5776](https://github.com/matrix-org/matrix-react-sdk/pull/5776)
* Spaces improve creation journeys
[\#5777](https://github.com/matrix-org/matrix-react-sdk/pull/5777)
* Make buttons in verify dialog respect the system font
[\#5778](https://github.com/matrix-org/matrix-react-sdk/pull/5778)
* Collapse redactions into an event list summary
[\#5728](https://github.com/matrix-org/matrix-react-sdk/pull/5728)
* Added invite option to room's context menu
[\#5648](https://github.com/matrix-org/matrix-react-sdk/pull/5648)
* Add an optional config option to make the welcome page the login page
[\#5658](https://github.com/matrix-org/matrix-react-sdk/pull/5658)
* Fix username showing instead of display name in Jitsi widgets
[\#5770](https://github.com/matrix-org/matrix-react-sdk/pull/5770)
* Convert a bunch more js-sdk imports to absolute paths
[\#5774](https://github.com/matrix-org/matrix-react-sdk/pull/5774)
* Remove forgotten rooms from the room list once forgotten
[\#5775](https://github.com/matrix-org/matrix-react-sdk/pull/5775)
* Log error when failing to list usermedia devices
[\#5771](https://github.com/matrix-org/matrix-react-sdk/pull/5771)
* Fix weird timeline jumps
[\#5772](https://github.com/matrix-org/matrix-react-sdk/pull/5772)
* Replace type declaration in Registration.tsx
[\#5773](https://github.com/matrix-org/matrix-react-sdk/pull/5773)
* Add possibility to delay rageshake persistence in app startup
[\#5767](https://github.com/matrix-org/matrix-react-sdk/pull/5767)
* Fix left panel resizing and lower min-width improving flexibility
[\#5764](https://github.com/matrix-org/matrix-react-sdk/pull/5764)
* Work around more cases where a rageshake server might not be present
[\#5766](https://github.com/matrix-org/matrix-react-sdk/pull/5766)
* Iterate space panel visually and functionally
[\#5761](https://github.com/matrix-org/matrix-react-sdk/pull/5761)
* Make some dispatches async
[\#5765](https://github.com/matrix-org/matrix-react-sdk/pull/5765)
* fix: make room directory correct when using a homeserver with explicit port
[\#5762](https://github.com/matrix-org/matrix-react-sdk/pull/5762)
* Hangup all calls on logout
[\#5756](https://github.com/matrix-org/matrix-react-sdk/pull/5756)
* Remove now-unused assets and CSS from CompleteSecurity step
[\#5757](https://github.com/matrix-org/matrix-react-sdk/pull/5757)
* Add details and summary to allowed HTML tags
[\#5760](https://github.com/matrix-org/matrix-react-sdk/pull/5760)
* Support a media handling customisation endpoint
[\#5714](https://github.com/matrix-org/matrix-react-sdk/pull/5714)
* Edit button on View Source dialog that takes you to devtools ->
SendCustomEvent
[\#5718](https://github.com/matrix-org/matrix-react-sdk/pull/5718)
* Show room alias in plain/formatted body
[\#5748](https://github.com/matrix-org/matrix-react-sdk/pull/5748)
* Allow pills on the beginning of a part string
[\#5754](https://github.com/matrix-org/matrix-react-sdk/pull/5754)
* [SK-3] Decorate easy components with replaceableComponent
[\#5734](https://github.com/matrix-org/matrix-react-sdk/pull/5734)
* Use fsync in reskindex to ensure file is written to disk
[\#5753](https://github.com/matrix-org/matrix-react-sdk/pull/5753)
* Remove unused common CSS classes
[\#5752](https://github.com/matrix-org/matrix-react-sdk/pull/5752)
* Rebuild space previews with new designs
[\#5751](https://github.com/matrix-org/matrix-react-sdk/pull/5751)
* Rework cross-signing login flow
[\#5727](https://github.com/matrix-org/matrix-react-sdk/pull/5727)
* Change read receipt drift to be non-fractional
[\#5745](https://github.com/matrix-org/matrix-react-sdk/pull/5745)
Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15) Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0)
@ -127,11 +312,12 @@ Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/
## Security notice ## Security notice
matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the matrix-react-sdk 3.15.0 fixes a moderate severity issue (CVE-2021-21320) where
user content sandbox can be abused to trick users into opening unexpected the user content sandbox can be abused to trick users into opening unexpected
documents. The content is opened with a `blob` origin that cannot access Matrix documents after several user interactions. The content can be opened with a
user data, so messages and secrets are not at risk. Thanks to @keerok for `blob` origin from the Matrix client, so it is possible for a malicious document
responsibly disclosing this via Matrix's Security Disclosure Policy. to access user messages and secrets. Thanks to @keerok for responsibly
disclosing this via Matrix's Security Disclosure Policy.
## All changes ## All changes

View file

@ -124,12 +124,19 @@ all kinds of filtering.
## Filtering ## Filtering
Filters are provided to the store as condition classes, which are then passed along to the algorithm Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.
implementations. The implementations then get to decide how to actually filter the rooms, however in
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms, Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is
as the old room list store does. When a filter condition changes, it emits an update which (in this due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of
rooms to the user. The algorithm implementations will not see a room being prefiltered out.
Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These
filters are passed along to the algorithm implementations where those implementations decide how and
when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for
optimization reasons.
The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of
rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
minor subset where possible to avoid over-iterating rooms. minor subset where possible to avoid over-iterating rooms.
@ -137,6 +144,13 @@ All filter conditions are considered "stable" by the consumers, meaning that the
expect a change in the condition unless the condition says it has changed. This is intentional to expect a change in the condition unless the condition says it has changed. This is intentional to
maintain the caching behaviour described above. maintain the caching behaviour described above.
One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight
subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where
room notifications are self-contained within that workspace. Runtime filters tend to not want to affect
visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as
they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead,
the notification counts would vary while the user was typing and "found 2/12" UX would not be possible.
## Class breakdowns ## Class breakdowns
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care The `RoomListStore` is the major coordinator of various algorithm implementations, which take care

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.16.0", "version": "3.18.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -102,7 +102,6 @@
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2", "text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
"velocity-animate": "^2.0.6",
"what-input": "^5.2.10", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },

View file

@ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e
:root { :root {
font-size: 10px; font-size: 10px;
--transition-short: .1s;
--transition-standard: .3s;
}
@media (prefers-reduced-motion) {
:root {
--transition-short: 0;
--transition-standard: 0;
}
} }
html { html {
@ -303,7 +313,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
} }
.mx_Dialog_lightbox .mx_Dialog_background { .mx_Dialog_lightbox .mx_Dialog_background {
opacity: 0.85; opacity: $lightbox-background-bg-opacity;
background-color: $lightbox-background-bg-color; background-color: $lightbox-background-bg-color;
} }
@ -315,6 +325,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
pointer-events: none; pointer-events: none;
padding: 0;
} }
.mx_Dialog_header { .mx_Dialog_header {

View file

@ -117,11 +117,13 @@
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_EventListSummary.scss";
@import "./views/elements/_FacePile.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_FormButton.scss"; @import "./views/elements/_FormButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InfoTooltip.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_InviteReason.scss";
@import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_MiniAvatarUploader.scss"; @import "./views/elements/_MiniAvatarUploader.scss";
@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_PowerSelector.scss";
@ -246,8 +248,10 @@
@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/_Waveform.scss";
@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_CallViewForRoom.scss";
@import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.scss"; @import "./views/voip/_DialPadModal.scss";

View file

@ -22,7 +22,7 @@ limitations under the License.
// keep border thickness consistent to prevent movement // keep border thickness consistent to prevent movement
border: 1px solid transparent; border: 1px solid transparent;
height: 28px; height: 28px;
padding: 2px; padding: 1px;
// Create a flexbox for the icons (easier to manage) // Create a flexbox for the icons (easier to manage)
display: flex; display: flex;

View file

@ -262,12 +262,6 @@ hr.mx_RoomView_myReadMarker {
padding-top: 1px; padding-top: 1px;
} }
.mx_RoomView_inCall .mx_RoomView_statusAreaBox {
background-color: $accent-color;
color: $accent-fg-color;
position: relative;
}
.mx_RoomView_voipChevron { .mx_RoomView_voipChevron {
position: absolute; position: absolute;
bottom: -11px; bottom: -11px;

View file

@ -21,6 +21,5 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
overflow-y: hidden;
} }
} }

View file

@ -276,15 +276,17 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton:hover, .mx_SpaceButton:hover,
.mx_SpaceButton:focus-within, .mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen { .mx_SpaceButton_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button &:not(.mx_SpaceButton_home) {
.mx_SpacePanel_badgeContainer { // Hide the badge container on hover because it'll be a menu button
width: 0; .mx_SpacePanel_badgeContainer {
height: 0; width: 0;
display: none; height: 0;
} display: none;
}
.mx_SpaceButton_menuButton { .mx_SpaceButton_menuButton {
display: block; display: block;
}
} }
} }
} }
@ -330,10 +332,6 @@ $activeBorderColor: $secondary-fg-color;
mask-image: url('$(res)/img/element-icons/leave.svg'); mask-image: url('$(res)/img/element-icons/leave.svg');
} }
.mx_SpacePanel_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
}
.mx_SpacePanel_iconMembers::before { .mx_SpacePanel_iconMembers::before {
mask-image: url('$(res)/img/element-icons/room/members.svg'); mask-image: url('$(res)/img/element-icons/room/members.svg');
} }

View file

@ -182,7 +182,7 @@ limitations under the License.
.mx_SpaceRoomDirectory_roomTile { .mx_SpaceRoomDirectory_roomTile {
position: relative; position: relative;
padding: 6px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
min-height: 56px; min-height: 56px;
box-sizing: border-box; box-sizing: border-box;
@ -190,6 +190,7 @@ limitations under the License.
display: grid; display: grid;
grid-template-columns: 20px auto max-content; grid-template-columns: 20px auto max-content;
grid-column-gap: 8px; grid-column-gap: 8px;
grid-row-gap: 6px;
align-items: center; align-items: center;
.mx_BaseAvatar { .mx_BaseAvatar {
@ -213,16 +214,28 @@ limitations under the License.
.mx_InfoTooltip_icon { .mx_InfoTooltip_icon {
margin-right: 4px; margin-right: 4px;
position: relative;
vertical-align: text-top;
&::before {
position: absolute;
top: 0;
left: 0;
}
} }
} }
} }
.mx_SpaceRoomDirectory_roomTile_info { .mx_SpaceRoomDirectory_roomTile_info {
font-size: $font-12px; font-size: $font-14px;
line-height: $font-15px; line-height: $font-18px;
color: $tertiary-fg-color; color: $secondary-fg-color;
grid-row: 2; grid-row: 2;
grid-column: 1/3; grid-column: 1/3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
} }
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
@ -232,9 +245,9 @@ limitations under the License.
grid-row: 1/3; grid-row: 1/3;
.mx_AccessibleButton { .mx_AccessibleButton {
padding: 6px 18px; padding: 8px 18px;
display: inline-block;
display: none; visibility: hidden;
} }
.mx_Checkbox { .mx_Checkbox {
@ -248,7 +261,7 @@ limitations under the License.
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
.mx_AccessibleButton { .mx_AccessibleButton {
display: inline-block; visibility: visible;
} }
} }
} }

View file

@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px;
width: 432px; width: 432px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
border: 1px solid $space-button-outline-color; border: 1px solid $input-border-color;
font-size: $font-15px; font-size: $font-15px;
margin: 20px 0; margin: 20px 0;
@ -122,7 +122,6 @@ $SpaceRoomViewInnerWidth: 428px;
max-width: 480px; max-width: 480px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color; box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px; border-radius: 8px;
.mx_SpaceRoomView_preview_inviter { .mx_SpaceRoomView_preview_inviter {
@ -154,53 +153,6 @@ $SpaceRoomViewInnerWidth: 428px;
margin: 20px 0 !important; // override default margin from above margin: 20px 0 !important; // override default margin from above
} }
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic { .mx_SpaceRoomView_preview_topic {
font-size: $font-14px; font-size: $font-14px;
line-height: $font-22px; line-height: $font-22px;
@ -254,36 +206,90 @@ $SpaceRoomViewInnerWidth: 428px;
vertical-align: middle; vertical-align: middle;
} }
} }
}
.mx_SpaceRoomView_landing_memberCount { .mx_SpaceRoomView_landing_info {
display: flex;
align-items: center;
.mx_SpaceRoomView_info {
display: inline-block;
margin: 0;
}
.mx_FacePile {
display: inline-block;
margin-left: auto;
margin-right: 12px;
.mx_FacePile_faces {
cursor: pointer;
> span:hover {
.mx_BaseAvatar {
filter: brightness(0.8);
}
}
> span:first-child {
position: relative;
.mx_BaseAvatar {
filter: brightness(0.8);
}
&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: 30px;
width: 30px;
background: #ffffff; // white icon fill
mask-position: center;
mask-size: 24px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
}
}
.mx_SpaceRoomView_landing_inviteButton {
position: relative; position: relative;
margin-left: 24px; padding-left: 40px;
padding: 0 0 0 28px; height: min-content;
line-height: $font-24px;
vertical-align: text-bottom;
&::before { &::before {
position: absolute; position: absolute;
content: ''; content: "";
width: 24px; left: 8px;
height: 24px; height: 16px;
top: 0; width: 16px;
left: 0; background: #ffffff; // white icon fill
mask-position: center; mask-position: center;
mask-size: 16px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-image: url('$(res)/img/element-icons/room/invite.svg');
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
} }
} }
} }
.mx_SpaceRoomView_landing_topic { .mx_SpaceRoomView_landing_topic {
font-size: $font-15px; font-size: $font-15px;
margin-top: 12px;
margin-bottom: 16px;
}
> hr {
border: none;
height: 1px;
background-color: $groupFilterPanel-bg-color;
} }
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 24px;
.mx_AccessibleButton { .mx_AccessibleButton {
position: relative; position: relative;
@ -292,9 +298,9 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box; box-sizing: border-box;
padding: 72px 16px 0; padding: 72px 16px 0;
border-radius: 12px; border-radius: 12px;
border: 1px solid $space-button-outline-color; border: 1px solid $input-border-color;
margin-right: 28px; margin-right: 28px;
margin-bottom: 28px; margin-bottom: 20px;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
@ -324,16 +330,6 @@ $SpaceRoomViewInnerWidth: 428px;
background: #ffffff; // white icon fill background: #ffffff; // white icon fill
} }
&.mx_SpaceRoomView_landing_inviteButton {
&::before {
background-color: $accent-color;
}
&::after {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
&.mx_SpaceRoomView_landing_addButton { &.mx_SpaceRoomView_landing_addButton {
&::before { &::before {
background-color: #ac3ba8; background-color: #ac3ba8;
@ -366,12 +362,8 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomDirectory_list { .mx_SearchBox {
max-width: 600px; margin: 0 0 20px;
.mx_SpaceRoomDirectory_roomTile_actions {
display: none;
}
} }
} }
@ -424,3 +416,50 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
} }
.mx_SpaceRoomView_info {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_info_public,
.mx_SpaceRoomView_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}

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.
@ -158,6 +158,10 @@ limitations under the License.
} }
} }
.mx_Toast_detail {
color: $secondary-fg-color;
}
.mx_Toast_deviceID { .mx_Toast_deviceID {
font-size: $font-10px; font-size: $font-10px;
} }

View file

@ -117,6 +117,32 @@ limitations under the License.
.mx_UserMenu_headerButtons { .mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work. // No special styles: the rest of the layout happens to make it work.
} }
.mx_UserMenu_dnd {
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
&::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $muted-fg-color;
}
&.mx_UserMenu_dnd_noisy::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
&.mx_UserMenu_dnd_muted::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
}
} }
&.mx_UserMenu_minimized { &.mx_UserMenu_minimized {

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018, 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.
@ -15,6 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_AccessSecretStorageDialog_reset {
position: relative;
padding-left: 24px; // 16px icon + 8px padding
margin-top: 7px; // vertical alignment to buttons
&::before {
content: "";
display: inline-block;
position: absolute;
height: 16px;
width: 16px;
left: 0;
top: 2px; // alignment
background-image: url("$(res)/img/element-icons/warning-badge.svg");
}
.mx_AccessSecretStorageDialog_reset_link {
color: $warning-color;
}
}
.mx_AccessSecretStorageDialog_titleWithIcon::before { .mx_AccessSecretStorageDialog_titleWithIcon::before {
content: ''; content: '';
display: inline-block; display: inline-block;
@ -26,6 +46,13 @@ limitations under the License.
background-color: $primary-fg-color; background-color: $primary-fg-color;
} }
.mx_AccessSecretStorageDialog_resetBadge::before {
// The image isn't capable of masking, so we use a background instead.
background-image: url("$(res)/img/element-icons/warning-badge.svg");
background-size: 24px;
background-color: transparent;
}
.mx_AccessSecretStorageDialog_secureBackupTitle::before { .mx_AccessSecretStorageDialog_secureBackupTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
} }

View file

@ -0,0 +1,42 @@
/*
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_FacePile {
.mx_FacePile_faces {
display: inline-flex;
flex-direction: row-reverse;
vertical-align: middle;
> span + span {
margin-right: -8px;
}
.mx_BaseAvatar_image {
border: 1px solid $primary-bg-color;
}
.mx_BaseAvatar_initial {
margin: 1px; // to offset the border on the image
}
}
> span {
margin-left: 12px;
font-size: $font-14px;
line-height: $font-24px;
color: $tertiary-fg-color;
}
}

View file

@ -14,139 +14,108 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/* This has got to be the most fragile piece of CSS ever written.
But empirically it works on Chrome/FF/Safari
*/
.mx_ImageView { .mx_ImageView {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
align-items: center;
}
.mx_ImageView_lhs {
order: 1;
flex: 1 1 10%;
min-width: 60px;
// background-color: #080;
// height: 20px;
}
.mx_ImageView_content {
order: 2;
/* min-width hack needed for FF */
min-width: 0px;
height: 90%;
flex: 15 15 0;
display: flex;
align-items: center;
justify-content: center;
}
.mx_ImageView_content img {
max-width: 100%;
/* XXX: max-height interacts badly with flex on Chrome and doesn't relayout properly until you refresh */
max-height: 100%;
/* object-fit hack needed for Chrome due to Chrome not re-laying-out until you refresh */
object-fit: contain;
/* background-image: url('$(res)/img/trans.png'); */
pointer-events: all;
}
.mx_ImageView_labelWrapper {
position: absolute;
top: 0px;
right: 0px;
height: 100%;
overflow: auto;
pointer-events: all;
}
.mx_ImageView_label {
text-align: left;
display: flex;
justify-content: center;
flex-direction: column; flex-direction: column;
padding-left: 30px; }
padding-right: 30px;
min-height: 100%; .mx_ImageView_image_wrapper {
max-width: 240px; display: flex;
justify-content: center;
align-items: center;
height: 100%;
overflow: hidden;
}
.mx_ImageView_image {
pointer-events: all;
max-width: 95%;
max-height: 95%;
}
.mx_ImageView_panel {
width: 100%;
height: 68px;
display: flex;
justify-content: space-between;
align-items: center;
}
.mx_ImageView_info_wrapper {
pointer-events: all;
padding-left: 32px;
display: flex;
flex-direction: row;
align-items: center;
color: $lightbox-fg-color; color: $lightbox-fg-color;
} }
.mx_ImageView_cancel { .mx_ImageView_info {
position: absolute; padding-left: 12px;
// hack for mx_Dialog having a top padding of 40px display: flex;
top: 40px; flex-direction: column;
right: 0px;
padding-top: 35px;
padding-right: 35px;
cursor: pointer;
} }
.mx_ImageView_rotateClockwise { .mx_ImageView_info_sender {
position: absolute; font-weight: bold;
top: 40px;
right: 70px;
padding-top: 35px;
cursor: pointer;
} }
.mx_ImageView_rotateCounterClockwise { .mx_ImageView_toolbar {
position: absolute; padding-right: 16px;
top: 40px; pointer-events: all;
right: 105px; display: flex;
padding-top: 35px; align-items: center;
cursor: pointer;
}
.mx_ImageView_name {
font-size: $font-18px;
margin-bottom: 6px;
word-wrap: break-word;
}
.mx_ImageView_metadata {
font-size: $font-15px;
opacity: 0.5;
}
.mx_ImageView_download {
display: table;
margin-top: 24px;
margin-bottom: 6px;
border-radius: 5px;
background-color: $lightbox-bg-color;
font-size: $font-14px;
padding: 9px;
border: 1px solid $lightbox-border-color;
}
.mx_ImageView_size {
font-size: $font-11px;
}
.mx_ImageView_link {
color: $lightbox-fg-color !important;
text-decoration: none !important;
} }
.mx_ImageView_button { .mx_ImageView_button {
font-size: $font-15px; margin-left: 24px;
opacity: 0.5; display: block;
margin-top: 18px;
cursor: pointer; &::before {
content: '';
height: 22px;
width: 22px;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
display: block;
background-color: $icon-button-color;
}
} }
.mx_ImageView_shim { .mx_ImageView_button_rotateCW::before {
height: 30px; mask-image: url('$(res)/img/image-view/rotate-cw.svg');
} }
.mx_ImageView_rhs { .mx_ImageView_button_rotateCCW::before {
order: 3; mask-image: url('$(res)/img/image-view/rotate-ccw.svg');
flex: 1 1 10%; }
min-width: 300px;
// background-color: #800; .mx_ImageView_button_zoomOut::before {
// height: 20px; mask-image: url('$(res)/img/image-view/zoom-out.svg');
}
.mx_ImageView_button_zoomIn::before {
mask-image: url('$(res)/img/image-view/zoom-in.svg');
}
.mx_ImageView_button_download::before {
mask-image: url('$(res)/img/image-view/download.svg');
}
.mx_ImageView_button_more::before {
mask-image: url('$(res)/img/image-view/more.svg');
}
.mx_ImageView_button_close {
border-radius: 100%;
background: #21262c; // same on all themes
&::before {
width: 32px;
height: 32px;
mask-image: url('$(res)/img/image-view/close.svg');
mask-size: 40%;
}
} }

View file

@ -0,0 +1,57 @@
/*
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_InviteReason {
position: relative;
margin-bottom: 1em;
.mx_InviteReason_reason {
visibility: visible;
}
.mx_InviteReason_view {
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
justify-content: center;
align-items: center;
cursor: pointer;
color: $secondary-fg-color;
&::before {
content: "";
margin-right: 8px;
background-color: $secondary-fg-color;
mask-image: url('$(res)/img/feather-customised/eye.svg');
display: inline-block;
width: 18px;
height: 14px;
}
}
}
.mx_InviteReason_hidden {
.mx_InviteReason_reason {
visibility: hidden;
}
.mx_InviteReason_view {
display: flex;
}
}

View file

@ -159,6 +159,7 @@ $left-gutter: 64px;
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, .mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, .mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, .mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, .mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { .mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
visibility: visible; visibility: visible;
@ -282,6 +283,10 @@ $left-gutter: 64px;
display: inline-block; display: inline-block;
height: $font-14px; height: $font-14px;
width: $font-14px; width: $font-14px;
transition:
left var(--transition-short) ease-out,
top var(--transition-standard) ease-out;
} }
.mx_EventTile_readAvatarRemainder { .mx_EventTile_readAvatarRemainder {

View file

@ -216,6 +216,25 @@ $irc-line-height: $font-18px;
} }
} }
} }
.mx_EventTile_emote {
> .mx_EventTile_avatar {
margin-left: initial;
}
}
.mx_MessageTimestamp {
width: initial;
}
/**
* adding the icon back in the document flow
* if it's not present, there's no unwanted wasted space
*/
.mx_EventTile_e2eIcon {
position: relative;
order: -1;
}
} }
.mx_ProfileResizer { .mx_ProfileResizer {

View file

@ -37,7 +37,7 @@ limitations under the License.
.mx_RoomList_explorePrompt { .mx_RoomList_explorePrompt {
margin: 4px 12px 4px; margin: 4px 12px 4px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid $tertiary-fg-color; border-top: 1px solid $input-border-color;
font-size: $font-14px; font-size: $font-14px;
div:first-child { div:first-child {

View file

@ -34,3 +34,43 @@ limitations under the License.
background-color: $voice-record-stop-symbol-color; background-color: $voice-record-stop-symbol-color;
} }
} }
.mx_VoiceRecordComposerTile_waveformContainer {
padding: 5px;
padding-right: 4px; // there's 1px from the waveform itself, so account for that
padding-left: 15px; // +10px for the live circle, +5px for regular padding
background-color: $voice-record-waveform-bg-color;
border-radius: 12px;
margin-right: 12px; // isolate from stop button
// Cheat at alignment a bit
display: flex;
align-items: center;
position: relative; // important for the live circle
color: $voice-record-waveform-fg-color;
font-size: $font-14px;
&::before {
// TODO: @@ TravisR: Animate
content: '';
background-color: $voice-record-live-circle-color;
width: 10px;
height: 10px;
position: absolute;
left: 8px;
top: 16px; // vertically center
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

@ -0,0 +1,40 @@
/*
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_Waveform {
position: relative;
height: 30px; // tallest bar can only be 30px
top: 1px; // because of our border trick (see below), we're off by 1px of aligntment
display: flex;
align-items: center; // so the bars grow from the middle
overflow: hidden; // this is cheaper than a `max-height: calc(100% - 4px)` in the bar's CSS.
// A bar is meant to be a 2x2 circle when at zero height, and otherwise a 2px wide line
// with rounded caps.
.mx_Waveform_bar {
width: 0; // 0px width means we'll end up using the border as our width
border: 1px solid transparent; // transparent means we'll use the background colour
border-radius: 2px; // rounded end caps, based on the border
min-height: 0; // like the width, we'll rely on the border to give us height
max-height: 100%; // this makes the `height: 42%` work on the element
margin-left: 1px; // we want 2px between each bar, so 1px on either side for balance
margin-right: 1px;
// background color is handled by the parent components
}
}

View file

@ -27,13 +27,12 @@ limitations under the License.
.mx_CallView_large { .mx_CallView_large {
padding-bottom: 10px; padding-bottom: 10px;
margin: 5px 5px 5px 18px; margin: 5px 5px 5px 18px;
display: flex;
flex-direction: column;
flex: 1;
.mx_CallView_voice { .mx_CallView_voice {
height: 360px; flex: 1;
@media only screen and (max-height: 768px) {
height: 220px;
}
} }
} }
@ -59,7 +58,7 @@ limitations under the License.
} }
} }
.mx_CallView_voice_holdText { .mx_CallView_holdTransferContent {
padding-top: 10px; padding-top: 10px;
padding-bottom: 25px; padding-bottom: 25px;
} }
@ -86,7 +85,7 @@ limitations under the License.
} }
} }
.mx_CallView_voice_hold { .mx_CallView_voice .mx_CallView_holdTransferContent {
// This masks the avatar image so when it's blurred, the edge is still crisp // This masks the avatar image so when it's blurred, the edge is still crisp
.mx_CallView_voice_avatarContainer { .mx_CallView_voice_avatarContainer {
border-radius: 2000px; border-radius: 2000px;
@ -95,7 +94,7 @@ limitations under the License.
} }
} }
.mx_CallView_voice_holdText { .mx_CallView_holdTransferContent {
height: 20px; height: 20px;
padding-top: 20px; padding-top: 20px;
padding-bottom: 15px; padding-bottom: 15px;
@ -108,6 +107,7 @@ limitations under the License.
.mx_CallView_video { .mx_CallView_video {
width: 100%; width: 100%;
height: 100%;
position: relative; position: relative;
z-index: 30; z-index: 30;
border-radius: 8px; border-radius: 8px;
@ -146,7 +146,7 @@ limitations under the License.
} }
} }
.mx_CallView_video_holdContent { .mx_CallView_video .mx_CallView_holdTransferContent {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -181,6 +181,7 @@ limitations under the License.
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: left; justify-content: left;
flex-shrink: 0;
} }
.mx_CallView_header_callType { .mx_CallView_header_callType {

View file

@ -0,0 +1,46 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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_CallViewForRoom {
overflow: hidden;
.mx_CallViewForRoom_ResizeWrapper {
display: flex;
margin-bottom: 8px;
&:hover .mx_CallViewForRoom_ResizeHandle {
// Need to use important to override element style attributes
// set by re-resizable
width: 100% !important;
display: flex;
justify-content: center;
&::after {
content: '';
margin-top: 3px;
border-radius: 4px;
height: 4px;
width: 100%;
max-width: 64px;
background-color: $primary-fg-color;
}
}
}
}

View file

@ -16,7 +16,7 @@ limitations under the License.
.mx_VideoFeed_remote { .mx_VideoFeed_remote {
width: 100%; width: 100%;
max-height: 100%; height: 100%;
background-color: #000; background-color: #000;
z-index: 50; z-index: 50;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#ffffff" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4684 2.04056C11.8589 1.65003 11.8589 1.01687 11.4684 0.626342C11.0779 0.235818 10.4447 0.235818 10.0542 0.626342L6.04718 4.63333L1.81137 0.397522C1.42084 0.00699783 0.78768 0.00699781 0.397156 0.397522C0.0066314 0.788046 0.00663096 1.42121 0.397155 1.81174L4.63297 6.04755L0.62608 10.0544C0.235557 10.445 0.235556 11.0781 0.626081 11.4686C1.0166 11.8592 1.64977 11.8592 2.04029 11.4686L6.04718 7.46176L9.82525 11.2398C10.2158 11.6303 10.8489 11.6303 11.2395 11.2398C11.63 10.8493 11.63 10.2161 11.2395 9.82561L7.46139 6.04755L11.4684 2.04056Z" fill="#A9B2BC"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1C9 0.447715 8.55229 0 8 0C7.44772 0 7 0.447715 7 1L7 12.5858L2.20711 7.79289C1.81658 7.40237 1.18342 7.40237 0.792893 7.79289C0.402369 8.18342 0.402369 8.81658 0.792893 9.20711L7.29289 15.7071C7.68342 16.0976 8.31658 16.0976 8.70711 15.7071L15.2071 9.20711C15.5976 8.81658 15.5976 8.18342 15.2071 7.79289C14.8166 7.40237 14.1834 7.40237 13.7929 7.79289L9 12.5858L9 1Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 542 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 12C6.66699 13.1046 5.77156 14 4.66699 14C3.56242 14 2.66699 13.1046 2.66699 12C2.66699 10.8954 3.56242 10 4.66699 10C5.77156 10 6.66699 10.8954 6.66699 12ZM14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12ZM19.333 14C20.4376 14 21.333 13.1046 21.333 12C21.333 10.8954 20.4376 10 19.333 10C18.2284 10 17.333 10.8954 17.333 12C17.333 13.1046 18.2284 14 19.333 14Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View file

@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0.25C14.2811 0.25 16.3824 1.03493 18.0435 2.34854L18.0645 2.36549C19.294 3.38165 21.75 6.28172 21.75 10C21.75 15.3848 17.3848 19.75 12 19.75C11.3096 19.75 10.75 19.1904 10.75 18.5C10.75 17.8096 11.3096 17.25 12 17.25C16.0041 17.25 19.25 14.0041 19.25 10C19.25 7.32797 17.4103 5.07339 16.4819 4.30089C15.2482 3.32907 13.6934 2.75 12 2.75C8.33522 2.75 5.30553 5.46916 4.8184 9H6.50851C6.9004 9 7.13415 9.43723 6.91677 9.76366L3.90826 14.2813C3.71404 14.5729 3.28596 14.5729 3.09174 14.2813L0.083231 9.76366C-0.134151 9.43723 0.0995979 9 0.491489 9H2.30066C2.80139 4.085 6.95284 0.25 12 0.25Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 764 B

View file

@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0.25C7.71886 0.25 5.61758 1.03493 3.95646 2.34854L3.93549 2.36549C2.70597 3.38165 0.25 6.28172 0.25 10C0.25 15.3848 4.61522 19.75 10 19.75C10.6904 19.75 11.25 19.1904 11.25 18.5C11.25 17.8096 10.6904 17.25 10 17.25C5.99594 17.25 2.75 14.0041 2.75 10C2.75 7.32797 4.58973 5.07339 5.51806 4.30089C6.7518 3.32907 8.30655 2.75 10 2.75C13.6648 2.75 16.6945 5.46916 17.1816 9H15.4915C15.0996 9 14.8658 9.43723 15.0832 9.76366L18.0917 14.2813C18.286 14.5729 18.714 14.5729 18.9083 14.2813L21.9168 9.76366C22.1342 9.43723 21.9004 9 21.5085 9H19.6993C19.1986 4.085 15.0472 0.25 10 0.25Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

View file

@ -0,0 +1,3 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3293 13.5616C16.379 12.1476 17 10.3963 17 8.5C17 3.80558 13.1944 0 8.5 0C3.80558 0 0 3.80558 0 8.5C0 13.1944 3.80558 17 8.5 17C10.3963 17 12.1476 16.379 13.5616 15.3293L18.1161 19.8839C18.6043 20.372 19.3957 20.372 19.8839 19.8839C20.372 19.3957 20.372 18.6043 19.8839 18.1161L15.3293 13.5616ZM9.5 4C9.5 3.44772 9.05228 3 8.5 3C7.94772 3 7.5 3.44772 7.5 4V7.5H4C3.44772 7.5 3 7.94772 3 8.5C3 9.05228 3.44772 9.5 4 9.5H7.5V13C7.5 13.5523 7.94771 14 8.5 14C9.05228 14 9.5 13.5523 9.5 13V9.5H13C13.5523 9.5 14 9.05228 14 8.5C14 7.94772 13.5523 7.5 13 7.5H9.5V4Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View file

@ -0,0 +1,3 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3293 13.5616C16.379 12.1476 17 10.3963 17 8.5C17 3.80558 13.1944 0 8.5 0C3.80558 0 0 3.80558 0 8.5C0 13.1944 3.80558 17 8.5 17C10.3963 17 12.1476 16.379 13.5616 15.3293L18.1161 19.8839C18.6043 20.372 19.3957 20.372 19.8839 19.8839C20.372 19.3957 20.372 18.6043 19.8839 18.1161L15.3293 13.5616ZM3 8.5C3 7.94772 3.44772 7.5 4 7.5H13C13.5523 7.5 14 7.94772 14 8.5C14 9.05229 13.5523 9.5 13 9.5H4C3.44772 9.5 3 9.05229 3 8.5Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>

Before

Width:  |  Height:  |  Size: 311 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-cw"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>

Before

Width:  |  Height:  |  Size: 315 B

View file

@ -85,6 +85,7 @@ $dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: $header-panel-bg-color; $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 85%;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #21262c; $settings-profile-placeholder-bg-color: #21262c;
@ -123,7 +124,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
@ -243,7 +243,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
@define-mixin mx_DialogButton_secondary { @define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones // flip colours for the secondary ones
font-weight: 600; font-weight: 600;
border: 1px solid $accent-color ! important; border: 1px solid $accent-color !important;
color: $accent-color; color: $accent-color;
background-color: $button-secondary-bg-color; background-color: $button-secondary-bg-color;
} }

View file

@ -83,6 +83,7 @@ $dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: $header-panel-bg-color; $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 85%;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #e7e7e7;
@ -120,7 +121,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -127,6 +127,7 @@ $dialog-close-fg-color: #c1c1c1;
$dialog-background-bg-color: #e9e9e9; $dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 95%;
$imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
@ -187,10 +188,12 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -15,8 +15,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
@ -24,8 +24,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
@ -34,8 +34,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
@ -43,8 +43,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
@ -53,8 +53,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
@ -62,8 +62,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
@ -72,8 +72,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
@ -81,8 +81,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range; unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"), src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"),
url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.13") format("woff"); url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff");
} }
/* latin-ext */ /* latin-ext */

View file

@ -118,6 +118,7 @@ $dialog-close-fg-color: #c1c1c1;
$dialog-background-bg-color: #e9e9e9; $dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 95%;
$imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
@ -178,10 +179,12 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color;
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -22,15 +22,26 @@ clone() {
} }
# Try the PR author's branch in case it exists on the deps as well. # Try the PR author's branch in case it exists on the deps as well.
# If BUILDKITE_BRANCH is set, it will contain either: # First we check if BUILDKITE_BRANCH is defined,
# if it isn't we can assume this is a Netlify build
if [ -z ${BUILDKITE_BRANCH+x} ]; then
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
apiEndpoint+=$REVIEW_ID
head=$(curl $apiEndpoint | jq -r '.head.label')
else
head=$BUILDKITE_BRANCH
fi
# If head is set, it will contain either:
# * "branch" when the author's branch and target branch are in the same repo # * "branch" when the author's branch and target branch are in the same repo
# * "author:branch" when the author's branch is in their fork # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build
# We can split on `:` into an array to check. # We can split on `:` into an array to check.
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ }) BRANCH_ARRAY=(${head//:/ })
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "1" ]]; then if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then
clone $deforg $defrepo $BUILDKITE_BRANCH clone $deforg $defrepo $BUILDKITE_BRANCH
elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then
clone ${BUILDKITE_BRANCH_ARRAY[0]} $defrepo ${BUILDKITE_BRANCH_ARRAY[1]} clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]}
fi fi
# Try the target branch of the push or PR. # Try the target branch of the push or PR.
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH

View file

@ -212,6 +212,18 @@ export default abstract class BasePlatform {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
supportsWarnBeforeExit(): boolean {
return false;
}
async shouldWarnBeforeExit(): Promise<boolean> {
return false;
}
async setWarnBeforeExit(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
supportsAutoHideMenuBar(): boolean { supportsAutoHideMenuBar(): boolean {
return false; return false;
} }

View file

@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
export default class CallHandler { export default class CallHandler {
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
// call with a different party to this one.
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
private audioPromises = new Map<AudioID, Promise<void>>(); private audioPromises = new Map<AudioID, Promise<void>>();
private dispatcherRef: string = null; private dispatcherRef: string = null;
private supportsPstnProtocol = null; private supportsPstnProtocol = null;
@ -325,6 +328,10 @@ export default class CallHandler {
return callsNotInThatRoom; return callsNotInThatRoom;
} }
getTransfereeForCallId(callId: string): MatrixCall {
return this.transferees[callId];
}
play(audioId: AudioID) { play(audioId: AudioID) {
// TODO: Attach an invisible element for this instead // TODO: Attach an invisible element for this instead
// which listens? // which listens?
@ -622,6 +629,7 @@ export default class CallHandler {
private async placeCall( private async placeCall(
roomId: string, type: PlaceCallType, roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, 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);
@ -634,6 +642,9 @@ export default class CallHandler {
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
if (transferee) {
this.transferees[call.callId] = transferee;
}
this.setCallListeners(call); this.setCallListeners(call);
this.setCallAudioElement(call); this.setCallAudioElement(call);
@ -723,7 +734,10 @@ 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(payload.room_id, payload.type, payload.local_element, payload.remote_element); this.placeCall(
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",

View file

@ -97,7 +97,7 @@ export function formatFullDateNoTime(date: Date): string {
}); });
} }
export function formatFullDate(date: Date, showTwelveHour = false): string { export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string {
const days = getDaysArray(); const days = getDaysArray();
const months = getMonthsArray(); const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
@ -105,7 +105,7 @@ export function formatFullDate(date: Date, showTwelveHour = false): string {
monthName: months[date.getMonth()], monthName: months[date.getMonth()],
day: date.getDate(), day: date.getDate(),
fullYear: date.getFullYear(), fullYear: date.getFullYear(),
time: formatFullTime(date, showTwelveHour), time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour),
}); });
} }

407
src/KeyBindingsDefaults.ts Normal file
View file

@ -0,0 +1,407 @@
/*
Copyright 2021 Clemens Zeidler
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { AutocompleteAction, IKeyBindingsProvider, KeyBinding, MessageComposerAction, NavigationAction, RoomAction,
RoomListAction } from "./KeyBindingsManager";
import { isMac, Key } from "./Keyboard";
import SettingsStore from "./settings/SettingsStore";
const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
const bindings: KeyBinding<MessageComposerAction>[] = [
{
action: MessageComposerAction.SelectPrevSendHistory,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
ctrlKey: true,
},
},
{
action: MessageComposerAction.SelectNextSendHistory,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
ctrlKey: true,
},
},
{
action: MessageComposerAction.EditPrevMessage,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: MessageComposerAction.EditNextMessage,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
{
action: MessageComposerAction.CancelEditing,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: MessageComposerAction.FormatBold,
keyCombo: {
key: Key.B,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.FormatItalics,
keyCombo: {
key: Key.I,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.FormatQuote,
keyCombo: {
key: Key.GREATER_THAN,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: MessageComposerAction.EditUndo,
keyCombo: {
key: Key.Z,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.MoveCursorToStart,
keyCombo: {
key: Key.HOME,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.MoveCursorToEnd,
keyCombo: {
key: Key.END,
ctrlOrCmd: true,
},
},
];
if (isMac) {
bindings.push({
action: MessageComposerAction.EditRedo,
keyCombo: {
key: Key.Z,
ctrlOrCmd: true,
shiftKey: true,
},
});
} else {
bindings.push({
action: MessageComposerAction.EditRedo,
keyCombo: {
key: Key.Y,
ctrlOrCmd: true,
},
});
}
if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) {
bindings.push({
action: MessageComposerAction.Send,
keyCombo: {
key: Key.ENTER,
ctrlOrCmd: true,
},
});
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
},
});
} else {
bindings.push({
action: MessageComposerAction.Send,
keyCombo: {
key: Key.ENTER,
},
});
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
shiftKey: true,
},
});
if (isMac) {
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
altKey: true,
},
});
}
}
return bindings;
}
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
return [
{
action: AutocompleteAction.CompleteOrNextSelection,
keyCombo: {
key: Key.TAB,
},
},
{
action: AutocompleteAction.CompleteOrNextSelection,
keyCombo: {
key: Key.TAB,
ctrlKey: true,
},
},
{
action: AutocompleteAction.CompleteOrPrevSelection,
keyCombo: {
key: Key.TAB,
shiftKey: true,
},
},
{
action: AutocompleteAction.CompleteOrPrevSelection,
keyCombo: {
key: Key.TAB,
ctrlKey: true,
shiftKey: true,
},
},
{
action: AutocompleteAction.Cancel,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: AutocompleteAction.PrevSelection,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: AutocompleteAction.NextSelection,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
];
}
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
return [
{
action: RoomListAction.ClearSearch,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: RoomListAction.PrevRoom,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: RoomListAction.NextRoom,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
{
action: RoomListAction.SelectRoom,
keyCombo: {
key: Key.ENTER,
},
},
{
action: RoomListAction.CollapseSection,
keyCombo: {
key: Key.ARROW_LEFT,
},
},
{
action: RoomListAction.ExpandSection,
keyCombo: {
key: Key.ARROW_RIGHT,
},
},
];
}
const roomBindings = (): KeyBinding<RoomAction>[] => {
const bindings: KeyBinding<RoomAction>[] = [
{
action: RoomAction.ScrollUp,
keyCombo: {
key: Key.PAGE_UP,
},
},
{
action: RoomAction.RoomScrollDown,
keyCombo: {
key: Key.PAGE_DOWN,
},
},
{
action: RoomAction.DismissReadMarker,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: RoomAction.JumpToOldestUnread,
keyCombo: {
key: Key.PAGE_UP,
shiftKey: true,
},
},
{
action: RoomAction.UploadFile,
keyCombo: {
key: Key.U,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: RoomAction.JumpToFirstMessage,
keyCombo: {
key: Key.HOME,
ctrlKey: true,
},
},
{
action: RoomAction.JumpToLatestMessage,
keyCombo: {
key: Key.END,
ctrlKey: true,
},
},
];
if (SettingsStore.getValue('ctrlFForSearch')) {
bindings.push({
action: RoomAction.FocusSearch,
keyCombo: {
key: Key.F,
ctrlOrCmd: true,
},
});
}
return bindings;
}
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
return [
{
action: NavigationAction.FocusRoomSearch,
keyCombo: {
key: Key.K,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleRoomSidePanel,
keyCombo: {
key: Key.PERIOD,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleUserMenu,
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"
// was previously chosen but conflicted with italics in
// composer, so CTRL+` it is
keyCombo: {
key: Key.BACKTICK,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleShortCutDialog,
keyCombo: {
key: Key.SLASH,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleShortCutDialog,
keyCombo: {
key: Key.SLASH,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: NavigationAction.GoToHome,
keyCombo: {
key: Key.H,
ctrlKey: true,
altKey: !isMac,
shiftKey: isMac,
},
},
{
action: NavigationAction.SelectPrevRoom,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
},
},
{
action: NavigationAction.SelectNextRoom,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
},
},
{
action: NavigationAction.SelectPrevUnreadRoom,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
shiftKey: true,
},
},
{
action: NavigationAction.SelectNextUnreadRoom,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
shiftKey: true,
},
},
];
}
export const defaultBindingsProvider: IKeyBindingsProvider = {
getMessageComposerBindings: messageComposerBindings,
getAutocompleteBindings: autocompleteBindings,
getRoomListBindings: roomListBindings,
getRoomBindings: roomBindings,
getNavigationBindings: navigationBindings,
}

271
src/KeyBindingsManager.ts Normal file
View file

@ -0,0 +1,271 @@
/*
Copyright 2021 Clemens Zeidler
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { defaultBindingsProvider } from './KeyBindingsDefaults';
import { isMac } from './Keyboard';
/** Actions for the chat message composer component */
export enum MessageComposerAction {
/** Send a message */
Send = 'Send',
/** Go backwards through the send history and use the message in composer view */
SelectPrevSendHistory = 'SelectPrevSendHistory',
/** Go forwards through the send history */
SelectNextSendHistory = 'SelectNextSendHistory',
/** Start editing the user's last sent message */
EditPrevMessage = 'EditPrevMessage',
/** Start editing the user's next sent message */
EditNextMessage = 'EditNextMessage',
/** Cancel editing a message or cancel replying to a message */
CancelEditing = 'CancelEditing',
/** Set bold format the current selection */
FormatBold = 'FormatBold',
/** Set italics format the current selection */
FormatItalics = 'FormatItalics',
/** Format the current selection as quote */
FormatQuote = 'FormatQuote',
/** Undo the last editing */
EditUndo = 'EditUndo',
/** Redo editing */
EditRedo = 'EditRedo',
/** Insert new line */
NewLine = 'NewLine',
/** Move the cursor to the start of the message */
MoveCursorToStart = 'MoveCursorToStart',
/** Move the cursor to the end of the message */
MoveCursorToEnd = 'MoveCursorToEnd',
}
/** Actions for text editing autocompletion */
export enum AutocompleteAction {
/**
* Select previous selection or, if the autocompletion window is not shown, open the window and select the first
* selection.
*/
CompleteOrPrevSelection = 'ApplySelection',
/** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */
CompleteOrNextSelection = 'CompleteOrNextSelection',
/** Move to the previous autocomplete selection */
PrevSelection = 'PrevSelection',
/** Move to the next autocomplete selection */
NextSelection = 'NextSelection',
/** Close the autocompletion window */
Cancel = 'Cancel',
}
/** Actions for the room list sidebar */
export enum RoomListAction {
/** Clear room list filter field */
ClearSearch = 'ClearSearch',
/** Navigate up/down in the room list */
PrevRoom = 'PrevRoom',
/** Navigate down in the room list */
NextRoom = 'NextRoom',
/** Select room from the room list */
SelectRoom = 'SelectRoom',
/** Collapse room list section */
CollapseSection = 'CollapseSection',
/** Expand room list section, if already expanded, jump to first room in the selection */
ExpandSection = 'ExpandSection',
}
/** Actions for the current room view */
export enum RoomAction {
/** Scroll up in the timeline */
ScrollUp = 'ScrollUp',
/** Scroll down in the timeline */
RoomScrollDown = 'RoomScrollDown',
/** Dismiss read marker and jump to bottom */
DismissReadMarker = 'DismissReadMarker',
/** Jump to oldest unread message */
JumpToOldestUnread = 'JumpToOldestUnread',
/** Upload a file */
UploadFile = 'UploadFile',
/** Focus search message in a room (must be enabled) */
FocusSearch = 'FocusSearch',
/** Jump to the first (downloaded) message in the room */
JumpToFirstMessage = 'JumpToFirstMessage',
/** Jump to the latest message in the room */
JumpToLatestMessage = 'JumpToLatestMessage',
}
/** Actions for navigating do various menus, dialogs or screens */
export enum NavigationAction {
/** Jump to room search (search for a room) */
FocusRoomSearch = 'FocusRoomSearch',
/** Toggle the room side panel */
ToggleRoomSidePanel = 'ToggleRoomSidePanel',
/** Toggle the user menu */
ToggleUserMenu = 'ToggleUserMenu',
/** Toggle the short cut help dialog */
ToggleShortCutDialog = 'ToggleShortCutDialog',
/** Got to the Element home screen */
GoToHome = 'GoToHome',
/** Select prev room */
SelectPrevRoom = 'SelectPrevRoom',
/** Select next room */
SelectNextRoom = 'SelectNextRoom',
/** Select prev room with unread messages */
SelectPrevUnreadRoom = 'SelectPrevUnreadRoom',
/** Select next room with unread messages */
SelectNextUnreadRoom = 'SelectNextUnreadRoom',
}
/**
* Represent a key combination.
*
* The combo is evaluated strictly, i.e. the KeyboardEvent must match exactly what is specified in the KeyCombo.
*/
export type KeyCombo = {
key?: string;
/** On PC: ctrl is pressed; on Mac: meta is pressed */
ctrlOrCmd?: boolean;
altKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
}
export type KeyBinding<T extends string> = {
action: T;
keyCombo: KeyCombo;
}
/**
* Helper method to check if a KeyboardEvent matches a KeyCombo
*
* Note, this method is only exported for testing.
*/
export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo: KeyCombo, onMac: boolean): boolean {
if (combo.key !== undefined) {
// When shift is pressed, letters are returned as upper case chars. In this case do a lower case comparison.
// This works for letter combos such as shift + U as well for none letter combos such as shift + Escape.
// If shift is not pressed, the toLowerCase conversion can be avoided.
if (ev.shiftKey) {
if (ev.key.toLowerCase() !== combo.key.toLowerCase()) {
return false;
}
} else if (ev.key !== combo.key) {
return false;
}
}
const comboCtrl = combo.ctrlKey ?? false;
const comboAlt = combo.altKey ?? false;
const comboShift = combo.shiftKey ?? false;
const comboMeta = combo.metaKey ?? false;
// Tests mock events may keep the modifiers undefined; convert them to booleans
const evCtrl = ev.ctrlKey ?? false;
const evAlt = ev.altKey ?? false;
const evShift = ev.shiftKey ?? false;
const evMeta = ev.metaKey ?? false;
// When ctrlOrCmd is set, the keys need do evaluated differently on PC and Mac
if (combo.ctrlOrCmd) {
if (onMac) {
if (!evMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
} else {
if (!evCtrl
|| evMeta !== comboMeta
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
}
return true;
}
if (evMeta !== comboMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
return true;
}
export type KeyBindingGetter<T extends string> = () => KeyBinding<T>[];
export interface IKeyBindingsProvider {
getMessageComposerBindings: KeyBindingGetter<MessageComposerAction>;
getAutocompleteBindings: KeyBindingGetter<AutocompleteAction>;
getRoomListBindings: KeyBindingGetter<RoomListAction>;
getRoomBindings: KeyBindingGetter<RoomAction>;
getNavigationBindings: KeyBindingGetter<NavigationAction>;
}
export class KeyBindingsManager {
/**
* List of key bindings providers.
*
* Key bindings from the first provider(s) in the list will have precedence over key bindings from later providers.
*
* To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for
* customized key bindings.
*/
bindingsProviders: IKeyBindingsProvider[] = [
defaultBindingsProvider,
];
/**
* Finds a matching KeyAction for a given KeyboardEvent
*/
private getAction<T extends string>(getters: KeyBindingGetter<T>[], ev: KeyboardEvent | React.KeyboardEvent)
: T | undefined {
for (const getter of getters) {
const bindings = getter();
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
if (binding) {
return binding.action;
}
}
return undefined;
}
getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): MessageComposerAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev);
}
getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): AutocompleteAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev);
}
getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): RoomListAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev);
}
getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): RoomAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev);
}
getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): NavigationAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev);
}
}
const manager = new KeyBindingsManager();
export function getKeyBindingsManager(): KeyBindingsManager {
return manager;
}

View file

@ -36,6 +36,7 @@ export interface IModal<T extends any[]> {
onBeforeClose?(reason?: string): Promise<boolean>; onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void; onFinished(...args: T): void;
close(...args: T): void; close(...args: T): void;
hidden?: boolean;
} }
export interface IHandle<T extends any[]> { export interface IHandle<T extends any[]> {
@ -93,6 +94,12 @@ export class ModalManager {
return container; return container;
} }
public toggleCurrentDialogVisibility() {
const modal = this.getCurrentModal();
if (!modal) return;
modal.hidden = !modal.hidden;
}
public hasDialogs() { public hasDialogs() {
return this.priorityModal || this.staticModal || this.modals.length > 0; return this.priorityModal || this.staticModal || this.modals.length > 0;
} }
@ -364,7 +371,7 @@ export class ModalManager {
} }
const modal = this.getCurrentModal(); const modal = this.getCurrentModal();
if (modal !== this.staticModal) { if (modal !== this.staticModal && !modal.hidden) {
const classes = classNames("mx_Dialog_wrapper", modal.className, { const classes = classNames("mx_Dialog_wrapper", modal.className, {
mx_Dialog_wrapperWithStaticUnder: this.staticModal, mx_Dialog_wrapperWithStaticUnder: this.staticModal,
}); });

View file

@ -1,16 +1,15 @@
import React from "react"; import React from "react";
import ReactDom from "react-dom"; import ReactDom from "react-dom";
import Velocity from "velocity-animate";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
/** /**
* The Velociraptor contains components and animates transitions with velocity. * The NodeAnimator contains components and animates transitions.
* It will only pick up direct changes to properties ('left', currently), and so * It will only pick up direct changes to properties ('left', currently), and so
* will not work for animating positional changes where the position is implicit * will not work for animating positional changes where the position is implicit
* from DOM order. This makes it a lot simpler and lighter: if you need fully * from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries. * automatic positional animation, look at react-shuffle or similar libraries.
*/ */
export default class Velociraptor extends React.Component { export default class NodeAnimator extends React.Component {
static propTypes = { static propTypes = {
// either a list of child nodes, or a single child. // either a list of child nodes, or a single child.
children: PropTypes.any, children: PropTypes.any,
@ -20,14 +19,10 @@ export default class Velociraptor extends React.Component {
// a list of state objects to apply to each child node in turn // a list of state objects to apply to each child node in turn
startStyles: PropTypes.array, startStyles: PropTypes.array,
// a list of transition options from the corresponding startStyle
enterTransitionOpts: PropTypes.array,
}; };
static defaultProps = { static defaultProps = {
startStyles: [], startStyles: [],
enterTransitionOpts: [],
}; };
constructor(props) { constructor(props) {
@ -41,6 +36,18 @@ export default class Velociraptor extends React.Component {
this._updateChildren(this.props.children); this._updateChildren(this.props.children);
} }
/**
*
* @param {HTMLElement} node element to apply styles to
* @param {object} styles a key/value pair of CSS properties
* @returns {void}
*/
_applyStyles(node, styles) {
Object.entries(styles).forEach(([property, value]) => {
node.style[property] = value;
});
}
_updateChildren(newChildren) { _updateChildren(newChildren) {
const oldChildren = this.children || {}; const oldChildren = this.children || {};
this.children = {}; this.children = {};
@ -50,17 +57,8 @@ export default class Velociraptor extends React.Component {
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]); const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left !== c.props.style.left) { if (oldNode && oldNode.style.left !== c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => { this._applyStyles(oldNode, { left: c.props.style.left });
// special case visibility because it's nonsensical to animate an invisible element // console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
// so we always hidden->visible pre-transition and visible->hidden after
if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
oldNode.style.visibility = c.props.style.visibility;
}
});
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
oldNode.style.visibility = c.props.style.visibility;
} }
// clone the old element with the props (and children) of the new element // clone the old element with the props (and children) of the new element
// so prop updates are still received by the children. // so prop updates are still received by the children.
@ -94,33 +92,22 @@ export default class Velociraptor extends React.Component {
this.props.startStyles.length > 0 this.props.startStyles.length > 0
) { ) {
const startStyles = this.props.startStyles; const startStyles = this.props.startStyles;
const transitionOpts = this.props.enterTransitionOpts;
const domNode = ReactDom.findDOMNode(node); const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it // start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc. // to start with, so now we animate 1 etc.
for (var i = 1; i < startStyles.length; ++i) { for (let i = 1; i < startStyles.length; ++i) {
Velocity(domNode, startStyles[i], transitionOpts[i-1]); this._applyStyles(domNode, startStyles[i]);
/* // console.log("start:"
console.log("start:", // JSON.stringify(startStyles[i]),
JSON.stringify(transitionOpts[i-1]), // );
"->",
JSON.stringify(startStyles[i]),
);
*/
} }
// and then we animate to the resting state // and then we animate to the resting state
Velocity(domNode, restingStyle, setTimeout(() => {
transitionOpts[i-1]) this._applyStyles(domNode, restingStyle);
.then(() => { }, 0);
// once we've reached the resting state, hide the element if
// appropriate
domNode.style.visibility = restingStyle.visibility;
});
// console.log("enter:", // console.log("enter:",
// JSON.stringify(transitionOpts[i-1]),
// "->",
// JSON.stringify(restingStyle)); // JSON.stringify(restingStyle));
} }
this.nodes[k] = node; this.nodes[k] = node;
@ -128,9 +115,7 @@ export default class Velociraptor extends React.Component {
render() { render() {
return ( return (
<span> <>{ Object.values(this.children) }</>
{ Object.values(this.children) }
</span>
); );
} }
} }

View file

@ -383,6 +383,10 @@ export const Notifier = {
// don't bother notifying as user was recently active in this room // don't bother notifying as user was recently active in this room
return; return;
} }
if (SettingsStore.getValue("doNotDisturb")) {
// Don't bother the user if they didn't ask to be bothered
return;
}
if (this.isEnabled()) { if (this.isEnabled()) {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);

View file

@ -395,6 +395,8 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
} catch (e) { } catch (e) {
SecurityCustomisations.catchAccessSecretStorageError?.(e); SecurityCustomisations.catchAccessSecretStorageError?.(e);
console.error(e); console.error(e);
// Re-throw so that higher level logic can abort as needed
throw e;
} finally { } finally {
// Clear secret storage key cache now that work is complete // Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false; secretStorageBeingAccessed = false;

View file

@ -20,7 +20,7 @@ limitations under the License.
import * as React from 'react'; import * as React from 'react';
import { ContentHelpers } from 'matrix-js-sdk'; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
@ -155,6 +155,18 @@ function success(promise?: Promise<any>) {
*/ */
export const Commands = [ export const Commands = [
new Command({
command: 'spoiler',
args: '<message>',
description: _td('Sends the given message as a spoiler'),
runFn: function(roomId, message) {
return success(ContentHelpers.makeHtmlMessage(
message,
`<span data-mx-spoiler>${message}</span>`,
));
},
category: CommandCategories.messages,
}),
new Command({ new Command({
command: 'shrug', command: 'shrug',
args: '<message>', args: '<message>',

View file

@ -95,9 +95,10 @@ function textForMemberEvent(ev) {
senderName, senderName,
targetName, targetName,
}) + ' ' + reason; }) + ' ' + reason;
} else { } else if (prevContent.membership === "join") {
// sender is not target and made the target leave, if not from invite/ban then this is a kick
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason; return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
} else {
return "";
} }
} }
} }

View file

@ -1,17 +0,0 @@
import Velocity from "velocity-animate";
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
let pow2;
let bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2
}
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
}
Velocity.Easings.easeOutBounce = function(p) {
return 1 - bounce(1 - p);
};

View file

@ -265,7 +265,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
description: _td("Toggle this dialog"), description: _td("Toggle this dialog"),
}, { }, {
keybinds: [{ keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.ALT], modifiers: [Modifiers.CONTROL, isMac ? Modifiers.SHIFT : Modifiers.ALT],
key: Key.H, key: Key.H,
}], }],
description: _td("Go to Home View"), description: _td("Go to Home View"),

View file

@ -23,7 +23,6 @@ interface IOptions<T extends {}> {
keys: Array<string | keyof T>; keys: Array<string | keyof T>;
funcs?: Array<(T) => string>; funcs?: Array<(T) => string>;
shouldMatchWordsOnly?: boolean; shouldMatchWordsOnly?: boolean;
shouldMatchPrefix?: boolean;
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
fuzzy?: boolean; fuzzy?: boolean;
} }
@ -56,12 +55,6 @@ export default class QueryMatcher<T extends Object> {
if (this._options.shouldMatchWordsOnly === undefined) { if (this._options.shouldMatchWordsOnly === undefined) {
this._options.shouldMatchWordsOnly = true; this._options.shouldMatchWordsOnly = true;
} }
// By default, match anywhere in the string being searched. If enabled, only return
// matches that are prefixed with the query.
if (this._options.shouldMatchPrefix === undefined) {
this._options.shouldMatchPrefix = false;
}
} }
setObjects(objects: T[]) { setObjects(objects: T[]) {
@ -112,7 +105,7 @@ export default class QueryMatcher<T extends Object> {
resultKey = resultKey.replace(/[^\w]/g, ''); resultKey = resultKey.replace(/[^\w]/g, '');
} }
const index = resultKey.indexOf(query); const index = resultKey.indexOf(query);
if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { if (index !== -1) {
matches.push( matches.push(
...candidates.map((candidate) => ({index, ...candidate})), ...candidates.map((candidate) => ({index, ...candidate})),
); );

View file

@ -56,7 +56,6 @@ export default class UserProvider extends AutocompleteProvider {
this.matcher = new QueryMatcher([], { this.matcher = new QueryMatcher([], {
keys: ['name'], keys: ['name'],
funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@' funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@'
shouldMatchPrefix: true,
shouldMatchWordsOnly: false, shouldMatchWordsOnly: false,
}); });

View file

@ -981,7 +981,7 @@ export default class GroupView extends React.Component {
<Spinner /> <Spinner />
</div>; </div>;
} }
const httpInviterAvatar = this.state.inviterProfile const httpInviterAvatar = this.state.inviterProfile && this.state.inviterProfile.avatarUrl
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
: null; : null;

View file

@ -34,7 +34,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
@ -43,6 +42,7 @@ import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media"; import {mediaFromMxc} from "../../customisations/Media";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -297,17 +297,18 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
if (!this.focusedElement) return; if (!this.focusedElement) return;
switch (ev.key) { const action = getKeyBindingsManager().getRoomListAction(ev);
case Key.ARROW_UP: switch (action) {
case Key.ARROW_DOWN: case RoomListAction.NextRoom:
case RoomListAction.PrevRoom:
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.onMoveFocus(ev.key === Key.ARROW_UP); this.onMoveFocus(action === RoomListAction.PrevRoom);
break; break;
} }
}; };
private onEnter = () => { private selectRoom = () => {
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile"); const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
if (firstRoom) { if (firstRoom) {
firstRoom.click(); firstRoom.click();
@ -388,8 +389,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
> >
<RoomSearch <RoomSearch
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown} onKeyDown={this.onKeyDown}
onEnter={this.onEnter} onSelectRoom={this.selectRoom}
/> />
<AccessibleTooltipButton <AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", { className={classNames("mx_LeftPanel_exploreButton", {

View file

@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard'; import {Key} from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
@ -55,6 +55,7 @@ import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBindingsManager';
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";
@ -436,86 +437,55 @@ class LoggedInView extends React.Component<IProps, IState> {
_onKeyDown = (ev) => { _onKeyDown = (ev) => {
let handled = false; let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
const modKey = isMac ? ev.metaKey : ev.ctrlKey;
switch (ev.key) { const roomAction = getKeyBindingsManager().getRoomAction(ev);
case Key.PAGE_UP: switch (roomAction) {
case Key.PAGE_DOWN: case RoomAction.ScrollUp:
if (!hasModifier && !isModifier) { case RoomAction.RoomScrollDown:
this._onScrollKeyPressed(ev); case RoomAction.JumpToFirstMessage:
handled = true; case RoomAction.JumpToLatestMessage:
} // pass the event down to the scroll panel
this._onScrollKeyPressed(ev);
handled = true;
break; break;
case RoomAction.FocusSearch:
dis.dispatch({
action: 'focus_search',
});
handled = true;
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
return;
}
case Key.HOME: const navAction = getKeyBindingsManager().getNavigationAction(ev);
case Key.END: switch (navAction) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { case NavigationAction.FocusRoomSearch:
this._onScrollKeyPressed(ev); dis.dispatch({
handled = true; action: 'focus_room_filter',
} });
handled = true;
break; break;
case Key.K: case NavigationAction.ToggleUserMenu:
if (ctrlCmdOnly) { dis.fire(Action.ToggleUserMenu);
dis.dispatch({ handled = true;
action: 'focus_room_filter',
});
handled = true;
}
break; break;
case Key.F: case NavigationAction.ToggleShortCutDialog:
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) { KeyboardShortcuts.toggleDialog();
dis.dispatch({ handled = true;
action: 'focus_search',
});
handled = true;
}
break; break;
case Key.BACKTICK: case NavigationAction.GoToHome:
// Ideally this would be CTRL+P for "Profile", but that's dis.dispatch({
// taken by the print dialog. CTRL+I for "Information" action: 'view_home_page',
// was previously chosen but conflicted with italics in });
// composer, so CTRL+` it is Modal.closeCurrentModal("homeKeyboardShortcut");
handled = true;
if (ctrlCmdOnly) {
dis.fire(Action.ToggleUserMenu);
handled = true;
}
break; break;
case NavigationAction.ToggleRoomSidePanel:
case Key.SLASH: if (this.props.page_type === "room_view" || this.props.page_type === "group_view") {
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
KeyboardShortcuts.toggleDialog();
handled = true;
}
break;
case Key.H:
if (ev.altKey && modKey) {
dis.dispatch({
action: 'view_home_page',
});
Modal.closeCurrentModal("homeKeyboardShortcut");
handled = true;
}
break;
case Key.ARROW_UP:
case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: ev.key === Key.ARROW_UP ? -1 : 1,
unread: ev.shiftKey,
});
handled = true;
}
break;
case Key.PERIOD:
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
dis.dispatch<ToggleRightPanelPayload>({ dis.dispatch<ToggleRightPanelPayload>({
action: Action.ToggleRightPanel, action: Action.ToggleRightPanel,
type: this.props.page_type === "room_view" ? "room" : "group", type: this.props.page_type === "room_view" ? "room" : "group",
@ -523,16 +493,48 @@ class LoggedInView extends React.Component<IProps, IState> {
handled = true; handled = true;
} }
break; break;
case NavigationAction.SelectPrevRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: -1,
unread: false,
});
handled = true;
break;
case NavigationAction.SelectNextRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: 1,
unread: false,
});
handled = true;
break;
case NavigationAction.SelectPrevUnreadRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: -1,
unread: true,
});
break;
case NavigationAction.SelectNextUnreadRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: 1,
unread: true,
});
break;
default: default:
// if we do not have a handler for it, pass it to the platform which might // if we do not have a handler for it, pass it to the platform which might
handled = PlatformPeg.get().onKeyDown(ev); handled = PlatformPeg.get().onKeyDown(ev);
} }
if (handled) { if (handled) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { return;
}
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// The above condition is crafted to _allow_ characters with Shift // The above condition is crafted to _allow_ characters with Shift
// already pressed (but not the Shift key down itself). // already pressed (but not the Shift key down itself).

View file

@ -80,10 +80,11 @@ import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages"; import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models"; import {RoomUpdateCause} from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -395,7 +396,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) { if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {
this.onLoggedIn();
} else {
this.setStateForNewView({view: Views.COMPLETE_SECURITY});
}
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP }); this.setStateForNewView({ view: Views.E2E_SETUP });
} else { } else {
@ -690,10 +695,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) { if (SpaceStore.instance.activeSpace) {
Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, { defaultDispatcher.dispatch({
space: SpaceStore.instance.activeSpace, action: "view_room",
initialText: payload.initialText, room_id: SpaceStore.instance.activeSpace.roomId,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true); });
} else { } else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, { Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
@ -1554,7 +1559,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (request.pending) { } else if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
key: 'verifreq_' + request.channel.transactionId, key: 'verifreq_' + request.channel.transactionId,
title: request.isSelfVerification ? _t("Self-verification request") : _t("Verification Request"), title: _t("Verification requested"),
icon: "verification", icon: "verification",
props: {request}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), component: sdk.getComponent("toasts.VerificationRequestToast"),

View file

@ -659,6 +659,7 @@ export default class MessagePanel extends React.Component {
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
layout={this.props.layout} layout={this.props.layout}
enableFlair={this.props.enableFlair} enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/> />
</TileErrorBoundary> </TileErrorBoundary>
</li>, </li>,

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.
@ -20,17 +20,21 @@ import classNames from "classnames";
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";
import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore"; 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 {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent): void; onKeyDown(ev: React.KeyboardEvent): void;
onEnter(ev: React.KeyboardEvent): boolean; /**
* @returns true if a room has been selected and the search field should be cleared
*/
onSelectRoom(): boolean;
} }
interface IState { interface IState {
@ -53,6 +57,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
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
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void { public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -72,6 +78,7 @@ 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);
} }
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
@ -108,18 +115,26 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.key === Key.ESCAPE) { const action = getKeyBindingsManager().getRoomListAction(ev);
this.clearInput(); switch (action) {
defaultDispatcher.fire(Action.FocusComposer); case RoomListAction.ClearSearch:
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { this.clearInput();
this.props.onVerticalArrow(ev); defaultDispatcher.fire(Action.FocusComposer);
} else if (ev.key === Key.ENTER) { break;
const shouldClear = this.props.onEnter(ev); case RoomListAction.NextRoom:
if (shouldClear) { case RoomListAction.PrevRoom:
// wrap in set immediate to delay it so that we don't clear the filter & then change room // we don't handle these actions here put pass the event on to the interested party (LeftPanel)
setImmediate(() => { this.props.onKeyDown(ev);
this.clearInput(); break;
}); case RoomListAction.SelectRoom: {
const shouldClear = this.props.onSelectRoom();
if (shouldClear) {
// wrap in set immediate to delay it so that we don't clear the filter & then change room
setImmediate(() => {
this.clearInput();
});
}
break;
} }
} }
}; };

View file

@ -40,7 +40,6 @@ import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc'; import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching'; import eventSearch, { searchPagination } from '../../Searching';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
import MainSplit from './MainSplit'; import MainSplit from './MainSplit';
import RightPanel from './RightPanel'; import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
@ -79,6 +78,7 @@ import Notifier from "../../Notifier";
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast"; import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects"; import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
@ -662,26 +662,20 @@ export default class RoomView extends React.Component<IProps, IState> {
private onReactKeyDown = ev => { private onReactKeyDown = ev => {
let handled = false; let handled = false;
switch (ev.key) { const action = getKeyBindingsManager().getRoomAction(ev);
case Key.ESCAPE: switch (action) {
if (!ev.altKey && !ev.ctrlKey && !ev.shiftKey && !ev.metaKey) { case RoomAction.DismissReadMarker:
this.messagePanel.forgetReadMarker(); this.messagePanel.forgetReadMarker();
this.jumpToLiveTimeline(); this.jumpToLiveTimeline();
handled = true; handled = true;
}
break; break;
case Key.PAGE_UP: case RoomAction.JumpToOldestUnread:
if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) { this.jumpToReadMarker();
this.jumpToReadMarker(); handled = true;
handled = true;
}
break; break;
case Key.U: // Mac returns lowercase case RoomAction.UploadFile:
case Key.U.toUpperCase(): dis.dispatch({ action: "upload_file" }, true);
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) { handled = true;
dis.dispatch({ action: "upload_file" }, true);
handled = true;
}
break; break;
} }
@ -1143,10 +1137,16 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.setState({ // We always increment the counter no matter the types, because dragging is
dragCounter: this.state.dragCounter + 1, // still happening. If we didn't, the drag counter would get out of sync.
draggingFile: true, this.setState({dragCounter: this.state.dragCounter + 1});
});
// See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
this.setState({draggingFile: true});
}
}; };
private onDragLeave = ev => { private onDragLeave = ev => {
@ -1170,6 +1170,9 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
// See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) { if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
} }

View file

@ -16,10 +16,10 @@ limitations under the License.
import React, {createRef} from "react"; import React, {createRef} from "react";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
@ -535,29 +535,19 @@ export default class ScrollPanel extends React.Component {
* @param {object} ev the keyboard event * @param {object} ev the keyboard event
*/ */
handleScrollKey = ev => { handleScrollKey = ev => {
switch (ev.key) { const roomAction = getKeyBindingsManager().getRoomAction(ev);
case Key.PAGE_UP: switch (roomAction) {
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { case RoomAction.ScrollUp:
this.scrollRelative(-1); this.scrollRelative(-1);
}
break; break;
case RoomAction.RoomScrollDown:
case Key.PAGE_DOWN: this.scrollRelative(1);
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(1);
}
break; break;
case RoomAction.JumpToFirstMessage:
case Key.HOME: this.scrollToTop();
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToTop();
}
break; break;
case RoomAction.JumpToLatestMessage:
case Key.END: this.scrollToBottom();
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToBottom();
}
break; break;
} }
}; };

View file

@ -40,10 +40,11 @@ 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";
interface IProps { interface IHierarchyProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
onFinished(): void; refreshToken?: any;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -111,7 +112,7 @@ const Tile: React.FC<ITileProps> = ({
let button; let button;
if (myMembership === "join") { if (myMembership === "join") {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline"> button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("Open") } { _t("View") }
</AccessibleButton>; </AccessibleButton>;
} else if (onJoinClick) { } else if (onJoinClick) {
button = <AccessibleButton onClick={onJoinClick} kind="primary"> button = <AccessibleButton onClick={onJoinClick} kind="primary">
@ -251,7 +252,7 @@ export const HierarchyLevel = ({
}: IHierarchyLevelProps) => { }: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
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 sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
@ -344,22 +345,20 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
}, [space, refreshToken], []); }, [space, refreshToken], []);
}; };
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => { export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space,
initialText = "",
showRoom,
refreshToken,
children,
}) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId(); const userId = cli.getUserId();
const [query, setQuery] = useState(initialText); const [query, setQuery] = useState(initialText);
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
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); const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;
@ -394,21 +393,6 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
return roomsMap; return roomsMap;
}, [rooms, childParentMap, query]); }, [rooms, childParentMap, query]);
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", null,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [removing, setRemoving] = useState(false); const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -503,6 +487,8 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
let results; let results;
if (roomsMap.size) { if (roomsMap.size) {
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
results = <> results = <>
<HierarchyLevel <HierarchyLevel
spaceId={space.roomId} spaceId={space.roomId}
@ -510,7 +496,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
relations={parentChildMap} relations={parentChildMap}
parents={new Set()} parents={new Set()}
selectedMap={selected} selectedMap={selected}
onToggleClick={(parentId, childId) => { onToggleClick={hasPermissions ? (parentId, childId) => {
setError(""); setError("");
if (!selected.has(parentId)) { if (!selected.has(parentId)) {
setSelected(new Map(selected.set(parentId, new Set([childId])))); setSelected(new Map(selected.set(parentId, new Set([childId]))));
@ -525,13 +511,12 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
parentSet.delete(childId); parentSet.delete(childId);
setSelected(new Map(selected.set(parentId, new Set(parentSet)))); setSelected(new Map(selected.set(parentId, new Set(parentSet))));
}} } : undefined}
onViewRoomClick={(roomId, autoJoin) => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
onFinished();
}} }}
/> />
<hr /> { children && <hr /> }
</>; </>;
} else { } else {
results = <div className="mx_SpaceRoomDirectory_noResults"> results = <div className="mx_SpaceRoomDirectory_noResults">
@ -550,34 +535,78 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
</div> } </div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list"> <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
{ results } { results }
<AccessibleButton { children }
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</AutoHideScrollbar> </AutoHideScrollbar>
</>; </>;
} else { } else if (!rooms) {
content = <Spinner />; content = <Spinner />;
} else {
content = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
} }
// TODO loading state/error state // TODO loading state/error state
return <>
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") }
onSearch={setQuery}
autoFocus={true}
initialValue={initialText}
/>
{ content }
</>;
};
interface IProps {
space: Room;
initialText?: string;
onFinished(): void;
}
const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }) => {
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
return ( return (
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}> <BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{ explanation } { _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
null,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
) }
<SearchBox <SpaceHierarchy
className="mx_textinput_icon mx_textinput_search" space={space}
placeholder={ _t("Search names and description") } showRoom={(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
onSearch={setQuery} showRoom(room, viaServers, autoJoin);
autoFocus={true} onFinished();
initialValue={initialText} }}
/> initialText={initialText}
>
{ content } <AccessibleButton
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</SpaceHierarchy>
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {RefObject, useContext, useMemo, useRef, useState} from "react"; import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {EventSubscription} from "fbemitter"; import {EventSubscription} from "fbemitter";
@ -46,11 +46,11 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel
import {useStateArray} from "../../hooks/useStateArray"; import {useStateArray} from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare"; import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory";
import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar"; 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";
interface IProps { interface IProps {
space: Room; space: Room;
@ -92,6 +92,41 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceInfo = ({ space }) => {
const joinRule = space.getJoinRule();
let visibilitySection;
if (joinRule === "public") {
visibilitySection = <span className="mx_SpaceRoomView_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_info">
{ visibilitySection }
{ joinRule === "public" && <RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount> }
</div>
};
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
@ -158,43 +193,13 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
joinButtons = <InlineSpinner />; joinButtons = <InlineSpinner />;
} }
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview"> return <div className="mx_SpaceRoomView_preview">
{ inviterSection } { inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name"> <h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} /> <RoomName room={space} />
</h1> </h1>
<div className="mx_SpaceRoomView_preview_info"> <SpaceInfo space={space} />
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}> <RoomTopic room={space}>
{(topic, ref) => {(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}> <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
@ -202,6 +207,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div> </div>
} }
</RoomTopic> </RoomTopic>
{ space.getJoinRule() === "public" && <FacePile room={space} /> }
<div className="mx_SpaceRoomView_preview_joinButtons"> <div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons } { joinButtons }
</div> </div>
@ -216,10 +222,14 @@ const SpaceLanding = ({ space }) => {
let inviteButton; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
<AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => { <AccessibleButton
showRoomInviteDialog(space.roomId); kind="primary"
}}> className="mx_SpaceRoomView_landing_inviteButton"
{ _t("Invite people") } onClick={() => {
showRoomInviteDialog(space.roomId);
}}
>
{ _t("Invite") }
</AccessibleButton> </AccessibleButton>
); );
} }
@ -256,36 +266,13 @@ const SpaceLanding = ({ space }) => {
</AccessibleButton>; </AccessibleButton>;
} }
const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken); const onMembersClick = () => {
const [roomsMap, numRooms] = useMemo(() => { defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
if (!rooms) return []; action: Action.SetRightPanelPhase,
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r])); phase: RightPanelPhases.RoomMemberList,
const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length; refireParams: { space },
return [roomsMap, numRooms]; });
}, [rooms]); };
let previewRooms;
if (roomsMap) {
previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<div className="mx_SpaceRoomDirectory_roomCount">
<h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
<span>{ numRooms }</span>
</div>
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
relations={relations}
parents={new Set()}
onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
}}
/>
</AutoHideScrollbar>;
} else if (!rooms) {
previewRooms = <InlineSpinner />;
} else {
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
}
return <div className="mx_SpaceRoomView_landing"> return <div className="mx_SpaceRoomView_landing">
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
@ -294,45 +281,26 @@ const SpaceLanding = ({ space }) => {
{(name) => { {(name) => {
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow"> const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
<h1>{ name }</h1> <h1>{ name }</h1>
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_landing_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div> }; </div> };
if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else {
return _t("Your private space <name/>", {}, tags) as JSX.Element;
}
}
return _t("Welcome to <name/>", {}, tags) as JSX.Element; return _t("Welcome to <name/>", {}, tags) as JSX.Element;
}} }}
</RoomName> </RoomName>
</div> </div>
<div className="mx_SpaceRoomView_landing_info">
<SpaceInfo space={space} />
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
{ inviteButton }
</div>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
<hr />
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton }
{ addRoomButtons } { addRoomButtons }
{ settingsButton } { settingsButton }
</div> </div>
{ previewRooms } <SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
</div>; </div>;
}; };
@ -675,9 +643,13 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What are some things you want to discuss?")} title={_t("What are some things you want to discuss in %(spaceName)s?", {
description={_t("Let's create a room for each of them. " + spaceName: this.props.space.name,
"You can add more later too, including already existing ones.")} })}
description={
_t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.")
}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:

View file

@ -43,7 +43,11 @@ export default class UploadBar extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {uploadsHere: []};
// Set initial state to any available upload in this room - we might be mounting
// earlier than the first progress event, so should show something relevant.
const uploadsHere = this.getUploadsInRoom();
this.state = {currentUpload: uploadsHere[0], uploadsHere};
} }
componentDidMount() { componentDidMount() {
@ -56,6 +60,11 @@ export default class UploadBar extends React.Component<IProps, IState> {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
private getUploadsInRoom(): IUpload[] {
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
return uploads.filter(u => u.roomId === this.props.room.roomId);
}
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
switch (payload.action) { switch (payload.action) {
case Action.UploadStarted: case Action.UploadStarted:
@ -64,8 +73,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
case Action.UploadCanceled: case Action.UploadCanceled:
case Action.UploadFailed: { case Action.UploadFailed: {
if (!this.mounted) return; if (!this.mounted) return;
const uploads = ContentMessages.sharedInstance().getCurrentUploads(); const uploadsHere = this.getUploadsInRoom();
const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
this.setState({currentUpload: uploadsHere[0], uploadsHere}); this.setState({currentUpload: uploadsHere[0], uploadsHere});
break; break;
} }

View file

@ -74,6 +74,7 @@ interface IState {
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private themeWatcherRef: string; private themeWatcherRef: string;
private dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef(); private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription; private tagStoreRef: fbEmitter.EventSubscription;
@ -89,6 +90,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_spaces")) { if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
} }
// Force update is the easiest way to trigger the UI update (we don't store state for this)
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
} }
private get hasHomePage(): boolean { private get hasHomePage(): boolean {
@ -103,6 +107,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove(); this.tagStoreRef.remove();
@ -288,6 +293,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onDndToggle = (ev) => {
ev.stopPropagation();
const current = SettingsStore.getValue("doNotDisturb");
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
};
private renderContextMenu = (): React.ReactNode => { private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null; if (!this.state.contextMenuPosition) return null;
@ -534,6 +545,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
{/* masked image in CSS */} {/* masked image in CSS */}
</span> </span>
); );
let dnd;
if (this.state.selectedSpace) { if (this.state.selectedSpace) {
name = ( name = (
<div className="mx_UserMenu_doubleName"> <div className="mx_UserMenu_doubleName">
@ -560,6 +572,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
</div> </div>
); );
isPrototype = true; isPrototype = true;
} else if (SettingsStore.getValue("feature_dnd")) {
const isDnd = SettingsStore.getValue("doNotDisturb");
dnd = <AccessibleButton
onClick={this.onDndToggle}
className={classNames({
"mx_UserMenu_dnd": true,
"mx_UserMenu_dnd_noisy": !isDnd,
"mx_UserMenu_dnd_muted": isDnd,
})}
/>;
} }
if (this.props.isMinimized) { if (this.props.isMinimized) {
name = null; name = null;
@ -595,6 +617,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
/> />
</span> </span>
{name} {name}
{dnd}
{buttons} {buttons}
</div> </div>
</ContextMenuButton> </ContextMenuButton>

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import PasswordReset from "../../../PasswordReset"; import PasswordReset from "../../../PasswordReset";
@ -27,7 +27,9 @@ import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
import PassphraseField from '../../views/auth/PassphraseField';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
// Phases // Phases
// Show the forgot password inputs // Show the forgot password inputs
@ -137,10 +139,14 @@ export default class ForgotPassword extends React.Component {
// refresh the server errors, just in case the server came back online // refresh the server errors, just in case the server came back online
await this._checkServerLiveliness(this.props.serverConfig); await this._checkServerLiveliness(this.props.serverConfig);
await this['password_field'].validate({ allowEmpty: false });
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.')); this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) { } else if (!this.state.password || !this.state.password2) {
this.showErrorDialog(_t('A new password must be entered.')); this.showErrorDialog(_t('A new password must be entered.'));
} else if (!this.state.passwordFieldValid) {
this.showErrorDialog(_t('Please choose a strong password'));
} else if (this.state.password !== this.state.password2) { } else if (this.state.password !== this.state.password2) {
this.showErrorDialog(_t('New passwords must match each other.')); this.showErrorDialog(_t('New passwords must match each other.'));
} else { } else {
@ -186,6 +192,12 @@ export default class ForgotPassword extends React.Component {
}); });
} }
onPasswordValidate(result) {
this.setState({
passwordFieldValid: result.valid,
});
}
renderForgot() { renderForgot() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
@ -230,12 +242,15 @@ export default class ForgotPassword extends React.Component {
/> />
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<Field <PassphraseField
name="reset_password" name="reset_password"
type="password" type="password"
label={_t('New Password')} label={_td('New Password')}
value={this.state.password} value={this.state.password}
minScore={PASSWORD_MIN_SCORE}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
fieldRef={field => this['password_field'] = field}
onValidate={(result) => this.onPasswordValidate(result)}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
autoComplete="new-password" autoComplete="new-password"

View file

@ -436,6 +436,8 @@ export default class Registration extends React.Component<IProps, IState> {
// ok fine, there's still no session: really go to the login page // ok fine, there's still no session: really go to the login page
this.props.onLoginClick(); this.props.onLoginClick();
} }
return sessionLoaded;
}; };
private renderRegisterComponent() { private renderRegisterComponent() {
@ -557,7 +559,12 @@ export default class Registration extends React.Component<IProps, IState> {
loggedInUserId: this.state.differentLoggedInUserId, loggedInUserId: this.state.differentLoggedInUserId,
}, },
)}</p> )}</p>
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this.onLoginClickWithCheck}> <p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
const sessionLoaded = await this.onLoginClickWithCheck(event);
if (sessionLoaded) {
dis.dispatch({action: "view_welcome_page"});
}
}}>
{_t("Continue with previous account")} {_t("Continue with previous account")}
</AccessibleButton></p> </AccessibleButton></p>
</div>; </div>;

View file

@ -155,15 +155,14 @@ export default class SetupEncryptionBody extends React.Component {
let verifyButton; let verifyButton;
if (store.hasDevicesToVerifyAgainst) { if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}> verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") } { _t("Use another login") }
</AccessibleButton>; </AccessibleButton>;
} }
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Verify this login to access your encrypted messages and " + "Verify your identity to access encrypted messages and prove your identity to others.",
"prove to others that this login is really you.",
)}</p> )}</p>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
@ -205,8 +204,8 @@ export default class SetupEncryptionBody extends React.Component {
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Without completing security on this session, it wont have " + "Without verifying, you wont have access to all your messages " +
"access to encrypted messages.", "and may appear as untrusted to others.",
)}</p> )}</p>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
<AccessibleButton <AccessibleButton

View file

@ -40,7 +40,7 @@ enum RegistrationField {
PasswordConfirm = "field_password_confirm", PasswordConfirm = "field_password_confirm",
} }
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. export const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
interface IProps { interface IProps {
// Values pre-filled in the input boxes when the component loads // Values pre-filled in the input boxes when the component loads

View file

@ -26,6 +26,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units"; import {toPx} from "../../../utils/units";
import {ResizeMethod} from "../../../Avatar"; import {ResizeMethod} from "../../../Avatar";
import { _t } from '../../../languageHandler';
interface IProps { interface IProps {
name: string; // The name (first initial used as default) name: string; // The name (first initial used as default)
@ -140,6 +141,7 @@ const BaseAvatar = (props: IProps) => {
if (onClick) { if (onClick) {
return ( return (
<AccessibleButton <AccessibleButton
aria-label={_t("Avatar")}
{...otherProps} {...otherProps}
element="span" element="span"
className={classNames("mx_BaseAvatar", className)} className={classNames("mx_BaseAvatar", className)}

View file

@ -129,7 +129,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
name: this.props.room.name, name: this.props.room.name,
}; };
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
}; };
public render() { public render() {

View file

@ -52,6 +52,9 @@ export default class MessageContextMenu extends React.Component {
/* callback called when the menu is dismissed */ /* callback called when the menu is dismissed */
onFinished: PropTypes.func, onFinished: PropTypes.func,
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog: PropTypes.func,
}; };
state = { state = {
@ -141,6 +144,7 @@ export default class MessageContextMenu extends React.Component {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
if (this.props.onCloseDialog) this.props.onCloseDialog();
await cli.redactEvent( await cli.redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getRoomId(),
this.props.mxEvent.getId(), this.props.mxEvent.getId(),
@ -190,6 +194,7 @@ export default class MessageContextMenu extends React.Component {
}; };
onForwardClick = () => { onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog();
dis.dispatch({ dis.dispatch({
action: 'forward_event', action: 'forward_event',
event: this.props.mxEvent, event: this.props.mxEvent,

View file

@ -29,7 +29,10 @@ import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient"; import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize"; import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom"; import createRoom, {
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
IInvite3PID,
} from "../../../createRoom";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
@ -43,6 +46,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media"; import {mediaFromMxc} from "../../../customisations/Media";
import {getAddressType} from "../../../UserAddress";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -331,6 +335,7 @@ interface IInviteDialogState {
threepidResultsMixin: { user: Member, userId: string}[]; threepidResultsMixin: { user: Member, userId: string}[];
canUseIdentityServer: boolean; canUseIdentityServer: boolean;
tryingIdentityServer: boolean; tryingIdentityServer: boolean;
consultFirst: boolean;
// These two flags are used for the 'Go' button to communicate what is going on. // These two flags are used for the 'Go' button to communicate what is going on.
busy: boolean, busy: boolean,
@ -379,6 +384,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
threepidResultsMixin: [], threepidResultsMixin: [],
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(), canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
tryingIdentityServer: false, tryingIdentityServer: false,
consultFirst: false,
// These two flags are used for the 'Go' button to communicate what is going on. // These two flags are used for the 'Go' button to communicate what is going on.
busy: false, busy: false,
@ -394,6 +400,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} }
} }
private onConsultFirstChange = (ev) => {
this.setState({consultFirst: ev.target.checked});
}
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] { static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
@ -609,13 +619,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
_startDm = async () => { _startDm = async () => {
this.setState({busy: true}); this.setState({busy: true});
const client = MatrixClientPeg.get();
const targets = this._convertFilter(); const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId); const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible. // Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room; let existingRoom: Room;
if (targetIds.length === 1) { if (targetIds.length === 1) {
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]); existingRoom = findDMForUser(client, targetIds[0]);
} else { } else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
} }
@ -637,7 +648,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// If so, enable encryption in the new room. // If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember); const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) { if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds); const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) { if (allHaveDeviceKeys) {
createRoomOptions.encryption = true; createRoomOptions.encryption = true;
@ -647,45 +657,52 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Check if it's a traditional DM and create the room if required. // Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM // TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>; try {
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId(); const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) { if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0]; createRoomOptions.dmUserId = targetIds[0];
createRoomPromise = createRoom(createRoomOptions); }
} else if (isSelf) {
createRoomPromise = createRoom(createRoomOptions);
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
return inviteMultipleToRoom(roomId, targetIds);
}).then(result => {
if (this._shouldAbortAfterInviteError(result)) {
return true; // abort
}
});
}
// the createRoom call will show the room for us, so we don't need to worry about that. if (targetIds.length > 1) {
createRoomPromise.then(abort => { createRoomOptions.createOpts = targetIds.reduce(
if (abort === true) return; // only abort on true booleans, not roomIds or something (roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
)
}
await createRoom(createRoomOptions);
this.props.onFinished(); this.props.onFinished();
}).catch(err => { } catch (err) {
console.error(err); console.error(err);
this.setState({ this.setState({
busy: false, busy: false,
errorText: _t("We couldn't create your DM."), errorText: _t("We couldn't create your DM."),
}); });
}); }
}; };
_inviteUsers = () => { _inviteUsers = async () => {
const startTime = CountlyAnalytics.getTimestamp(); const startTime = CountlyAnalytics.getTimestamp();
this.setState({busy: true}); this.setState({busy: true});
this._convertFilter(); this._convertFilter();
const targets = this._convertFilter(); const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId); const targetIds = targets.map(t => t.userId);
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
if (!room) { if (!room) {
console.error("Failed to find the room to invite users to"); console.error("Failed to find the room to invite users to");
this.setState({ this.setState({
@ -695,12 +712,33 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return; return;
} }
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => { try {
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length); CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
this.props.onFinished(); this.props.onFinished();
} }
}).catch(err => {
if (cli.isRoomEncrypted(this.props.roomId)) {
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility == "world_readable" || visibility == "shared") {
const invitedUsers = [];
for (const [addr, state] of Object.entries(result.states)) {
if (state === "invited" && getAddressType(addr) === "mx-user-id") {
invitedUsers.push(addr);
}
}
console.log("Sharing history with", invitedUsers);
cli.sendSharedHistoryKeys(
this.props.roomId, invitedUsers,
);
}
}
} catch (err) {
console.error(err); console.error(err);
this.setState({ this.setState({
busy: false, busy: false,
@ -708,7 +746,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
"We couldn't invite those users. Please check the users you want to invite and try again.", "We couldn't invite those users. Please check the users you want to invite and try again.",
), ),
}); });
}); }
}; };
_transferCall = async () => { _transferCall = async () => {
@ -721,16 +759,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}); });
} }
this.setState({busy: true}); if (this.state.consultFirst) {
try { const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
await this.props.call.transfer(targetIds[0]);
this.setState({busy: false}); dis.dispatch({
this.props.onFinished(); action: 'place_call',
} catch (e) { type: this.props.call.type,
this.setState({ room_id: dmRoomId,
busy: false, transferee: this.props.call,
errorText: _t("Failed to transfer call"),
}); });
dis.dispatch({
action: 'view_room',
room_id: dmRoomId,
should_peek: false,
joining: false,
});
this.props.onFinished();
} else {
this.setState({busy: true});
try {
await this.props.call.transfer(targetIds[0]);
this.setState({busy: false});
this.props.onFinished();
} catch (e) {
this.setState({
busy: false,
errorText: _t("Failed to transfer call"),
});
}
} }
}; };
@ -1191,10 +1247,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
let helpText; let helpText;
let buttonText; let buttonText;
let goButtonFn; let goButtonFn;
let consultSection;
let keySharingWarning = <span />;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const userId = MatrixClientPeg.get().getUserId(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
if (this.props.kind === KIND_DM) { if (this.props.kind === KIND_DM) {
title = _t("Direct Messages"); title = _t("Direct Messages");
@ -1290,10 +1349,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
buttonText = _t("Invite"); buttonText = _t("Invite");
goButtonFn = this._inviteUsers; goButtonFn = this._inviteUsers;
if (cli.isRoomEncrypted(this.props.roomId)) {
const room = cli.getRoom(this.props.roomId);
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility === "world_readable" || visibility === "shared") {
keySharingWarning =
<p className='mx_InviteDialog_helpText'>
<img
src={require("../../../../res/img/element-icons/info.svg")}
width={14} height={14} />
{" " + _t("Invited people will be able to read old messages.")}
</p>;
}
}
} else if (this.props.kind === KIND_CALL_TRANSFER) { } else if (this.props.kind === KIND_CALL_TRANSFER) {
title = _t("Transfer"); title = _t("Transfer");
buttonText = _t("Transfer"); buttonText = _t("Transfer");
goButtonFn = this._transferCall; goButtonFn = this._transferCall;
consultSection = <div>
<label>
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
{_t("Consult first")}
</label>
</div>;
} else { } else {
console.error("Unknown kind of InviteDialog: " + this.props.kind); console.error("Unknown kind of InviteDialog: " + this.props.kind);
} }
@ -1323,12 +1406,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
{spinner} {spinner}
</div> </div>
</div> </div>
{keySharingWarning}
{this._renderIdentityServerWarning()} {this._renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div> <div className='error'>{this.state.errorText}</div>
<div className='mx_InviteDialog_userSections'> <div className='mx_InviteDialog_userSections'>
{this._renderSection('recents')} {this._renderSection('recents')}
{this._renderSection('suggestions')} {this._renderSection('suggestions')}
</div> </div>
{consultSection}
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -0,0 +1,54 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import {IDialogProps} from "./IDialogProps";
@replaceableComponent("views.dialogs.SeshatResetDialog")
export default class SeshatResetDialog extends React.PureComponent<IDialogProps> {
render() {
return (
<BaseDialog
hasCancel={true}
onFinished={this.props.onFinished.bind(null, false)}
title={_t("Reset event store?")}>
<div>
<p>
{_t("You most likely do not want to reset your event index store")}
<br />
{_t("If you do, please note that none of your messages will be deleted, " +
"but the search experience might be degraded for a few moments" +
"whilst the index is recreated",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("Reset event store")}
onPrimaryButtonClick={this.props.onFinished.bind(null, true)}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this.props.onFinished.bind(null, false)}
/>
</BaseDialog>
);
}
}

View file

@ -126,8 +126,8 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<div> <div>
{ _t("Make this space private") } { _t("Make this space private") }
<ToggleSwitch <ToggleSwitch
checked={joinRule === "private"} checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "private" : "invite")} onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule} disabled={!canSetJoinRule}
aria-label={_t("Make this space private")} aria-label={_t("Make this space private")}
/> />

View file

@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
const member = this.props.member || const member = this.props.member ||
otherUserId && MatrixClientPeg.get().getUser(otherUserId); otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ? const title = request && request.isSelfVerification ?
_t("Verify other session") : _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"

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2018, 2019 New Vector Ltd Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -17,14 +16,17 @@ limitations under the License.
import {debounce} from "lodash"; import {debounce} from "lodash";
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, {ChangeEvent, FormEvent} from 'react';
import PropTypes from "prop-types"; import {ISecretStorageKeyInfo} from "matrix-js-sdk/src";
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field'; import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton'; import AccessibleButton from '../../elements/AccessibleButton';
import {_t} from '../../../../languageHandler';
import { _t } from '../../../../languageHandler'; import {IDialogProps} from "../IDialogProps";
import {accessSecretStorage} from "../../../../SecurityManager";
import Modal from "../../../../Modal";
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode, // Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because // so this should be plenty and allow for people putting extra whitespace in the file because
@ -34,22 +36,31 @@ const KEY_FILE_MAX_SIZE = 128;
// Don't shout at the user that their key is invalid every time they type a key: wait a short time // Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200; const VALIDATION_THROTTLE_MS = 200;
interface IProps extends IDialogProps {
keyInfo: ISecretStorageKeyInfo;
checkPrivateKey: (k: {passphrase?: string, recoveryKey?: string}) => boolean;
}
interface IState {
recoveryKey: string;
recoveryKeyValid: boolean | null;
recoveryKeyCorrect: boolean | null;
recoveryKeyFileError: boolean | null;
forceRecoveryKey: boolean;
passPhrase: string;
keyMatches: boolean | null;
resetting: boolean;
}
/* /*
* Access Secure Secret Storage by requesting the user's passphrase. * Access Secure Secret Storage by requesting the user's passphrase.
*/ */
export default class AccessSecretStorageDialog extends React.PureComponent { export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
static propTypes = { private fileUpload = React.createRef<HTMLInputElement>();
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
}
constructor(props) { constructor(props) {
super(props); super(props);
this._fileUpload = React.createRef();
this.state = { this.state = {
recoveryKey: "", recoveryKey: "",
recoveryKeyValid: null, recoveryKeyValid: null,
@ -58,24 +69,28 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
forceRecoveryKey: false, forceRecoveryKey: false,
passPhrase: '', passPhrase: '',
keyMatches: null, keyMatches: null,
resetting: false,
}; };
} }
_onCancel = () => { private onCancel = () => {
if (this.state.resetting) {
this.setState({resetting: false});
}
this.props.onFinished(false); this.props.onFinished(false);
} };
_onUseRecoveryKeyClick = () => { private onUseRecoveryKeyClick = () => {
this.setState({ this.setState({
forceRecoveryKey: true, forceRecoveryKey: true,
}); });
} };
_validateRecoveryKeyOnChange = debounce(() => { private validateRecoveryKeyOnChange = debounce(async () => {
this._validateRecoveryKey(); await this.validateRecoveryKey();
}, VALIDATION_THROTTLE_MS); }, VALIDATION_THROTTLE_MS);
async _validateRecoveryKey() { private async validateRecoveryKey() {
if (this.state.recoveryKey === '') { if (this.state.recoveryKey === '') {
this.setState({ this.setState({
recoveryKeyValid: null, recoveryKeyValid: null,
@ -102,27 +117,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} }
} }
_onRecoveryKeyChange = (e) => { private onRecoveryKeyChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
recoveryKey: e.target.value, recoveryKey: ev.target.value,
recoveryKeyFileError: null, recoveryKeyFileError: null,
}); });
// also clear the file upload control so that the user can upload the same file // also clear the file upload control so that the user can upload the same file
// the did before (otherwise the onchange wouldn't fire) // the did before (otherwise the onchange wouldn't fire)
if (this._fileUpload.current) this._fileUpload.current.value = null; if (this.fileUpload.current) this.fileUpload.current.value = null;
// We don't use Field's validation here because a) we want it in a separate place rather // We don't use Field's validation here because a) we want it in a separate place rather
// than in a tooltip and b) we want it to display feedback based on the uploaded file // than in a tooltip and b) we want it to display feedback based on the uploaded file
// as well as the text box. Ideally we would refactor Field's validation logic so we could // as well as the text box. Ideally we would refactor Field's validation logic so we could
// re-use some of it. // re-use some of it.
this._validateRecoveryKeyOnChange(); this.validateRecoveryKeyOnChange();
} };
_onRecoveryKeyFileChange = async e => { private onRecoveryKeyFileChange = async (ev: ChangeEvent<HTMLInputElement>) => {
if (e.target.files.length === 0) return; if (ev.target.files.length === 0) return;
const f = e.target.files[0]; const f = ev.target.files[0];
if (f.size > KEY_FILE_MAX_SIZE) { if (f.size > KEY_FILE_MAX_SIZE) {
this.setState({ this.setState({
@ -140,7 +155,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
recoveryKeyFileError: null, recoveryKeyFileError: null,
recoveryKey: contents.trim(), recoveryKey: contents.trim(),
}); });
this._validateRecoveryKey(); await this.validateRecoveryKey();
} else { } else {
this.setState({ this.setState({
recoveryKeyFileError: true, recoveryKeyFileError: true,
@ -150,14 +165,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}); });
} }
} }
};
private onRecoveryKeyFileUploadClick = () => {
this.fileUpload.current.click();
} }
_onRecoveryKeyFileUploadClick = () => { private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
this._fileUpload.current.click(); ev.preventDefault();
}
_onPassPhraseNext = async (e) => {
e.preventDefault();
if (this.state.passPhrase.length <= 0) return; if (this.state.passPhrase.length <= 0) return;
@ -169,10 +184,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else { } else {
this.setState({ keyMatches }); this.setState({ keyMatches });
} }
} };
_onRecoveryKeyNext = async (e) => { private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement>) => {
e.preventDefault(); ev.preventDefault();
if (!this.state.recoveryKeyValid) return; if (!this.state.recoveryKeyValid) return;
@ -184,16 +199,65 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else { } else {
this.setState({ keyMatches }); this.setState({ keyMatches });
} }
} };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: ev.target.value,
keyMatches: null, keyMatches: null,
}); });
} };
getKeyValidationText() { private onResetAllClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
ev.preventDefault();
this.setState({resetting: true});
};
private onConfirmResetAllClick = async () => {
// Hide ourselves so the user can interact with the reset dialogs.
// We don't conclude the promise chain (onFinished) yet to avoid confusing
// any upstream code flows.
//
// Note: this will unmount us, so don't call `setState` or anything in the
// rest of this function.
Modal.toggleCurrentDialogVisibility();
try {
// Force reset secret storage (which resets the key backup)
await accessSecretStorage(async () => {
// Now reset cross-signing so everything Just Works™ again.
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
// XXX: Making this an import breaks the app.
const InteractiveAuthDialog = sdk.getComponent("views.dialogs.InteractiveAuthDialog");
const {finished} = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: cli,
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
setupNewCrossSigning: true,
});
// Now we can indicate that the user is done pressing buttons, finally.
// Upstream flows will detect the new secret storage, key backup, etc and use it.
this.props.onFinished(true);
}, true);
} catch (e) {
console.error(e);
this.props.onFinished(false);
}
};
private getKeyValidationText(): string {
if (this.state.recoveryKeyFileError) { if (this.state.recoveryKeyFileError) {
return _t("Wrong file type"); return _t("Wrong file type");
} else if (this.state.recoveryKeyCorrect) { } else if (this.state.recoveryKeyCorrect) {
@ -208,7 +272,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} }
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); // Caution: Making these an import will break tests.
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const hasPassphrase = ( const hasPassphrase = (
this.props.keyInfo && this.props.keyInfo &&
@ -217,11 +283,36 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
this.props.keyInfo.passphrase.iterations this.props.keyInfo.passphrase.iterations
); );
const resetButton = (
<div className="mx_AccessSecretStorageDialog_reset">
{_t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a
href="" onClick={this.onResetAllClick}
className="mx_AccessSecretStorageDialog_reset_link">{sub}</a>,
})}
</div>
);
let content; let content;
let title; let title;
let titleClass; let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) { if (this.state.resetting) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); title = _t("Reset everything");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_resetBadge'];
content = <div>
<p>{_t("Only do this if you have no other device to complete verification with.")}</p>
<p>{_t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
+ "might not be able to see past messages.")}</p>
<DialogButtons
primaryButton={_t('Reset')}
onPrimaryButtonClick={this.onConfirmResetAllClick}
hasCancel={true}
onCancel={this.onCancel}
focus={false}
primaryButtonClass="danger"
/>
</div>;
} else if (hasPassphrase && !this.state.forceRecoveryKey) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Security Phrase"); title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle']; titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
@ -244,18 +335,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{ {
button: s => <AccessibleButton className="mx_linkButton" button: s => <AccessibleButton className="mx_linkButton"
element="span" element="span"
onClick={this._onUseRecoveryKeyClick} onClick={this.onUseRecoveryKeyClick}
> >
{s} {s}
</AccessibleButton>, </AccessibleButton>,
}, },
)}</p> )}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<input <input
type="password" type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput" className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
autoFocus={true} autoFocus={true}
autoComplete="new-password" autoComplete="new-password"
@ -264,18 +355,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{keyStatus} {keyStatus}
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNext} onPrimaryButtonClick={this.onPassPhraseNext}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={this.state.passPhrase.length === 0} primaryDisabled={this.state.passPhrase.length === 0}
additive={resetButton}
/> />
</form> </form>
</div>; </div>;
} else { } else {
title = _t("Security Key"); title = _t("Security Key");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const feedbackClasses = classNames({ const feedbackClasses = classNames({
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true, 'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
@ -291,7 +382,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<form <form
className="mx_AccessSecretStorageDialog_primaryContainer" className="mx_AccessSecretStorageDialog_primaryContainer"
onSubmit={this._onRecoveryKeyNext} onSubmit={this.onRecoveryKeyNext}
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete="off"
> >
@ -301,7 +392,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
type="password" type="password"
label={_t('Security Key')} label={_t('Security Key')}
value={this.state.recoveryKey} value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange} onChange={this.onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect} forceValidity={this.state.recoveryKeyCorrect}
autoComplete="off" autoComplete="off"
/> />
@ -312,10 +403,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<div> <div>
<input type="file" <input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput" className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this._fileUpload} ref={this.fileUpload}
onChange={this._onRecoveryKeyFileChange} onChange={this.onRecoveryKeyFileChange}
/> />
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}> <AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
{_t("Upload")} {_t("Upload")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -323,13 +414,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{recoveryKeyFeedback} {recoveryKeyFeedback}
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onRecoveryKeyNext} onPrimaryButtonClick={this.onRecoveryKeyNext}
hasCancel={true} hasCancel={true}
cancelButton={_t("Go Back")} cancelButton={_t("Go Back")}
cancelButtonClass='danger' cancelButtonClass='danger'
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
additive={resetButton}
/> />
</form> </form>
</div>; </div>;
@ -341,9 +433,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
title={title} title={title}
titleClass={titleClass} titleClass={titleClass}
> >
<div> <div>
{content} {content}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -19,7 +19,6 @@ import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import * as Avatar from '../../../Avatar'; import * as Avatar from '../../../Avatar';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTile from '../rooms/EventTile'; import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {Layout} from "../../../settings/Layout"; import {Layout} from "../../../settings/Layout";
@ -41,15 +40,26 @@ interface IProps {
* classnames to apply to the wrapper of the preview * classnames to apply to the wrapper of the preview
*/ */
className: string; className: string;
/**
* The ID of the displayed user
*/
userId: string;
/**
* The display name of the displayed user
*/
displayName?: string;
/**
* The mxc:// avatar URL of the displayed user
*/
avatarUrl?: string;
} }
/* eslint-disable camelcase */
interface IState { interface IState {
userId: string; message: string;
displayname: string;
avatar_url: string;
} }
/* eslint-enable camelcase */
const AVATAR_SIZE = 32; const AVATAR_SIZE = 32;
@ -57,45 +67,28 @@ const AVATAR_SIZE = 32;
export default class EventTilePreview extends React.Component<IProps, IState> { export default class EventTilePreview extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
userId: "@erim:fink.fink", message: props.message,
displayname: "Erimayas Fink",
avatar_url: null,
}; };
} }
async componentDidMount() { private fakeEvent({message}: IState) {
// Fetch current user data
const client = MatrixClientPeg.get();
const userId = client.getUserId();
const profileInfo = await client.getProfileInfo(userId);
const avatarUrl = profileInfo.avatar_url;
this.setState({
userId,
displayname: profileInfo.displayname,
avatar_url: avatarUrl,
});
}
private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
// Fake it till we make it // Fake it till we make it
/* eslint-disable quote-props */ /* eslint-disable quote-props */
const rawEvent = { const rawEvent = {
type: "m.room.message", type: "m.room.message",
sender: userId, sender: this.props.userId,
content: { content: {
"m.new_content": { "m.new_content": {
msgtype: "m.text", msgtype: "m.text",
body: this.props.message, body: message,
displayname: displayname, displayname: this.props.displayName,
avatar_url: avatarUrl, avatar_url: this.props.avatarUrl,
}, },
msgtype: "m.text", msgtype: "m.text",
body: this.props.message, body: message,
displayname: displayname, displayname: this.props.displayName,
avatar_url: avatarUrl, avatar_url: this.props.avatarUrl,
}, },
unsigned: { unsigned: {
age: 97, age: 97,
@ -108,12 +101,15 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
// Fake it more // Fake it more
event.sender = { event.sender = {
name: displayname, name: this.props.displayName,
userId: userId, userId: this.props.userId,
getAvatarUrl: (..._) => { getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop"); return Avatar.avatarUrlForUser(
{ avatarUrl: this.props.avatarUrl },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
}, },
getMxcAvatarUrl: () => avatarUrl, getMxcAvatarUrl: () => this.props.avatarUrl,
}; };
return event; return event;

View file

@ -0,0 +1,66 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers";
const DEFAULT_NUM_FACES = 5;
interface IProps extends HTMLAttributes<HTMLSpanElement> {
room: Room;
onlyKnownUsers?: boolean;
numShown?: number;
}
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
let members = useRoomMembers(room);
// sort users with an explicit avatar first
const iteratees = [member => !!member.getMxcAvatarUrl()];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member));
}
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
return <div {...props} className="mx_FacePile">
<div className="mx_FacePile_faces">
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}>
<MemberAvatar member={member} width={28} height={28} />
</TextWithTooltip>;
}) }
</div>
{ onlyKnownUsers && <span>
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</div>
};
export default FacePile;

Some files were not shown because too many files have changed in this diff Show more