Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into pr/only-member-warning
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
103
CHANGELOG.md
|
@ -1,3 +1,95 @@
|
||||||
|
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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0-rc.1...v3.17.0)
|
||||||
|
@ -220,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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.17.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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -123,6 +123,7 @@
|
||||||
@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";
|
||||||
|
@ -250,6 +251,7 @@
|
||||||
@import "./views/voice_messages/_Waveform.scss";
|
@import "./views/voice_messages/_Waveform.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
|
@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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
57
res/css/views/elements/_InviteReason.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,8 +68,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_BasicMessageComposer_input_disabled {
|
&.mx_BasicMessageComposer_input_disabled {
|
||||||
|
// Ignore all user input to avoid accidentally triggering the composer
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -53,7 +53,8 @@ limitations under the License.
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
// TODO: @@ TravisR: Animate
|
animation: recording-pulse 2s infinite;
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
background-color: $voice-record-live-circle-color;
|
background-color: $voice-record-live-circle-color;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
|
@ -74,3 +75,26 @@ limitations under the License.
|
||||||
width: 42px; // we're not using a monospace font, so fake it
|
width: 42px; // we're not using a monospace font, so fake it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The keyframes are slightly weird here to help make a ramping/punch effect
|
||||||
|
// for the recording dot. We start and end at 100% opacity to help make the
|
||||||
|
// dot feel a bit like a real lamp that is blinking: the animation ends up
|
||||||
|
// spending a lot of its time showing a steady state without a fade effect.
|
||||||
|
// This lamp effect extends into why the 0% opacity keyframe is not in the
|
||||||
|
// midpoint: lamps take longer to turn off than they do to turn on, and the
|
||||||
|
// extra frames give it a bit of a realistic punch for when the animation is
|
||||||
|
// ramping back up to 100% opacity.
|
||||||
|
//
|
||||||
|
// Target animation timings: steady for 1.5s, fade out for 0.3s, fade in for 0.2s
|
||||||
|
// (intended to be used in a loop for 2s animation speed)
|
||||||
|
@keyframes recording-pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
35% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
65% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,9 +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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,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;
|
||||||
}
|
}
|
||||||
|
@ -82,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;
|
||||||
|
@ -91,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;
|
||||||
|
@ -104,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;
|
||||||
|
@ -142,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%;
|
||||||
|
@ -177,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 {
|
||||||
|
|
46
res/css/views/voip/_CallViewForRoom.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
3
res/img/image-view/close.svg
Normal 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 |
3
res/img/image-view/download.svg
Normal 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 |
3
res/img/image-view/more.svg
Normal 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 |
3
res/img/image-view/rotate-ccw.svg
Normal 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 |
3
res/img/image-view/rotate-cw.svg
Normal 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 |
3
res/img/image-view/zoom-in.svg
Normal 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 |
3
res/img/image-view/zoom-out.svg
Normal 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 |
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||||
|
@ -242,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -188,11 +189,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
||||||
|
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
|
// See non-legacy _light for variable information
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
$voice-record-stop-border-color: #E3E8F0;
|
||||||
$voice-record-stop-symbol-color: $warning-color;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
$voice-record-waveform-bg-color: #E3E8F0;
|
$voice-record-waveform-bg-color: #E3E8F0;
|
||||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
$voice-record-waveform-fg-color: $muted-fg-color;
|
||||||
$voice-record-live-circle-color: $warning-color;
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
|
|
||||||
$roomtile-preview-color: #9e9e9e;
|
$roomtile-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -180,10 +181,10 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
$voice-record-stop-border-color: #E3E8F0;
|
||||||
$voice-record-stop-symbol-color: $warning-color;
|
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
||||||
$voice-record-waveform-bg-color: #E3E8F0;
|
$voice-record-waveform-bg-color: #E3E8F0;
|
||||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
$voice-record-waveform-fg-color: $muted-fg-color;
|
||||||
$voice-record-live-circle-color: $warning-color;
|
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
||||||
|
|
||||||
$roomtile-preview-color: $secondary-fg-color;
|
$roomtile-preview-color: $secondary-fg-color;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
src/@types/global.d.ts
vendored
|
@ -39,7 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
import VoipUserMapper from "../VoipUserMapper";
|
import VoipUserMapper from "../VoipUserMapper";
|
||||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||||
import {VoiceRecorder} from "../voice/VoiceRecorder";
|
import {VoiceRecording} from "../voice/VoiceRecording";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -71,7 +71,7 @@ declare global {
|
||||||
mxModalWidgetStore: ModalWidgetStore;
|
mxModalWidgetStore: ModalWidgetStore;
|
||||||
mxVoipUserMapper: VoipUserMapper;
|
mxVoipUserMapper: VoipUserMapper;
|
||||||
mxSpaceStore: SpaceStoreClass;
|
mxSpaceStore: SpaceStoreClass;
|
||||||
mxVoiceRecorder: typeof VoiceRecorder;
|
mxVoiceRecorder: typeof VoiceRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -360,11 +360,11 @@ const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
||||||
action: NavigationAction.GoToHome,
|
action: NavigationAction.GoToHome,
|
||||||
keyCombo: {
|
keyCombo: {
|
||||||
key: Key.H,
|
key: Key.H,
|
||||||
ctrlOrCmd: true,
|
ctrlKey: true,
|
||||||
altKey: true,
|
altKey: !isMac,
|
||||||
|
shiftKey: isMac,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
action: NavigationAction.SelectPrevRoom,
|
action: NavigationAction.SelectPrevRoom,
|
||||||
keyCombo: {
|
keyCombo: {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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})),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@ 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 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 {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -1137,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 => {
|
||||||
|
@ -1164,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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -332,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,
|
||||||
|
@ -380,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,
|
||||||
|
@ -395,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
|
||||||
|
|
||||||
|
@ -610,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);
|
||||||
}
|
}
|
||||||
|
@ -638,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;
|
||||||
|
@ -648,35 +657,41 @@ 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 = async () => {
|
_inviteUsers = async () => {
|
||||||
|
@ -704,8 +719,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cli.isRoomEncrypted(this.props.roomId) &&
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||||
SettingsStore.getValue("feature_room_history_key_sharing")) {
|
|
||||||
const visibilityEvent = room.currentState.getStateEvents(
|
const visibilityEvent = room.currentState.getStateEvents(
|
||||||
"m.room.history_visibility", "",
|
"m.room.history_visibility", "",
|
||||||
);
|
);
|
||||||
|
@ -745,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"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1215,6 +1247,7 @@ 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 />;
|
let keySharingWarning = <span />;
|
||||||
|
|
||||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||||
|
@ -1317,8 +1350,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
buttonText = _t("Invite");
|
buttonText = _t("Invite");
|
||||||
goButtonFn = this._inviteUsers;
|
goButtonFn = this._inviteUsers;
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||||
cli.isRoomEncrypted(this.props.roomId)) {
|
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
const visibilityEvent = room.currentState.getStateEvents(
|
const visibilityEvent = room.currentState.getStateEvents(
|
||||||
"m.room.history_visibility", "",
|
"m.room.history_visibility", "",
|
||||||
|
@ -1339,6 +1371,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -1375,6 +1413,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
{this._renderSection('recents')}
|
{this._renderSection('recents')}
|
||||||
{this._renderSection('suggestions')}
|
{this._renderSection('suggestions')}
|
||||||
</div>
|
</div>
|
||||||
|
{consultSection}
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
54
src/components/views/dialogs/SeshatResetDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ 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 {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
|
||||||
|
@ -47,6 +49,7 @@ interface IState {
|
||||||
forceRecoveryKey: boolean;
|
forceRecoveryKey: boolean;
|
||||||
passPhrase: string;
|
passPhrase: string;
|
||||||
keyMatches: boolean | null;
|
keyMatches: boolean | null;
|
||||||
|
resetting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -66,10 +69,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
forceRecoveryKey: false,
|
forceRecoveryKey: false,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
keyMatches: null,
|
keyMatches: null,
|
||||||
|
resetting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCancel = () => {
|
private onCancel = () => {
|
||||||
|
if (this.state.resetting) {
|
||||||
|
this.setState({resetting: false});
|
||||||
|
}
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -201,6 +208,55 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
private getKeyValidationText(): string {
|
||||||
if (this.state.recoveryKeyFileError) {
|
if (this.state.recoveryKeyFileError) {
|
||||||
return _t("Wrong file type");
|
return _t("Wrong file type");
|
||||||
|
@ -216,8 +272,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Caution: Making this an import will break tests.
|
// Caution: Making these an import will break tests.
|
||||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||||
|
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||||
|
|
||||||
const hasPassphrase = (
|
const hasPassphrase = (
|
||||||
this.props.keyInfo &&
|
this.props.keyInfo &&
|
||||||
|
@ -226,11 +283,36 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
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'];
|
||||||
|
@ -278,13 +360,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
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,
|
||||||
|
@ -339,6 +421,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
focus={false}
|
focus={false}
|
||||||
primaryDisabled={!this.state.recoveryKeyValid}
|
primaryDisabled={!this.state.recoveryKeyValid}
|
||||||
|
additive={resetButton}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,235 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import {formatDate} from '../../../DateUtils';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import filesize from "filesize";
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import {Key} from "../../../Keyboard";
|
|
||||||
import FocusLock from "react-focus-lock";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.elements.ImageView")
|
|
||||||
export default class ImageView extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
src: PropTypes.string.isRequired, // the source of the image being displayed
|
|
||||||
name: PropTypes.string, // the main title ('name') for the image
|
|
||||||
link: PropTypes.string, // the link (if any) applied to the name of the image
|
|
||||||
width: PropTypes.number, // width of the image src in pixels
|
|
||||||
height: PropTypes.number, // height of the image src in pixels
|
|
||||||
fileSize: PropTypes.number, // size of the image src in bytes
|
|
||||||
onFinished: PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
|
||||||
|
|
||||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
|
||||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
|
||||||
// properties above, which let us use lightboxes to display images which aren't associated
|
|
||||||
// with events.
|
|
||||||
mxEvent: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { rotationDegrees: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown = (ev) => {
|
|
||||||
if (ev.key === Key.ESCAPE) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
this.props.onFinished();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onRedactClick = () => {
|
|
||||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
|
||||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
|
||||||
onFinished: (proceed) => {
|
|
||||||
if (!proceed) return;
|
|
||||||
this.props.onFinished();
|
|
||||||
MatrixClientPeg.get().redactEvent(
|
|
||||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
|
|
||||||
).catch(function(e) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
// display error message stating you couldn't delete this.
|
|
||||||
const code = e.errcode || e.statusCode;
|
|
||||||
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
|
|
||||||
title: _t('Error'),
|
|
||||||
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
let name = this.props.name;
|
|
||||||
if (name && this.props.link) {
|
|
||||||
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateCounterClockwise = () => {
|
|
||||||
const cur = this.state.rotationDegrees;
|
|
||||||
const rotationDegrees = (cur - 90) % 360;
|
|
||||||
this.setState({ rotationDegrees });
|
|
||||||
};
|
|
||||||
|
|
||||||
rotateClockwise = () => {
|
|
||||||
const cur = this.state.rotationDegrees;
|
|
||||||
const rotationDegrees = (cur + 90) % 360;
|
|
||||||
this.setState({ rotationDegrees });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
/*
|
|
||||||
// In theory max-width: 80%, max-height: 80% on the CSS should work
|
|
||||||
// but in practice, it doesn't, so do it manually:
|
|
||||||
|
|
||||||
var width = this.props.width || 500;
|
|
||||||
var height = this.props.height || 500;
|
|
||||||
|
|
||||||
var maxWidth = document.documentElement.clientWidth * 0.8;
|
|
||||||
var maxHeight = document.documentElement.clientHeight * 0.8;
|
|
||||||
|
|
||||||
var widthFrac = width / maxWidth;
|
|
||||||
var heightFrac = height / maxHeight;
|
|
||||||
|
|
||||||
var displayWidth;
|
|
||||||
var displayHeight;
|
|
||||||
if (widthFrac > heightFrac) {
|
|
||||||
displayWidth = Math.min(width, maxWidth);
|
|
||||||
displayHeight = (displayWidth / width) * height;
|
|
||||||
} else {
|
|
||||||
displayHeight = Math.min(height, maxHeight);
|
|
||||||
displayWidth = (displayHeight / height) * width;
|
|
||||||
}
|
|
||||||
|
|
||||||
var style = {
|
|
||||||
width: displayWidth,
|
|
||||||
height: displayHeight
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
let style = {};
|
|
||||||
let res;
|
|
||||||
|
|
||||||
if (this.props.width && this.props.height) {
|
|
||||||
style = {
|
|
||||||
width: this.props.width,
|
|
||||||
height: this.props.height,
|
|
||||||
};
|
|
||||||
res = style.width + "x" + style.height + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
let size;
|
|
||||||
if (this.props.fileSize) {
|
|
||||||
size = filesize(this.props.fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sizeRes;
|
|
||||||
if (size && res) {
|
|
||||||
sizeRes = size + ", " + res;
|
|
||||||
} else {
|
|
||||||
sizeRes = size || res;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mayRedact = false;
|
|
||||||
const showEventMeta = !!this.props.mxEvent;
|
|
||||||
|
|
||||||
let eventMeta;
|
|
||||||
if (showEventMeta) {
|
|
||||||
// Figure out the sender, defaulting to mxid
|
|
||||||
let sender = this.props.mxEvent.getSender();
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
if (room) {
|
|
||||||
mayRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
|
|
||||||
const member = room.getMember(sender);
|
|
||||||
if (member) sender = member.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventMeta = (<div className="mx_ImageView_metadata">
|
|
||||||
{ _t('Uploaded on %(date)s by %(user)s', {
|
|
||||||
date: formatDate(new Date(this.props.mxEvent.getTs())),
|
|
||||||
user: sender,
|
|
||||||
}) }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let eventRedact;
|
|
||||||
if (mayRedact) {
|
|
||||||
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
|
|
||||||
{ _t('Remove') }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rotationDegrees = this.state.rotationDegrees;
|
|
||||||
const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusLock
|
|
||||||
returnFocus={true}
|
|
||||||
lockProps={{
|
|
||||||
onKeyDown: this.onKeyDown,
|
|
||||||
role: "dialog",
|
|
||||||
}}
|
|
||||||
className="mx_ImageView"
|
|
||||||
>
|
|
||||||
<div className="mx_ImageView_lhs">
|
|
||||||
</div>
|
|
||||||
<div className="mx_ImageView_content">
|
|
||||||
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
|
|
||||||
<div className="mx_ImageView_labelWrapper">
|
|
||||||
<div className="mx_ImageView_label">
|
|
||||||
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
|
|
||||||
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
|
|
||||||
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
|
|
||||||
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_ImageView_shim">
|
|
||||||
</div>
|
|
||||||
<div className="mx_ImageView_name">
|
|
||||||
{ this.getName() }
|
|
||||||
</div>
|
|
||||||
{ eventMeta }
|
|
||||||
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
|
|
||||||
<div className="mx_ImageView_download">
|
|
||||||
{ _t('Download this file') }<br />
|
|
||||||
<span className="mx_ImageView_size">{ sizeRes }</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{ eventRedact }
|
|
||||||
<div className="mx_ImageView_shim">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx_ImageView_rhs">
|
|
||||||
</div>
|
|
||||||
</FocusLock>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
439
src/components/views/elements/ImageView.tsx
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020, 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createRef } from 'react';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
import FocusLock from "react-focus-lock";
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||||
|
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||||
|
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
||||||
|
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import {formatFullDate} from "../../../DateUtils";
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
const MIN_ZOOM = 100;
|
||||||
|
const MAX_ZOOM = 300;
|
||||||
|
// This is used for the buttons
|
||||||
|
const ZOOM_STEP = 10;
|
||||||
|
// This is used for mouse wheel events
|
||||||
|
const ZOOM_COEFFICIENT = 10;
|
||||||
|
// If we have moved only this much we can zoom
|
||||||
|
const ZOOM_DISTANCE = 10;
|
||||||
|
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
src: string, // the source of the image being displayed
|
||||||
|
name?: string, // the main title ('name') for the image
|
||||||
|
link?: string, // the link (if any) applied to the name of the image
|
||||||
|
width?: number, // width of the image src in pixels
|
||||||
|
height?: number, // height of the image src in pixels
|
||||||
|
fileSize?: number, // size of the image src in bytes
|
||||||
|
onFinished(): void, // callback when the lightbox is dismissed
|
||||||
|
|
||||||
|
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||||
|
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||||
|
// properties above, which let us use lightboxes to display images which aren't associated
|
||||||
|
// with events.
|
||||||
|
mxEvent: MatrixEvent,
|
||||||
|
permalinkCreator: RoomPermalinkCreator,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
rotation: number,
|
||||||
|
zoom: number,
|
||||||
|
translationX: number,
|
||||||
|
translationY: number,
|
||||||
|
moving: boolean,
|
||||||
|
contextMenuDisplayed: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.ImageView")
|
||||||
|
export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
rotation: 0,
|
||||||
|
zoom: MIN_ZOOM,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
moving: false,
|
||||||
|
contextMenuDisplayed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Refs to functional components
|
||||||
|
private contextMenuButton = createRef<any>();
|
||||||
|
private focusLock = createRef<any>();
|
||||||
|
|
||||||
|
private initX = 0;
|
||||||
|
private initY = 0;
|
||||||
|
private lastX = 0;
|
||||||
|
private lastY = 0;
|
||||||
|
private previousX = 0;
|
||||||
|
private previousY = 0;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// We have to use addEventListener() because the listener
|
||||||
|
// needs to be passive in order to work with Chromium
|
||||||
|
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.key === Key.ESCAPE) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onWheel = (ev: WheelEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
const newZoom = this.state.zoom - (ev.deltaY * ZOOM_COEFFICIENT);
|
||||||
|
|
||||||
|
if (newZoom <= MIN_ZOOM) {
|
||||||
|
this.setState({
|
||||||
|
zoom: MIN_ZOOM,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newZoom >= MAX_ZOOM) {
|
||||||
|
this.setState({zoom: MAX_ZOOM});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
zoom: newZoom,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRotateCounterClockwiseClick = () => {
|
||||||
|
const cur = this.state.rotation;
|
||||||
|
const rotationDegrees = cur - 90;
|
||||||
|
this.setState({ rotation: rotationDegrees });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRotateClockwiseClick = () => {
|
||||||
|
const cur = this.state.rotation;
|
||||||
|
const rotationDegrees = cur + 90;
|
||||||
|
this.setState({ rotation: rotationDegrees });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onZoomInClick = () => {
|
||||||
|
if (this.state.zoom >= MAX_ZOOM) {
|
||||||
|
this.setState({zoom: MAX_ZOOM});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
zoom: this.state.zoom + ZOOM_STEP,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onZoomOutClick = () => {
|
||||||
|
if (this.state.zoom <= MIN_ZOOM) {
|
||||||
|
this.setState({
|
||||||
|
zoom: MIN_ZOOM,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
zoom: this.state.zoom - ZOOM_STEP,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDownloadClick = () => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = this.props.src;
|
||||||
|
a.download = this.props.name;
|
||||||
|
a.target = "_blank";
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onOpenContextMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
contextMenuDisplayed: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCloseContextMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
contextMenuDisplayed: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPermalinkClicked = (ev: React.MouseEvent) => {
|
||||||
|
// This allows the permalink to be opened in a new tab/window or copied as
|
||||||
|
// matrix.to, but also for it to enable routing within Element when clicked.
|
||||||
|
ev.preventDefault();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
event_id: this.props.mxEvent.getId(),
|
||||||
|
highlighted: true,
|
||||||
|
room_id: this.props.mxEvent.getRoomId(),
|
||||||
|
});
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onStartMoving = (ev: React.MouseEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// Zoom in if we are completely zoomed out
|
||||||
|
if (this.state.zoom === MIN_ZOOM) {
|
||||||
|
this.setState({zoom: MAX_ZOOM});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({moving: true});
|
||||||
|
this.previousX = this.state.translationX;
|
||||||
|
this.previousY = this.state.translationY;
|
||||||
|
this.initX = ev.pageX - this.lastX;
|
||||||
|
this.initY = ev.pageY - this.lastY;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoving = (ev: React.MouseEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (!this.state.moving) return;
|
||||||
|
|
||||||
|
this.lastX = ev.pageX - this.initX;
|
||||||
|
this.lastY = ev.pageY - this.initY;
|
||||||
|
this.setState({
|
||||||
|
translationX: this.lastX,
|
||||||
|
translationY: this.lastY,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEndMoving = () => {
|
||||||
|
// Zoom out if we haven't moved much
|
||||||
|
if (
|
||||||
|
this.state.moving === true &&
|
||||||
|
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
|
||||||
|
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
zoom: MIN_ZOOM,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({moving: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderContextMenu() {
|
||||||
|
let contextMenu = null;
|
||||||
|
if (this.state.contextMenuDisplayed) {
|
||||||
|
contextMenu = (
|
||||||
|
<ContextMenu
|
||||||
|
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
|
||||||
|
onFinished={this.onCloseContextMenu}
|
||||||
|
>
|
||||||
|
<MessageContextMenu
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
onFinished={this.onCloseContextMenu}
|
||||||
|
onCloseDialog={this.props.onFinished}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{ contextMenu }
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const showEventMeta = !!this.props.mxEvent;
|
||||||
|
|
||||||
|
let cursor;
|
||||||
|
if (this.state.moving) {
|
||||||
|
cursor= "grabbing";
|
||||||
|
} else if (this.state.zoom === MIN_ZOOM) {
|
||||||
|
cursor = "zoom-in";
|
||||||
|
} else {
|
||||||
|
cursor = "zoom-out";
|
||||||
|
}
|
||||||
|
const rotationDegrees = this.state.rotation + "deg";
|
||||||
|
const zoomPercentage = this.state.zoom/100;
|
||||||
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
|
// The order of the values is important!
|
||||||
|
// First, we translate and only then we rotate, otherwise
|
||||||
|
// we would apply the translation to an already rotated
|
||||||
|
// image causing it translate in the wrong direction.
|
||||||
|
const style = {
|
||||||
|
cursor: cursor,
|
||||||
|
transition: this.state.moving ? null : "transform 200ms ease 0s",
|
||||||
|
transform: `translateX(${translatePixelsX})
|
||||||
|
translateY(${translatePixelsY})
|
||||||
|
scale(${zoomPercentage})
|
||||||
|
rotate(${rotationDegrees})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let info;
|
||||||
|
if (showEventMeta) {
|
||||||
|
const mxEvent = this.props.mxEvent;
|
||||||
|
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||||
|
let permalink = "#";
|
||||||
|
if (this.props.permalinkCreator) {
|
||||||
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
|
const sender = (
|
||||||
|
<div className="mx_ImageView_info_sender">
|
||||||
|
{senderName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const messageTimestamp = (
|
||||||
|
<a
|
||||||
|
href={permalink}
|
||||||
|
onClick={this.onPermalinkClicked}
|
||||||
|
aria-label={formatFullDate(new Date(this.props.mxEvent.getTs()), showTwelveHour, false)}
|
||||||
|
>
|
||||||
|
<MessageTimestamp
|
||||||
|
showFullDate={true}
|
||||||
|
showTwelveHour={showTwelveHour}
|
||||||
|
ts={mxEvent.getTs()}
|
||||||
|
showSeconds={false}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
const avatar = (
|
||||||
|
<MemberAvatar
|
||||||
|
member={mxEvent.sender}
|
||||||
|
width={32} height={32}
|
||||||
|
viewUserOnClick={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
info = (
|
||||||
|
<div className="mx_ImageView_info_wrapper">
|
||||||
|
{avatar}
|
||||||
|
<div className="mx_ImageView_info">
|
||||||
|
{sender}
|
||||||
|
{messageTimestamp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If there is no event - we're viewing an avatar, we set
|
||||||
|
// an empty div here, since the panel uses space-between
|
||||||
|
// and we want the same placement of elements
|
||||||
|
info = (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contextMenuButton;
|
||||||
|
if (this.props.mxEvent) {
|
||||||
|
contextMenuButton = (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_more"
|
||||||
|
title={_t("Options")}
|
||||||
|
onClick={this.onOpenContextMenu}
|
||||||
|
inputRef={this.contextMenuButton}
|
||||||
|
isExpanded={this.state.contextMenuDisplayed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusLock
|
||||||
|
returnFocus={true}
|
||||||
|
lockProps={{
|
||||||
|
onKeyDown: this.onKeyDown,
|
||||||
|
role: "dialog",
|
||||||
|
}}
|
||||||
|
className="mx_ImageView"
|
||||||
|
ref={this.focusLock}
|
||||||
|
>
|
||||||
|
<div className="mx_ImageView_panel">
|
||||||
|
{info}
|
||||||
|
<div className="mx_ImageView_toolbar">
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||||
|
title={_t("Rotate Right")}
|
||||||
|
onClick={this.onRotateClockwiseClick}>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||||
|
title={_t("Rotate Left")}
|
||||||
|
onClick={ this.onRotateCounterClockwiseClick }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||||
|
title={_t("Zoom out")}
|
||||||
|
onClick={ this.onZoomOutClick }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||||
|
title={_t("Zoom in")}
|
||||||
|
onClick={ this.onZoomInClick }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_download"
|
||||||
|
title={_t("Download")}
|
||||||
|
onClick={ this.onDownloadClick }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
{contextMenuButton}
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ImageView_button mx_ImageView_button_close"
|
||||||
|
title={_t("Close")}
|
||||||
|
onClick={ this.props.onFinished }>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
{this.renderContextMenu()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_ImageView_image_wrapper">
|
||||||
|
<img
|
||||||
|
src={this.props.src}
|
||||||
|
title={this.props.name}
|
||||||
|
style={style}
|
||||||
|
className="mx_ImageView_image"
|
||||||
|
draggable={true}
|
||||||
|
onMouseDown={this.onStartMoving}
|
||||||
|
onMouseMove={this.onMoving}
|
||||||
|
onMouseUp={this.onEndMoving}
|
||||||
|
onMouseLeave={this.onEndMoving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusLock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
src/components/views/elements/InviteReason.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
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 classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.InviteReason")
|
||||||
|
export default class InviteReason extends React.PureComponent<IProps, IState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
// We hide the reason for invitation by default, since it can be a
|
||||||
|
// vector for spam/harassment.
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewClick = () => {
|
||||||
|
this.setState({
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const classes = classNames({
|
||||||
|
"mx_InviteReason": true,
|
||||||
|
"mx_InviteReason_hidden": this.state.hidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={classes}>
|
||||||
|
<div className="mx_InviteReason_reason">{this.props.reason}</div>
|
||||||
|
<div className="mx_InviteReason_view"
|
||||||
|
onClick={this.onViewClick}
|
||||||
|
>
|
||||||
|
{_t("View message")}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,9 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
/* the maximum image height to use */
|
/* the maximum image height to use */
|
||||||
maxImageHeight: PropTypes.number,
|
maxImageHeight: PropTypes.number,
|
||||||
|
|
||||||
|
/* the permalinkCreator */
|
||||||
|
permalinkCreator: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
@ -106,6 +109,7 @@ export default class MImageBody extends React.Component {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
|
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (content.info) {
|
if (content.info) {
|
||||||
|
@ -114,7 +118,7 @@ export default class MImageBody extends React.Component {
|
||||||
params.fileSize = content.info.size;
|
params.fileSize = content.info.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
// enable the play button. Firefox does not seem to care either
|
// enable the play button. Firefox does not seem to care either
|
||||||
// way, so it's fine to do for all browsers.
|
// way, so it's fine to do for all browsers.
|
||||||
decryptedUrl: `data:${content?.info?.mimetype},`,
|
decryptedUrl: `data:${content?.info?.mimetype},`,
|
||||||
decryptedThumbnailUrl: thumbnailUrl,
|
decryptedThumbnailUrl: thumbnailUrl || `data:${content?.info?.mimetype},`,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ export default class MessageEvent extends React.Component {
|
||||||
|
|
||||||
/* the maximum image height to use, if the event is an image */
|
/* the maximum image height to use, if the event is an image */
|
||||||
maxImageHeight: PropTypes.number,
|
maxImageHeight: PropTypes.number,
|
||||||
|
|
||||||
|
/* the permalinkCreator */
|
||||||
|
permalinkCreator: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -126,6 +129,7 @@ export default class MessageEvent extends React.Component {
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
onMessageAllowed={this.onTileUpdate}
|
onMessageAllowed={this.onTileUpdate}
|
||||||
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {formatFullDate, formatTime} from '../../../DateUtils';
|
import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MessageTimestamp")
|
@replaceableComponent("views.messages.MessageTimestamp")
|
||||||
|
@ -25,13 +25,24 @@ export default class MessageTimestamp extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ts: PropTypes.number.isRequired,
|
ts: PropTypes.number.isRequired,
|
||||||
showTwelveHour: PropTypes.bool,
|
showTwelveHour: PropTypes.bool,
|
||||||
|
showFullDate: PropTypes.bool,
|
||||||
|
showSeconds: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const date = new Date(this.props.ts);
|
const date = new Date(this.props.ts);
|
||||||
|
let timestamp;
|
||||||
|
if (this.props.showFullDate) {
|
||||||
|
timestamp = formatFullDate(date, this.props.showTwelveHour, this.props.showSeconds);
|
||||||
|
} else if (this.props.showSeconds) {
|
||||||
|
timestamp = formatFullTime(date, this.props.showTwelveHour);
|
||||||
|
} else {
|
||||||
|
timestamp = formatTime(date, this.props.showTwelveHour);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
||||||
{ formatTime(date, this.props.showTwelveHour) }
|
{timestamp}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class RoomAvatarEvent extends React.Component {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
name: text,
|
name: text,
|
||||||
};
|
};
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
||||||
import {User} from 'matrix-js-sdk/src/models/user';
|
import {User} from 'matrix-js-sdk/src/models/user';
|
||||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -496,11 +497,11 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
||||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
if (!room) {
|
if (!room) return;
|
||||||
return;
|
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||||
}
|
|
||||||
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||||
if (event) {
|
if (event) {
|
||||||
setPowerLevels(event.getContent());
|
setPowerLevels(event.getContent());
|
||||||
} else {
|
} else {
|
||||||
|
@ -511,7 +512,7 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
};
|
};
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(cli, "RoomState.members", update);
|
useEventEmitter(cli, "RoomState.events", update);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
update();
|
update();
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1431,7 +1432,7 @@ const UserInfoHeader: React.FC<{
|
||||||
name: member.name,
|
name: member.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
}, [member]);
|
}, [member]);
|
||||||
|
|
||||||
const avatarElement = (
|
const avatarElement = (
|
||||||
|
@ -1494,7 +1495,7 @@ const UserInfoHeader: React.FC<{
|
||||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = member.name || member.displayname;
|
const displayName = member.rawDisplayName || member.displayname;
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
|
|
||||||
|
|
|
@ -149,8 +149,8 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
const callView = (
|
const callView = (
|
||||||
<CallViewForRoom
|
<CallViewForRoom
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
onResize={this.props.onResize}
|
|
||||||
maxVideoHeight={this.props.maxHeight}
|
maxVideoHeight={this.props.maxHeight}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps) {
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
|
// We need to re-check the placeholder when the enabled state changes because it causes the
|
||||||
|
// placeholder element to remount, which gets rid of the `::before` class. Re-evaluating the
|
||||||
|
// placeholder means we get a proper `::before` with the placeholder.
|
||||||
|
const enabledChange = this.props.disabled !== prevProps.disabled;
|
||||||
|
const placeholderChanged = this.props.placeholder !== prevProps.placeholder;
|
||||||
|
if (this.props.placeholder && (placeholderChanged || enabledChange)) {
|
||||||
const {isEmpty} = this.props.model;
|
const {isEmpty} = this.props.model;
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
this.showPlaceholder();
|
this.showPlaceholder();
|
||||||
|
@ -670,8 +675,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
});
|
});
|
||||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||||
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
||||||
|
|
||||||
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
|
|
||||||
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
|
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,18 +15,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
|
||||||
import {formatTime} from "../../../DateUtils";
|
import {formatTime} from "../../../DateUtils";
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||||
|
@ -43,39 +42,56 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import Tooltip from "../elements/Tooltip";
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||||
'm.sticker': 'messages.MessageEvent',
|
[EventType.Sticker]: 'messages.MessageEvent',
|
||||||
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
|
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
||||||
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
|
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
||||||
'm.room.encryption': 'messages.EncryptionEvent',
|
[EventType.CallInvite]: 'messages.TextualEvent',
|
||||||
'm.call.invite': 'messages.TextualEvent',
|
[EventType.CallAnswer]: 'messages.TextualEvent',
|
||||||
'm.call.answer': 'messages.TextualEvent',
|
[EventType.CallHangup]: 'messages.TextualEvent',
|
||||||
'm.call.hangup': 'messages.TextualEvent',
|
[EventType.CallReject]: 'messages.TextualEvent',
|
||||||
'm.call.reject': 'messages.TextualEvent',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateEventTileTypes = {
|
const stateEventTileTypes = {
|
||||||
'm.room.encryption': 'messages.EncryptionEvent',
|
[EventType.RoomEncryption]: 'messages.EncryptionEvent',
|
||||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
[EventType.RoomCanonicalAlias]: 'messages.TextualEvent',
|
||||||
'm.room.create': 'messages.RoomCreate',
|
[EventType.RoomCreate]: 'messages.RoomCreate',
|
||||||
'm.room.member': 'messages.TextualEvent',
|
[EventType.RoomMember]: 'messages.TextualEvent',
|
||||||
'm.room.name': 'messages.TextualEvent',
|
[EventType.RoomName]: 'messages.TextualEvent',
|
||||||
'm.room.avatar': 'messages.RoomAvatarEvent',
|
[EventType.RoomAvatar]: 'messages.RoomAvatarEvent',
|
||||||
'm.room.third_party_invite': 'messages.TextualEvent',
|
[EventType.RoomThirdPartyInvite]: 'messages.TextualEvent',
|
||||||
'm.room.history_visibility': 'messages.TextualEvent',
|
[EventType.RoomHistoryVisibility]: 'messages.TextualEvent',
|
||||||
'm.room.topic': 'messages.TextualEvent',
|
[EventType.RoomTopic]: 'messages.TextualEvent',
|
||||||
'm.room.power_levels': 'messages.TextualEvent',
|
[EventType.RoomPowerLevels]: 'messages.TextualEvent',
|
||||||
'm.room.pinned_events': 'messages.TextualEvent',
|
[EventType.RoomPinnedEvents]: 'messages.TextualEvent',
|
||||||
'm.room.server_acl': 'messages.TextualEvent',
|
[EventType.RoomServerAcl]: 'messages.TextualEvent',
|
||||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||||
'm.room.tombstone': 'messages.TextualEvent',
|
[EventType.RoomTombstone]: 'messages.TextualEvent',
|
||||||
'm.room.join_rules': 'messages.TextualEvent',
|
[EventType.RoomJoinRules]: 'messages.TextualEvent',
|
||||||
'm.room.guest_access': 'messages.TextualEvent',
|
[EventType.RoomGuestAccess]: 'messages.TextualEvent',
|
||||||
'm.room.related_groups': 'messages.TextualEvent',
|
'm.room.related_groups': 'messages.TextualEvent', // legacy communities flair
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stateEventSingular = new Set([
|
||||||
|
EventType.RoomEncryption,
|
||||||
|
EventType.RoomCanonicalAlias,
|
||||||
|
EventType.RoomCreate,
|
||||||
|
EventType.RoomName,
|
||||||
|
EventType.RoomAvatar,
|
||||||
|
EventType.RoomHistoryVisibility,
|
||||||
|
EventType.RoomTopic,
|
||||||
|
EventType.RoomPowerLevels,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
EventType.RoomServerAcl,
|
||||||
|
WIDGET_LAYOUT_EVENT_TYPE,
|
||||||
|
EventType.RoomTombstone,
|
||||||
|
EventType.RoomJoinRules,
|
||||||
|
EventType.RoomGuestAccess,
|
||||||
|
'm.room.related_groups',
|
||||||
|
]);
|
||||||
|
|
||||||
// Add all the Mjolnir stuff to the renderer
|
// Add all the Mjolnir stuff to the renderer
|
||||||
for (const evType of ALL_RULE_TYPES) {
|
for (const evType of ALL_RULE_TYPES) {
|
||||||
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
||||||
|
@ -132,7 +148,12 @@ export function getHandlerTile(ev) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
if (ev.isState()) {
|
||||||
|
if (stateEventSingular.has(type) && ev.getStateKey() !== "") return undefined;
|
||||||
|
return stateEventTileTypes[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTileTypes[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_READ_AVATARS = 5;
|
const MAX_READ_AVATARS = 5;
|
||||||
|
@ -239,6 +260,9 @@ export default class EventTile extends React.Component {
|
||||||
|
|
||||||
// whether or not to show flair at all
|
// whether or not to show flair at all
|
||||||
enableFlair: PropTypes.bool,
|
enableFlair: PropTypes.bool,
|
||||||
|
|
||||||
|
// whether or not to show read receipts
|
||||||
|
showReadReceipts: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -837,8 +861,6 @@ export default class EventTile extends React.Component {
|
||||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
const readAvatars = this.getReadAvatars();
|
|
||||||
|
|
||||||
let avatar;
|
let avatar;
|
||||||
let sender;
|
let sender;
|
||||||
let avatarSize;
|
let avatarSize;
|
||||||
|
@ -967,6 +989,16 @@ export default class EventTile extends React.Component {
|
||||||
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||||
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||||
|
|
||||||
|
let msgOption;
|
||||||
|
if (this.props.showReadReceipts) {
|
||||||
|
const readAvatars = this.getReadAvatars();
|
||||||
|
msgOption = (
|
||||||
|
<div className="mx_EventTile_msgOption">
|
||||||
|
{ readAvatars }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
case 'notif': {
|
case 'notif': {
|
||||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
|
@ -1080,14 +1112,13 @@ export default class EventTile extends React.Component {
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
onHeightChanged={this.props.onHeightChanged} />
|
onHeightChanged={this.props.onHeightChanged} />
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ reactionsRow }
|
{ reactionsRow }
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EventTile_msgOption">
|
{msgOption}
|
||||||
{ readAvatars }
|
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default class LinkPreviewWidget extends React.Component {
|
||||||
link: this.props.link,
|
link: this.props.link,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -29,11 +29,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||||
|
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||||
|
|
||||||
function ComposerAvatar(props) {
|
function ComposerAvatar(props) {
|
||||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||||
|
@ -178,15 +177,12 @@ export default class MessageComposer extends React.Component {
|
||||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||||
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
|
VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate);
|
||||||
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
|
|
||||||
this._dispatcherRef = null;
|
this._dispatcherRef = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
tombstone: this._getRoomTombstone(),
|
tombstone: this._getRoomTombstone(),
|
||||||
canSendMessages: this.props.room.maySendMessage(),
|
canSendMessages: this.props.room.maySendMessage(),
|
||||||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
|
||||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
|
||||||
isComposerEmpty: true,
|
isComposerEmpty: true,
|
||||||
haveRecording: false,
|
haveRecording: false,
|
||||||
};
|
};
|
||||||
|
@ -204,14 +200,6 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onWidgetUpdate = () => {
|
|
||||||
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
|
|
||||||
};
|
|
||||||
|
|
||||||
_onActiveWidgetUpdate = () => {
|
|
||||||
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||||
|
@ -238,8 +226,7 @@ export default class MessageComposer extends React.Component {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
}
|
}
|
||||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
|
VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate);
|
||||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
|
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,8 +314,8 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceUpdate = (haveRecording: boolean) => {
|
_onVoiceStoreUpdate = () => {
|
||||||
this.setState({haveRecording});
|
this.setState({haveRecording: !!VoiceRecordingStore.instance.activeRecording});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -352,7 +339,6 @@ export default class MessageComposer extends React.Component {
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
replyToEvent={this.props.replyToEvent}
|
replyToEvent={this.props.replyToEvent}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
// TODO: @@ TravisR - Disabling the composer doesn't work
|
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
@ -373,8 +359,7 @@ export default class MessageComposer extends React.Component {
|
||||||
if (SettingsStore.getValue("feature_voice_messages")) {
|
if (SettingsStore.getValue("feature_voice_messages")) {
|
||||||
controls.push(<VoiceRecordComposerTile
|
controls.push(<VoiceRecordComposerTile
|
||||||
key="controls_voice_record"
|
key="controls_voice_record"
|
||||||
room={this.props.room}
|
room={this.props.room} />);
|
||||||
onRecording={this.onVoiceUpdate} />);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||||
|
|
|
@ -17,22 +17,13 @@ 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 '../../../VelocityBounce';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {formatDate} from '../../../DateUtils';
|
import {formatDate} from '../../../DateUtils';
|
||||||
import Velociraptor from "../../../Velociraptor";
|
import NodeAnimator from "../../../NodeAnimator";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
let bounce = false;
|
|
||||||
try {
|
|
||||||
if (global.localStorage) {
|
|
||||||
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ReadReceiptMarker")
|
@replaceableComponent("views.rooms.ReadReceiptMarker")
|
||||||
export default class ReadReceiptMarker extends React.PureComponent {
|
export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
// we've already done our display - nothing more to do.
|
// we've already done our display - nothing more to do.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._animateMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
|
||||||
|
const visibilityChanged = prevProps.hidden !== this.props.hidden;
|
||||||
|
if (differentLeftOffset || visibilityChanged) {
|
||||||
|
this._animateMarker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_animateMarker() {
|
||||||
// treat new RRs as though they were off the top of the screen
|
// treat new RRs as though they were off the top of the screen
|
||||||
let oldTop = -15;
|
let oldTop = -15;
|
||||||
|
|
||||||
|
@ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const startStyles = [];
|
const startStyles = [];
|
||||||
const enterTransitionOpts = [];
|
|
||||||
|
|
||||||
if (oldInfo && oldInfo.left) {
|
if (oldInfo && oldInfo.left) {
|
||||||
// start at the old height and in the old h pos
|
// start at the old height and in the old h pos
|
||||||
|
|
||||||
startStyles.push({ top: startTopOffset+"px",
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
left: toPx(oldInfo.left) });
|
left: toPx(oldInfo.left) });
|
||||||
|
|
||||||
const reorderTransitionOpts = {
|
|
||||||
duration: 100,
|
|
||||||
easing: 'easeOut',
|
|
||||||
};
|
|
||||||
|
|
||||||
enterTransitionOpts.push(reorderTransitionOpts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// then shift to the rightmost column,
|
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||||
// and then it will drop down to its resting position
|
|
||||||
//
|
|
||||||
// XXX: We use a small left value to trick velocity-animate into actually animating.
|
|
||||||
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
|
|
||||||
// skip applying it, thus making our read receipt at +14px instead of +0px like it
|
|
||||||
// should be. This does cause a tiny amount of drift for read receipts, however with a
|
|
||||||
// value so small it's not perceived by a user.
|
|
||||||
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
|
|
||||||
// fail to fall down or cause gaps.
|
|
||||||
startStyles.push({ top: startTopOffset+'px', left: '1px' });
|
|
||||||
enterTransitionOpts.push({
|
|
||||||
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
|
||||||
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
suppressDisplay: false,
|
suppressDisplay: false,
|
||||||
startStyles: startStyles,
|
startStyles: startStyles,
|
||||||
enterTransitionOpts: enterTransitionOpts,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
const style = {
|
const style = {
|
||||||
left: toPx(this.props.leftOffset),
|
left: toPx(this.props.leftOffset),
|
||||||
top: '0px',
|
top: '0px',
|
||||||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
@ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Velociraptor
|
<NodeAnimator
|
||||||
startStyles={this.state.startStyles}
|
startStyles={this.state.startStyles} >
|
||||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
fallbackUserId={this.props.fallbackUserId}
|
fallbackUserId={this.props.fallbackUserId}
|
||||||
|
@ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
inputRef={this._avatar}
|
inputRef={this._avatar}
|
||||||
/>
|
/>
|
||||||
</Velociraptor>
|
</NodeAnimator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
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.
|
||||||
|
@ -27,7 +25,8 @@ import SdkConfig from "../../../SdkConfig";
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import InviteReason from "../elements/InviteReason";
|
||||||
|
|
||||||
const MessageCase = Object.freeze({
|
const MessageCase = Object.freeze({
|
||||||
NotLoggedIn: "NotLoggedIn",
|
NotLoggedIn: "NotLoggedIn",
|
||||||
|
@ -306,6 +305,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let title;
|
let title;
|
||||||
let subTitle;
|
let subTitle;
|
||||||
|
let reasonElement;
|
||||||
let primaryActionHandler;
|
let primaryActionHandler;
|
||||||
let primaryActionLabel;
|
let primaryActionLabel;
|
||||||
let secondaryActionHandler;
|
let secondaryActionHandler;
|
||||||
|
@ -491,6 +491,12 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
primaryActionLabel = _t("Accept");
|
primaryActionLabel = _t("Accept");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
|
||||||
|
if (reason) {
|
||||||
|
reasonElement = <InviteReason reason={reason} />;
|
||||||
|
}
|
||||||
|
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
secondaryActionLabel = _t("Reject");
|
secondaryActionLabel = _t("Reject");
|
||||||
secondaryActionHandler = this.props.onRejectClick;
|
secondaryActionHandler = this.props.onRejectClick;
|
||||||
|
@ -582,6 +588,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
{ titleElement }
|
{ titleElement }
|
||||||
{ subTitleElements }
|
{ subTitleElements }
|
||||||
</div>
|
</div>
|
||||||
|
{ reasonElement }
|
||||||
<div className="mx_RoomPreviewBar_actions">
|
<div className="mx_RoomPreviewBar_actions">
|
||||||
{ secondaryButton }
|
{ secondaryButton }
|
||||||
{ extraComponents }
|
{ extraComponents }
|
||||||
|
|
|
@ -563,7 +563,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.showMessagePreview && this.state.messagePreview) {
|
if (this.showMessagePreview && this.state.messagePreview) {
|
||||||
messagePreview = (
|
messagePreview = (
|
||||||
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
<div
|
||||||
|
className="mx_RoomTile_messagePreview"
|
||||||
|
id={messagePreviewId(this.props.room.roomId)}
|
||||||
|
title={this.state.messagePreview}
|
||||||
|
>
|
||||||
{this.state.messagePreview}
|
{this.state.messagePreview}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -477,6 +477,10 @@ export default class SendMessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
onAction = (payload) => {
|
||||||
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
// to the cursor being in the composer
|
||||||
|
if (this.props.disabled) return;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'reply_to_event':
|
case 'reply_to_event':
|
||||||
case Action.FocusComposer:
|
case Action.FocusComposer:
|
||||||
|
|
|
@ -17,21 +17,21 @@ limitations under the License.
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
|
import {VoiceRecording} from "../../../voice/VoiceRecording";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||||
|
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
onRecording: (haveRecording: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
recorder?: VoiceRecorder;
|
recorder?: VoiceRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,13 +57,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
msgtype: "org.matrix.msc2516.voice",
|
msgtype: "org.matrix.msc2516.voice",
|
||||||
url: mxc,
|
url: mxc,
|
||||||
});
|
});
|
||||||
|
await VoiceRecordingStore.instance.disposeRecording();
|
||||||
this.setState({recorder: null});
|
this.setState({recorder: null});
|
||||||
this.props.onRecording(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const recorder = new VoiceRecorder(MatrixClientPeg.get());
|
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||||
await recorder.start();
|
await recorder.start();
|
||||||
this.props.onRecording(true);
|
|
||||||
this.setState({recorder});
|
this.setState({recorder});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,12 @@ import Modal from "../../../Modal";
|
||||||
import PassphraseField from "../auth/PassphraseField";
|
import PassphraseField from "../auth/PassphraseField";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||||
|
|
||||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
|
||||||
|
|
||||||
@replaceableComponent("views.settings.ChangePassword")
|
@replaceableComponent("views.settings.ChangePassword")
|
||||||
export default class ChangePassword extends React.Component {
|
export default class ChangePassword extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|