Merge branch 'develop' into feed
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -22,7 +22,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_RoomView_messageListWrapper {
|
.mx_FilePanel .mx_RoomView_messageListWrapper {
|
||||||
margin-right: 20px;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
|
|
||||||
.mx_SpacePanel_spaceTreeWrapper {
|
.mx_SpacePanel_spaceTreeWrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: scroll;
|
padding: 8px 8px 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpacePanel_toggleCollapse {
|
.mx_SpacePanel_toggleCollapse {
|
||||||
|
@ -59,11 +59,10 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
> .mx_SpaceItem {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AutoHideScrollbar {
|
|
||||||
padding: 8px 0 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceButton_toggleCollapse {
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
@ -276,6 +275,7 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
.mx_SpaceButton:hover,
|
.mx_SpaceButton:hover,
|
||||||
.mx_SpaceButton:focus-within,
|
.mx_SpaceButton:focus-within,
|
||||||
.mx_SpaceButton_hasMenuOpen {
|
.mx_SpaceButton_hasMenuOpen {
|
||||||
|
&:not(.mx_SpaceButton_home) {
|
||||||
// Hide the badge container on hover because it'll be a menu button
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -288,6 +288,7 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* root space buttons are bigger and not indented */
|
/* root space buttons are bigger and not indented */
|
||||||
& > .mx_AutoHideScrollbar {
|
& > .mx_AutoHideScrollbar {
|
||||||
|
|
|
@ -224,35 +224,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
|
|
||||||
.mx_FacePile_faces {
|
.mx_FacePile_faces {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
> span:hover {
|
|
||||||
.mx_BaseAvatar {
|
|
||||||
filter: brightness(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:first-child {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
|
||||||
filter: brightness(0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
background: #ffffff; // white icon fill
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 24px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -101,7 +101,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SearchBox {
|
.mx_SearchBox {
|
||||||
margin: 0;
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +124,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_section {
|
.mx_AddExistingToSpaceDialog_section {
|
||||||
|
&:not(:first-child) {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
> h3 {
|
> h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
> span + span {
|
> .mx_FacePile_face + .mx_FacePile_face {
|
||||||
margin-right: -8px;
|
margin-right: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +31,32 @@ limitations under the License.
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
margin: 1px; // to offset the border on the image
|
margin: 1px; // to offset the border on the image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_FacePile_more {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background: $tertiary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> span {
|
.mx_FacePile_summary {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -40,35 +40,6 @@ limitations under the License.
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomPreviewBar_reason {
|
|
||||||
text-align: left;
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
border: 1px solid $invite-reason-border-color;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0 16px 12px 16px;
|
|
||||||
margin: 5px 0 20px 0;
|
|
||||||
|
|
||||||
div {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MatrixChat_useCompactLayout & {
|
|
||||||
padding-top: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_EventTilePreview_faded {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_SenderProfile, .mx_EventTile_avatar {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Spinner {
|
.mx_Spinner {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -18,6 +18,10 @@ limitations under the License.
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&.mx_RoomSublist_hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_headerContainer {
|
.mx_RoomSublist_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -37,7 +41,9 @@ limitations under the License.
|
||||||
// The combined height must be set in the LeftPanel component for sticky headers
|
// The combined height must be set in the LeftPanel component for sticky headers
|
||||||
// to work correctly.
|
// to work correctly.
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
height: 24px;
|
// Allow the container to collapse on itself if its children
|
||||||
|
// are not in the normal document flow
|
||||||
|
max-height: 24px;
|
||||||
color: $roomlist-header-color;
|
color: $roomlist-header-color;
|
||||||
|
|
||||||
.mx_RoomSublist_stickable {
|
.mx_RoomSublist_stickable {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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: 0.85;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: #21262c;
|
$settings-profile-placeholder-bg-color: #21262c;
|
||||||
|
@ -209,8 +210,6 @@ $message-body-panel-fg-color: $primary-fg-color;
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
$invite-reason-border-color: $room-highlight-color;
|
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||||
$roomlist-background-blur-amount: 60px;
|
$roomlist-background-blur-amount: 60px;
|
||||||
$groupFilterPanel-background-blur-amount: 30px;
|
$groupFilterPanel-background-blur-amount: 30px;
|
||||||
|
@ -244,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: 0.85;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||||
|
@ -204,8 +205,6 @@ $message-body-panel-fg-color: $primary-fg-color;
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
$invite-reason-border-color: $room-highlight-color;
|
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
|
@ -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: 0.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;
|
||||||
|
@ -333,8 +335,6 @@ $message-body-panel-fg-color: $muted-fg-color;
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
$invite-reason-border-color: $input-darker-bg-color;
|
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
|
@ -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: 0.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;
|
||||||
|
@ -331,8 +332,6 @@ $message-body-panel-fg-color: $muted-fg-color;
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
$invite-reason-border-color: $input-darker-bg-color;
|
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||||
$roomlist-background-blur-amount: 40px;
|
$roomlist-background-blur-amount: 40px;
|
||||||
$groupFilterPanel-background-blur-amount: 20px;
|
$groupFilterPanel-background-blur-amount: 20px;
|
||||||
|
|
30
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 {
|
||||||
|
@ -139,4 +139,30 @@ declare global {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber
|
||||||
columnNumber?: number;
|
columnNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
|
||||||
|
interface AudioWorkletProcessor {
|
||||||
|
readonly port: MessagePort;
|
||||||
|
process(
|
||||||
|
inputs: Float32Array[][],
|
||||||
|
outputs: Float32Array[][],
|
||||||
|
parameters: Record<string, Float32Array>
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
|
||||||
|
const AudioWorkletProcessor: {
|
||||||
|
prototype: AudioWorkletProcessor;
|
||||||
|
new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
|
||||||
|
function registerProcessor(
|
||||||
|
name: string,
|
||||||
|
processorCtor: (new (
|
||||||
|
options?: AudioWorkletNodeOptions
|
||||||
|
) => AudioWorkletProcessor) & {
|
||||||
|
parameterDescriptors?: AudioParamDescriptor[];
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -1222,4 +1222,5 @@ export function getCommand(input: string) {
|
||||||
args,
|
args,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -154,7 +154,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
private doStickyHeaders(list: HTMLDivElement) {
|
private doStickyHeaders(list: HTMLDivElement) {
|
||||||
const topEdge = list.scrollTop;
|
const topEdge = list.scrollTop;
|
||||||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
||||||
|
|
||||||
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||||
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -1091,8 +1096,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const isSpace = roomToLeave?.isSpaceRoom();
|
const isSpace = roomToLeave?.isSpaceRoom();
|
||||||
// Show a warning if there are additional complications.
|
// Show a warning if there are additional complications.
|
||||||
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
|
const memberCount = roomToLeave.currentState.getJoinedMemberCount();
|
||||||
|
if (memberCount === 1) {
|
||||||
|
warnings.push((
|
||||||
|
<span className="warning" key="only_member_warning">
|
||||||
|
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||||
|
{ _t("You are the only person here. " +
|
||||||
|
"If you leave, no one will be able to join in the future, including you.") }
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
||||||
if (joinRules) {
|
if (joinRules) {
|
||||||
const rule = joinRules.getContent().join_rule;
|
const rule = joinRules.getContent().join_rule;
|
||||||
if (rule !== "public") {
|
if (rule !== "public") {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -57,21 +57,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
|
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
const existingSubspacesSet = new Set(existingSubspaces);
|
const existingSubspacesSet = new Set(existingSubspaces);
|
||||||
const spaces = SpaceStore.instance.getSpaces().filter(s => {
|
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
||||||
return !existingSubspacesSet.has(s) // not already in space
|
|
||||||
&& space !== s // not the top-level space
|
|
||||||
&& selectedSpace !== s // not the selected space
|
|
||||||
&& s.name.toLowerCase().includes(lcQuery); // contains query
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
|
const joinRule = selectedSpace.getJoinRule();
|
||||||
const existingRoomsSet = new Set(existingRooms);
|
const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => {
|
||||||
const rooms = cli.getVisibleRooms().filter(room => {
|
if (room.getMyMembership() !== "join") return arr;
|
||||||
return !existingRoomsSet.has(room) // not already in space
|
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
||||||
&& !room.isSpaceRoom() // not a space itself
|
|
||||||
&& room.name.toLowerCase().includes(lcQuery) // contains query
|
if (room.isSpaceRoom()) {
|
||||||
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
|
if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) {
|
||||||
});
|
arr[0].push(room);
|
||||||
|
}
|
||||||
|
} else if (!existingRoomsSet.has(room) && joinRule !== "public") {
|
||||||
|
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
||||||
|
arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [[], [], []]);
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
@ -172,7 +174,28 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
</div>
|
</div>
|
||||||
) : null }
|
) : null }
|
||||||
|
|
||||||
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
{ dms.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_section">
|
||||||
|
<h3>{ _t("Direct Messages") }</h3>
|
||||||
|
{ dms.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={selectedToAdd.has(space)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedToAdd.add(space);
|
||||||
|
} else {
|
||||||
|
selectedToAdd.delete(space);
|
||||||
|
}
|
||||||
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
||||||
{ _t("No results") }
|
{ _t("No results") }
|
||||||
</span> : undefined }
|
</span> : undefined }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||||
|
IInvite3PID,
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
@ -618,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);
|
||||||
}
|
}
|
||||||
|
@ -646,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;
|
||||||
|
@ -656,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 () => {
|
||||||
|
@ -712,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", "",
|
||||||
);
|
);
|
||||||
|
@ -1344,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", "",
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default class SeshatResetDialog extends React.PureComponent<IDialogProps>
|
||||||
{_t("You most likely do not want to reset your event index store")}
|
{_t("You most likely do not want to reset your event index store")}
|
||||||
<br />
|
<br />
|
||||||
{_t("If you do, please note that none of your messages will be deleted, " +
|
{_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" +
|
"but the search experience might be degraded for a few moments " +
|
||||||
"whilst the index is recreated",
|
"whilst the index is recreated",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -55,22 +55,10 @@ interface IProps {
|
||||||
* The mxc:// avatar URL of the displayed user
|
* The mxc:// avatar URL of the displayed user
|
||||||
*/
|
*/
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the EventTile should appear faded
|
|
||||||
*/
|
|
||||||
faded?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback for when the component is clicked
|
|
||||||
*/
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
message: string;
|
message: string;
|
||||||
faded: boolean;
|
|
||||||
eventTileKey: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVATAR_SIZE = 32;
|
const AVATAR_SIZE = 32;
|
||||||
|
@ -81,23 +69,9 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
message: props.message,
|
message: props.message,
|
||||||
faded: !!props.faded,
|
|
||||||
eventTileKey: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMessage(message: string) {
|
|
||||||
this.setState({
|
|
||||||
message,
|
|
||||||
// Change the EventTile key to force React to create a new instance
|
|
||||||
eventTileKey: this.state.eventTileKey + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unfade() {
|
|
||||||
this.setState({ faded: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private fakeEvent({message}: IState) {
|
private fakeEvent({message}: IState) {
|
||||||
// Fake it till we make it
|
// Fake it till we make it
|
||||||
/* eslint-disable quote-props */
|
/* eslint-disable quote-props */
|
||||||
|
@ -147,12 +121,10 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||||
const className = classnames(this.props.className, {
|
const className = classnames(this.props.className, {
|
||||||
"mx_IRCLayout": this.props.layout == Layout.IRC,
|
"mx_IRCLayout": this.props.layout == Layout.IRC,
|
||||||
"mx_GroupLayout": this.props.layout == Layout.Group,
|
"mx_GroupLayout": this.props.layout == Layout.Group,
|
||||||
"mx_EventTilePreview_faded": this.state.faded,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={className} onClick={this.props.onClick}>
|
return <div className={className}>
|
||||||
<EventTile
|
<EventTile
|
||||||
key={this.state.eventTileKey}
|
|
||||||
mxEvent={event}
|
mxEvent={event}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes, ReactNode, useContext } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { sortBy } from "lodash";
|
import { sortBy } from "lodash";
|
||||||
|
@ -24,6 +24,7 @@ import { _t } from "../../../languageHandler";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
import { useRoomMembers } from "../../../hooks/useRoomMembers";
|
import { useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const DEFAULT_NUM_FACES = 5;
|
const DEFAULT_NUM_FACES = 5;
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||||
|
|
||||||
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
|
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
let members = useRoomMembers(room);
|
let members = useRoomMembers(room);
|
||||||
|
|
||||||
// sort users with an explicit avatar first
|
// sort users with an explicit avatar first
|
||||||
|
@ -46,21 +48,42 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, .
|
||||||
// sort known users first
|
// sort known users first
|
||||||
iteratees.unshift(member => isKnownMember(member));
|
iteratees.unshift(member => isKnownMember(member));
|
||||||
}
|
}
|
||||||
if (members.length < 1) return null;
|
|
||||||
|
|
||||||
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
|
// exclude ourselves from the shown members list
|
||||||
return <div {...props} className="mx_FacePile">
|
const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown);
|
||||||
<div className="mx_FacePile_faces">
|
if (shownMembers.length < 1) return null;
|
||||||
{ shownMembers.map(member => {
|
|
||||||
return <TextWithTooltip key={member.userId} tooltip={member.name}>
|
// We reverse the order of the shown faces in CSS to simplify their visual overlap,
|
||||||
<MemberAvatar member={member} width={28} height={28} />
|
// reverse members in tooltip order to make the order between the two match up.
|
||||||
</TextWithTooltip>;
|
const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", ");
|
||||||
}) }
|
|
||||||
|
let tooltip: ReactNode;
|
||||||
|
if (props.onClick) {
|
||||||
|
tooltip = <div>
|
||||||
|
<div className="mx_Tooltip_title">
|
||||||
|
{ _t("View all %(count)s members", { count: members.length }) }
|
||||||
</div>
|
</div>
|
||||||
{ onlyKnownUsers && <span>
|
<div className="mx_Tooltip_sub">
|
||||||
|
{ _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", {
|
||||||
|
count: members.length,
|
||||||
|
commaSeparatedMembers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div {...props} className="mx_FacePile">
|
||||||
|
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||||
|
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
|
||||||
|
{ shownMembers.map(m =>
|
||||||
|
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> )}
|
||||||
|
</TextWithTooltip>
|
||||||
|
{ onlyKnownUsers && <span className="mx_FacePile_summary">
|
||||||
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
||||||
</span> }
|
</span> }
|
||||||
</div>
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FacePile;
|
export default FacePile;
|
||||||
|
|
|
@ -262,7 +262,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||||
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
|
||||||
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
|
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
|
||||||
label={tooltipContent || this.state.feedback}
|
label={tooltipContent || this.state.feedback}
|
||||||
forceOnRight
|
alignment={Tooltip.Alignment.Right}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
446
src/components/views/elements/ImageView.tsx
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
/*
|
||||||
|
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";
|
||||||
|
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
||||||
|
|
||||||
|
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 = 0.5;
|
||||||
|
// 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 {deltaY} = normalizeWheelEvent(ev);
|
||||||
|
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
|
||||||
|
|
||||||
|
if (newZoom <= MIN_ZOOM) {
|
||||||
|
this.setState({
|
||||||
|
zoom: MIN_ZOOM,
|
||||||
|
translationX: 0,
|
||||||
|
translationY: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newZoom >= MAX_ZOOM) {
|
||||||
|
this.setState({zoom: MAX_ZOOM});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
zoom: newZoom,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRotateCounterClockwiseClick = () => {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Don't do anything if we pressed any
|
||||||
|
// other button than the left one
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip, {Alignment} from './Tooltip';
|
||||||
import { _t } from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface ITooltipProps {
|
interface ITooltipProps {
|
||||||
|
@ -61,7 +61,7 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
|
||||||
className="mx_InfoTooltip_container"
|
className="mx_InfoTooltip_container"
|
||||||
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
forceOnRight={true}
|
alignment={Alignment.Right}
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
||||||
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
|
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
|
||||||
|
|
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>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,6 +139,8 @@ export default class PersistedElement extends React.Component {
|
||||||
_onAction(payload) {
|
_onAction(payload) {
|
||||||
if (payload.action === 'timeline_resize') {
|
if (payload.action === 'timeline_resize') {
|
||||||
this._repositionChild();
|
this._repositionChild();
|
||||||
|
} else if (payload.action === 'logout') {
|
||||||
|
PersistedElement.destroyElement(this.props.persistKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default class TextWithTooltip extends React.Component {
|
||||||
class: PropTypes.string,
|
class: PropTypes.string,
|
||||||
tooltipClass: PropTypes.string,
|
tooltipClass: PropTypes.string,
|
||||||
tooltip: PropTypes.node.isRequired,
|
tooltip: PropTypes.node.isRequired,
|
||||||
|
tooltipProps: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -46,15 +47,17 @@ export default class TextWithTooltip extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
|
|
||||||
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
|
const {class: className, children, tooltip, tooltipClass, tooltipProps, ...props} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||||
{children}
|
{children}
|
||||||
{this.state.hover && <Tooltip
|
{this.state.hover && <Tooltip
|
||||||
|
{...tooltipProps}
|
||||||
label={tooltip}
|
label={tooltip}
|
||||||
tooltipClassName={tooltipClass}
|
tooltipClassName={tooltipClass}
|
||||||
className={"mx_TextWithTooltip_tooltip"} /> }
|
className={"mx_TextWithTooltip_tooltip"}
|
||||||
|
/> }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,14 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const MIN_TOOLTIP_HEIGHT = 25;
|
const MIN_TOOLTIP_HEIGHT = 25;
|
||||||
|
|
||||||
|
export enum Alignment {
|
||||||
|
Natural, // Pick left or right
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top, // Centered
|
||||||
|
Bottom, // Centered
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Class applied to the element used to position the tooltip
|
// Class applied to the element used to position the tooltip
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -36,7 +44,7 @@ interface IProps {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
// the react element to put into the tooltip
|
// the react element to put into the tooltip
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
forceOnRight?: boolean;
|
alignment?: Alignment; // defaults to Natural
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,10 +54,14 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
private tooltip: void | Element | Component<Element, any, any>;
|
private tooltip: void | Element | Component<Element, any, any>;
|
||||||
private parent: Element;
|
private parent: Element;
|
||||||
|
|
||||||
|
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
|
||||||
|
// so we expose the Alignment options off of us statically.
|
||||||
|
public static readonly Alignment = Alignment;
|
||||||
|
|
||||||
public static readonly defaultProps = {
|
public static readonly defaultProps = {
|
||||||
visible: true,
|
visible: true,
|
||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
|
alignment: Alignment.Natural,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||||
|
@ -86,11 +98,35 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset;
|
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
|
||||||
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
|
const top = baseTop + offset;
|
||||||
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
const right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
||||||
} else {
|
const left = parentBox.right + window.pageXOffset + 6;
|
||||||
style.left = parentBox.right + window.pageXOffset + 6;
|
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
|
||||||
|
switch (this.props.alignment) {
|
||||||
|
case Alignment.Natural:
|
||||||
|
if (parentBox.right > window.innerWidth / 2) {
|
||||||
|
style.right = right;
|
||||||
|
style.top = top;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// fall through to Right
|
||||||
|
case Alignment.Right:
|
||||||
|
style.left = left;
|
||||||
|
style.top = top;
|
||||||
|
break;
|
||||||
|
case Alignment.Left:
|
||||||
|
style.right = right;
|
||||||
|
style.top = top;
|
||||||
|
break;
|
||||||
|
case Alignment.Top:
|
||||||
|
style.top = baseTop - 16;
|
||||||
|
style.left = horizontalCenter;
|
||||||
|
break;
|
||||||
|
case Alignment.Bottom:
|
||||||
|
style.top = baseTop + parentBox.height;
|
||||||
|
style.left = horizontalCenter;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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,12 @@ 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";
|
||||||
|
import {RecordingState} from "../../../voice/VoiceRecording";
|
||||||
|
import Tooltip, {Alignment} from "../elements/Tooltip";
|
||||||
|
|
||||||
function ComposerAvatar(props) {
|
function ComposerAvatar(props) {
|
||||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||||
|
@ -178,17 +179,15 @@ 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,
|
||||||
|
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,14 +203,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 +229,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 +317,18 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceUpdate = (haveRecording: boolean) => {
|
_onVoiceStoreUpdate = () => {
|
||||||
this.setState({haveRecording});
|
const recording = VoiceRecordingStore.instance.activeRecording;
|
||||||
|
this.setState({haveRecording: !!recording});
|
||||||
|
if (recording) {
|
||||||
|
// We show a little heads up that the recording is about to automatically end soon. The 3s
|
||||||
|
// display time is completely arbitrary. Note that we don't need to deregister the listener
|
||||||
|
// because the recording instance will clean that up for us.
|
||||||
|
recording.on(RecordingState.EndingSoon, ({secondsLeft}) => {
|
||||||
|
this.setState({recordingTimeLeftSeconds: secondsLeft});
|
||||||
|
setTimeout(() => this.setState({recordingTimeLeftSeconds: null}), 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -352,7 +352,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 +372,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) {
|
||||||
|
@ -411,8 +409,18 @@ export default class MessageComposer extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let recordingTooltip;
|
||||||
|
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
|
||||||
|
if (secondsLeft) {
|
||||||
|
recordingTooltip = <Tooltip
|
||||||
|
label={_t("%(seconds)ss left", {seconds: secondsLeft})}
|
||||||
|
alignment={Alignment.Top} yOffset={-50}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer mx_GroupLayout">
|
<div className="mx_MessageComposer mx_GroupLayout">
|
||||||
|
{recordingTooltip}
|
||||||
<div className="mx_MessageComposer_wrapper">
|
<div className="mx_MessageComposer_wrapper">
|
||||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||||
<div className="mx_MessageComposer_row">
|
<div className="mx_MessageComposer_row">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,12 +289,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
// shallow-copy from the template as we need to make modifications to it
|
// shallow-copy from the template as we need to make modifications to it
|
||||||
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
||||||
this.updateDmAddRoomAction();
|
this.updateDmAddRoomAction();
|
||||||
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||||
|
@ -502,25 +501,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSublists(): React.ReactElement[] {
|
private renderSublists(): React.ReactElement[] {
|
||||||
const components: React.ReactElement[] = [];
|
|
||||||
|
|
||||||
const tagOrder = TAG_ORDER.reduce((p, c) => {
|
|
||||||
if (c === CUSTOM_TAGS_BEFORE_TAG) {
|
|
||||||
const customTags = Object.keys(this.state.sublists)
|
|
||||||
.filter(t => isCustomTag(t));
|
|
||||||
p.push(...customTags);
|
|
||||||
}
|
|
||||||
p.push(c);
|
|
||||||
return p;
|
|
||||||
}, [] as TagID[]);
|
|
||||||
|
|
||||||
// show a skeleton UI if the user is in no rooms and they are not filtering
|
// show a skeleton UI if the user is in no rooms and they are not filtering
|
||||||
const showSkeleton = !this.state.isNameFiltering &&
|
const showSkeleton = !this.state.isNameFiltering &&
|
||||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||||
|
|
||||||
for (const orderedTagId of tagOrder) {
|
return TAG_ORDER.reduce((tags, tagId) => {
|
||||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
if (tagId === CUSTOM_TAGS_BEFORE_TAG) {
|
||||||
|
const customTags = Object.keys(this.state.sublists)
|
||||||
|
.filter(tagId => isCustomTag(tagId));
|
||||||
|
tags.push(...customTags);
|
||||||
|
}
|
||||||
|
tags.push(tagId);
|
||||||
|
return tags;
|
||||||
|
}, [] as TagID[])
|
||||||
|
.map(orderedTagId => {
|
||||||
let extraTiles = null;
|
let extraTiles = null;
|
||||||
if (orderedTagId === DefaultTagID.Invite) {
|
if (orderedTagId === DefaultTagID.Invite) {
|
||||||
extraTiles = this.renderCommunityInvites();
|
extraTiles = this.renderCommunityInvites();
|
||||||
|
@ -528,17 +522,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
extraTiles = this.renderSuggestedRooms();
|
extraTiles = this.renderSuggestedRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
|
||||||
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
|
||||||
continue; // skip tag - not needed
|
|
||||||
}
|
|
||||||
|
|
||||||
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
||||||
? customTagAesthetics(orderedTagId)
|
? customTagAesthetics(orderedTagId)
|
||||||
: this.tagAesthetics[orderedTagId];
|
: this.tagAesthetics[orderedTagId];
|
||||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||||
|
|
||||||
components.push(<RoomSublist
|
// The cost of mounting/unmounting this component offsets the cost
|
||||||
|
// of keeping it in the DOM and hiding it when it is not required
|
||||||
|
return <RoomSublist
|
||||||
key={`sublist-${orderedTagId}`}
|
key={`sublist-${orderedTagId}`}
|
||||||
tagId={orderedTagId}
|
tagId={orderedTagId}
|
||||||
forRooms={true}
|
forRooms={true}
|
||||||
|
@ -551,13 +542,15 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
showSkeleton={showSkeleton}
|
showSkeleton={showSkeleton}
|
||||||
extraTiles={extraTiles}
|
extraTiles={extraTiles}
|
||||||
/>);
|
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||||
}
|
/>
|
||||||
|
});
|
||||||
return components;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
let explorePrompt: JSX.Element;
|
let explorePrompt: JSX.Element;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized) {
|
||||||
if (this.state.isNameFiltering) {
|
if (this.state.isNameFiltering) {
|
||||||
|
@ -578,21 +571,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.props.activeSpace) {
|
} else if (
|
||||||
|
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
|
||||||
|
) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{ _t("Quick actions") }</div>
|
<div>{ _t("Quick actions") }</div>
|
||||||
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
{ this.props.activeSpace.canInvite(userId) && <AccessibleButton
|
||||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||||
onClick={this.onSpaceInviteClick}
|
onClick={this.onSpaceInviteClick}
|
||||||
>
|
>
|
||||||
{_t("Invite people")}
|
{_t("Invite people")}
|
||||||
</AccessibleButton> }
|
</AccessibleButton> }
|
||||||
<AccessibleButton
|
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
|
||||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||||
onClick={this.onExplore}
|
onClick={this.onExplore}
|
||||||
>
|
>
|
||||||
{_t("Explore rooms")}
|
{_t("Explore rooms")}
|
||||||
</AccessibleButton>
|
</AccessibleButton> }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -25,10 +23,10 @@ import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
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",
|
||||||
|
@ -303,7 +301,6 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const EventTilePreview = sdk.getComponent('elements.EventTilePreview');
|
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let title;
|
let title;
|
||||||
|
@ -497,24 +494,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
|
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
|
||||||
if (reason) {
|
if (reason) {
|
||||||
this.reasonElement = React.createRef();
|
reasonElement = <InviteReason reason={reason} />;
|
||||||
// We hide the reason for invitation by default, since it can be a
|
|
||||||
// vector for spam/harassment.
|
|
||||||
const showReason = () => {
|
|
||||||
this.reasonElement.current.unfade();
|
|
||||||
this.reasonElement.current.changeMessage(reason);
|
|
||||||
};
|
|
||||||
reasonElement = <EventTilePreview
|
|
||||||
ref={this.reasonElement}
|
|
||||||
onClick={showReason}
|
|
||||||
className="mx_RoomPreviewBar_reason"
|
|
||||||
message={_t("Invite messages are hidden by default. Click to show the message.")}
|
|
||||||
layout={SettingsStore.getValue("layout")}
|
|
||||||
userId={inviteMember.userId}
|
|
||||||
displayName={inviteMember.rawDisplayName}
|
|
||||||
avatarUrl={inviteMember.events.member.event.content.avatar_url}
|
|
||||||
faded={true}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
|
|
|
@ -74,6 +74,7 @@ interface IProps {
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
|
alwaysVisible?: boolean;
|
||||||
|
|
||||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||||
|
|
||||||
|
@ -125,8 +126,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||||
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateInitialHeight() {
|
private calculateInitialHeight() {
|
||||||
|
@ -242,6 +241,11 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
@ -759,6 +763,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
'mx_RoomSublist': true,
|
'mx_RoomSublist': true,
|
||||||
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
||||||
'mx_RoomSublist_minimized': this.props.isMinimized,
|
'mx_RoomSublist_minimized': this.props.isMinimized,
|
||||||
|
'mx_RoomSublist_hidden': !this.state.rooms.length && this.props.alwaysVisible !== true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
|
|
@ -97,22 +97,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
// generatePreview() will return nothing if the user has previews disabled
|
// generatePreview() will return nothing if the user has previews disabled
|
||||||
messagePreview: this.generatePreview(),
|
messagePreview: this.generatePreview(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
|
||||||
MessagePreviewStore.instance.on(
|
|
||||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
|
||||||
this.onRoomPreviewChanged,
|
|
||||||
);
|
|
||||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
|
||||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
|
||||||
CommunityPrototypeStore.instance.on(
|
|
||||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
|
||||||
this.onCommunityUpdate,
|
|
||||||
);
|
|
||||||
this.props.room.on("Room.name", this.onRoomNameUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomNameUpdate = (room) => {
|
private onRoomNameUpdate = (room) => {
|
||||||
|
@ -167,6 +153,20 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
if (this.state.selected) {
|
if (this.state.selected) {
|
||||||
this.scrollIntoView();
|
this.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
MessagePreviewStore.instance.on(
|
||||||
|
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||||
|
this.onRoomPreviewChanged,
|
||||||
|
);
|
||||||
|
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
|
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
|
this.roomProps.on("Room.name", this.onRoomNameUpdate);
|
||||||
|
CommunityPrototypeStore.instance.on(
|
||||||
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
|
this.onCommunityUpdate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -182,8 +182,15 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||||
}
|
}
|
||||||
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
|
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
|
this.roomProps.off("Room.name", this.onRoomNameUpdate);
|
||||||
|
CommunityPrototypeStore.instance.off(
|
||||||
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
|
this.onCommunityUpdate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
@ -547,7 +554,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
let badge: React.ReactNode;
|
let badge: React.ReactNode;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized && this.notificationState) {
|
||||||
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||||
badge = (
|
badge = (
|
||||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||||
|
@ -563,7 +570,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,17 +53,45 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
await this.state.recorder.stop();
|
await this.state.recorder.stop();
|
||||||
const mxc = await this.state.recorder.upload();
|
const mxc = await this.state.recorder.upload();
|
||||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||||
body: "Voice message",
|
"body": "Voice message",
|
||||||
msgtype: "org.matrix.msc2516.voice",
|
"msgtype": "org.matrix.msc2516.voice",
|
||||||
|
//"msgtype": MsgType.Audio,
|
||||||
|
"url": mxc,
|
||||||
|
"info": {
|
||||||
|
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||||
|
mimetype: this.state.recorder.contentType,
|
||||||
|
size: this.state.recorder.contentLength,
|
||||||
|
},
|
||||||
|
|
||||||
|
// MSC1767 experiment
|
||||||
|
"org.matrix.msc1767.text": "Voice message",
|
||||||
|
"org.matrix.msc1767.file": {
|
||||||
url: mxc,
|
url: mxc,
|
||||||
|
name: "Voice message.ogg",
|
||||||
|
mimetype: this.state.recorder.contentType,
|
||||||
|
size: this.state.recorder.contentLength,
|
||||||
|
},
|
||||||
|
"org.matrix.msc1767.audio": {
|
||||||
|
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||||
|
// TODO: @@ TravisR: Waveform? (MSC1767 decision)
|
||||||
|
},
|
||||||
|
"org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment
|
||||||
|
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||||
|
|
||||||
|
// Events can't have floats, so we try to maintain resolution by using 1024
|
||||||
|
// as a maximum value. The waveform contains values between zero and 1, so this
|
||||||
|
// should come out largely sane.
|
||||||
|
//
|
||||||
|
// We're expecting about one data point per second of audio.
|
||||||
|
waveform: this.state.recorder.finalWaveform.map(v => Math.round(v * 1024)),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
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});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
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.
|
||||||
|
@ -22,17 +22,19 @@ import * as sdk from "../../../../..";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||||
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
const plEventsToLabels = {
|
const plEventsToLabels = {
|
||||||
// These will be translated for us later.
|
// These will be translated for us later.
|
||||||
"m.room.avatar": _td("Change room avatar"),
|
[EventType.RoomAvatar]: _td("Change room avatar"),
|
||||||
"m.room.name": _td("Change room name"),
|
[EventType.RoomName]: _td("Change room name"),
|
||||||
"m.room.canonical_alias": _td("Change main address for the room"),
|
[EventType.RoomCanonicalAlias]: _td("Change main address for the room"),
|
||||||
"m.room.history_visibility": _td("Change history visibility"),
|
[EventType.RoomHistoryVisibility]: _td("Change history visibility"),
|
||||||
"m.room.power_levels": _td("Change permissions"),
|
[EventType.RoomPowerLevels]: _td("Change permissions"),
|
||||||
"m.room.topic": _td("Change topic"),
|
[EventType.RoomTopic]: _td("Change topic"),
|
||||||
"m.room.tombstone": _td("Upgrade the room"),
|
[EventType.RoomTombstone]: _td("Upgrade the room"),
|
||||||
"m.room.encryption": _td("Enable room encryption"),
|
[EventType.RoomEncryption]: _td("Enable room encryption"),
|
||||||
|
[EventType.RoomServerAcl]: _td("Change server ACLs"),
|
||||||
|
|
||||||
// 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": _td("Modify widgets"),
|
"im.vector.modular.widgets": _td("Modify widgets"),
|
||||||
|
@ -40,14 +42,15 @@ const plEventsToLabels = {
|
||||||
|
|
||||||
const plEventsToShow = {
|
const plEventsToShow = {
|
||||||
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
|
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
|
||||||
"m.room.avatar": {isState: true},
|
[EventType.RoomAvatar]: {isState: true},
|
||||||
"m.room.name": {isState: true},
|
[EventType.RoomName]: {isState: true},
|
||||||
"m.room.canonical_alias": {isState: true},
|
[EventType.RoomCanonicalAlias]: {isState: true},
|
||||||
"m.room.history_visibility": {isState: true},
|
[EventType.RoomHistoryVisibility]: {isState: true},
|
||||||
"m.room.power_levels": {isState: true},
|
[EventType.RoomPowerLevels]: {isState: true},
|
||||||
"m.room.topic": {isState: true},
|
[EventType.RoomTopic]: {isState: true},
|
||||||
"m.room.tombstone": {isState: true},
|
[EventType.RoomTombstone]: {isState: true},
|
||||||
"m.room.encryption": {isState: true},
|
[EventType.RoomEncryption]: {isState: true},
|
||||||
|
[EventType.RoomServerAcl]: {isState: true},
|
||||||
|
|
||||||
// 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": {isState: true},
|
"im.vector.modular.widgets": {isState: true},
|
||||||
|
|
|
@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||||
import {SpaceItem} from "./SpaceTreeLevel";
|
import {SpaceItem} from "./SpaceTreeLevel";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
import SpaceStore, {
|
||||||
|
HOME_SPACE,
|
||||||
|
UPDATE_INVITED_SPACES,
|
||||||
|
UPDATE_SELECTED_SPACE,
|
||||||
|
UPDATE_TOP_LEVEL_SPACES,
|
||||||
|
} from "../../../stores/SpaceStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||||
import NotificationBadge from "../rooms/NotificationBadge";
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
@ -105,19 +110,21 @@ const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
</li>;
|
</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSpaces = (): [Room[], Room | null] => {
|
const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||||
|
const [invites, setInvites] = useState<Room[]>(SpaceStore.instance.invitedSpaces);
|
||||||
|
useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
|
||||||
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||||
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||||
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||||
return [spaces, activeSpace];
|
return [invites, spaces, activeSpace];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
// We don't need the handle as we position the menu in a constant location
|
// We don't need the handle as we position the menu in a constant location
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||||
const [spaces, activeSpace] = useSpaces();
|
const [invites, spaces, activeSpace] = useSpaces();
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
|
||||||
const newClasses = classNames("mx_SpaceButton_new", {
|
const newClasses = classNames("mx_SpaceButton_new", {
|
||||||
|
@ -209,6 +216,13 @@ const SpacePanel = () => {
|
||||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
|
{ invites.map(s => <SpaceItem
|
||||||
|
key={s.roomId}
|
||||||
|
space={s}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
onExpand={() => setPanelCollapsed(false)}
|
||||||
|
/>) }
|
||||||
{ spaces.map(s => <SpaceItem
|
{ spaces.map(s => <SpaceItem
|
||||||
key={s.roomId}
|
key={s.roomId}
|
||||||
space={s}
|
space={s}
|
||||||
|
|