Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into pr/only-member-warning
|
@ -1,7 +1,7 @@
|
|||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
src/Markdown.js
|
||||
src/Velociraptor.js
|
||||
src/NodeAnimator.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/views/rooms/MemberList.js
|
||||
src/ratelimitedfunc.js
|
||||
|
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
"stylelint-scss",
|
||||
],
|
||||
"rules": {
|
||||
"color-hex-case": null,
|
||||
"indentation": 4,
|
||||
"comment-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)
|
||||
=====================================================================================================
|
||||
[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
|
||||
|
||||
matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where 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
|
||||
user data, so messages and secrets are not at risk. Thanks to @keerok for
|
||||
responsibly disclosing this via Matrix's Security Disclosure Policy.
|
||||
matrix-react-sdk 3.15.0 fixes a moderate severity issue (CVE-2021-21320) where
|
||||
the user content sandbox can be abused to trick users into opening unexpected
|
||||
documents after several user interactions. The content can be opened with a
|
||||
`blob` origin from the Matrix client, so it is possible for a malicious document
|
||||
to access user messages and secrets. Thanks to @keerok for responsibly
|
||||
disclosing this via Matrix's Security Disclosure Policy.
|
||||
|
||||
## All changes
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ It's so complicated it needs its own README.
|
|||
|
||||
Legend:
|
||||
* Orange = External event.
|
||||
* Purple = Deterministic flow.
|
||||
* Purple = Deterministic flow.
|
||||
* Green = Algorithm definition.
|
||||
* Red = Exit condition/point.
|
||||
* Blue = Process definition.
|
||||
|
@ -24,8 +24,8 @@ algorithm to call, instead of having all the logic in the room list store itself
|
|||
|
||||
|
||||
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
|
||||
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
|
||||
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
|
||||
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
|
||||
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
|
||||
Each category then gets sorted by the appropriate tag sorting algorithm.
|
||||
|
||||
### Tag sorting algorithm: Alphabetical
|
||||
|
@ -36,7 +36,7 @@ useful.
|
|||
|
||||
### Tag sorting algorithm: Manual
|
||||
|
||||
Manual sorting makes use of the `order` property present on all tags for a room, per the
|
||||
Manual sorting makes use of the `order` property present on all tags for a room, per the
|
||||
[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values
|
||||
of `order` cause rooms to appear closer to the top of the list.
|
||||
|
||||
|
@ -74,7 +74,7 @@ relative (perceived) importance to the user:
|
|||
set to 'All Messages'.
|
||||
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
||||
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||
last read it.
|
||||
|
||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
||||
|
@ -82,7 +82,7 @@ above bold, etc.
|
|||
|
||||
Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
|
||||
gets applied to each category in a sub-list fashion. This should result in the red rooms (for example)
|
||||
being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
|
||||
being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
|
||||
collectively the tag will be sorted into categories with red being at the top.
|
||||
|
||||
## Sticky rooms
|
||||
|
@ -103,48 +103,62 @@ receive another notification which causes the room to move into the topmost posi
|
|||
above the sticky room will move underneath to allow for the new room to take the top slot, maintaining
|
||||
the sticky room's position.
|
||||
|
||||
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
|
||||
and thus the user can see a shift in what kinds of rooms move around their selection. An example would
|
||||
be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having
|
||||
the rooms above it read on another device. This would result in 1 red room and 1 other kind of room
|
||||
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
|
||||
and thus the user can see a shift in what kinds of rooms move around their selection. An example would
|
||||
be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having
|
||||
the rooms above it read on another device. This would result in 1 red room and 1 other kind of room
|
||||
above the sticky room as it will try to maintain 2 rooms above the sticky room.
|
||||
|
||||
An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
|
||||
exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
|
||||
the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed.
|
||||
The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
|
||||
The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
|
||||
put the sticky room in a position where it's had to decrease N will not increase N.
|
||||
|
||||
## Responsibilities of the store
|
||||
|
||||
The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
|
||||
an object containing the tags it needs to worry about and the rooms within. The room list component will
|
||||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||
The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
|
||||
an object containing the tags it needs to worry about and the rooms within. The room list component will
|
||||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||
all kinds of filtering.
|
||||
|
||||
## Filtering
|
||||
|
||||
Filters are provided to the store as condition classes, which are then passed along to the algorithm
|
||||
implementations. The implementations then get to decide how to actually filter the rooms, however in
|
||||
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
|
||||
Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.
|
||||
|
||||
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
|
||||
as the old room list store does. When a filter condition changes, it emits an update which (in this
|
||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
||||
Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is
|
||||
due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of
|
||||
rooms to the user. The algorithm implementations will not see a room being prefiltered out.
|
||||
|
||||
Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These
|
||||
filters are passed along to the algorithm implementations where those implementations decide how and
|
||||
when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for
|
||||
optimization reasons.
|
||||
|
||||
The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of
|
||||
rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this
|
||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
||||
minor subset where possible to avoid over-iterating rooms.
|
||||
|
||||
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
|
||||
expect a change in the condition unless the condition says it has changed. This is intentional to
|
||||
maintain the caching behaviour described above.
|
||||
|
||||
One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight
|
||||
subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where
|
||||
room notifications are self-contained within that workspace. Runtime filters tend to not want to affect
|
||||
visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as
|
||||
they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead,
|
||||
the notification counts would vary while the user was typing and "found 2/12" UX would not be possible.
|
||||
|
||||
## Class breakdowns
|
||||
|
||||
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
|
||||
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible
|
||||
for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get
|
||||
defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the
|
||||
user). Various list-specific utilities are also included, though they are expected to move somewhere
|
||||
more general when needed. For example, the `membership` utilities could easily be moved elsewhere
|
||||
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
|
||||
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible
|
||||
for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get
|
||||
defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the
|
||||
user). Various list-specific utilities are also included, though they are expected to move somewhere
|
||||
more general when needed. For example, the `membership` utilities could easily be moved elsewhere
|
||||
as needed.
|
||||
|
||||
The various bits throughout the room list store should also have jsdoc of some kind to help describe
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.17.0",
|
||||
"version": "3.18.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -102,7 +102,6 @@
|
|||
"tar-js": "^0.3.0",
|
||||
"text-encoding-utf-8": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"velocity-animate": "^2.0.6",
|
||||
"what-input": "^5.2.10",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
|
|
|
@ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e
|
|||
|
||||
:root {
|
||||
font-size: 10px;
|
||||
|
||||
--transition-short: .1s;
|
||||
--transition-standard: .3s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
:root {
|
||||
--transition-short: 0;
|
||||
--transition-standard: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -303,7 +313,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
}
|
||||
|
||||
.mx_Dialog_lightbox .mx_Dialog_background {
|
||||
opacity: 0.85;
|
||||
opacity: $lightbox-background-bg-opacity;
|
||||
background-color: $lightbox-background-bg-color;
|
||||
}
|
||||
|
||||
|
@ -315,6 +325,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
pointer-events: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_Dialog_header {
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
@import "./views/elements/_ImageView.scss";
|
||||
@import "./views/elements/_InfoTooltip.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_InviteReason.scss";
|
||||
@import "./views/elements/_ManageIntegsButton.scss";
|
||||
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||
@import "./views/elements/_PowerSelector.scss";
|
||||
|
@ -250,6 +251,7 @@
|
|||
@import "./views/voice_messages/_Waveform.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
@import "./views/voip/_DialPad.scss";
|
||||
@import "./views/voip/_DialPadContextMenu.scss";
|
||||
@import "./views/voip/_DialPadModal.scss";
|
||||
|
|
|
@ -262,12 +262,6 @@ hr.mx_RoomView_myReadMarker {
|
|||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.mx_RoomView_inCall .mx_RoomView_statusAreaBox {
|
||||
background-color: $accent-color;
|
||||
color: $accent-fg-color;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_RoomView_voipChevron {
|
||||
position: absolute;
|
||||
bottom: -11px;
|
||||
|
|
|
@ -21,6 +21,5 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,15 +276,17 @@ $activeBorderColor: $secondary-fg-color;
|
|||
.mx_SpaceButton:hover,
|
||||
.mx_SpaceButton:focus-within,
|
||||
.mx_SpaceButton_hasMenuOpen {
|
||||
// Hide the badge container on hover because it'll be a menu button
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
&:not(.mx_SpaceButton_home) {
|
||||
// Hide the badge container on hover because it'll be a menu button
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_SpaceButton_menuButton {
|
||||
display: block;
|
||||
.mx_SpaceButton_menuButton {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,32 @@ limitations under the License.
|
|||
.mx_UserMenu_headerButtons {
|
||||
// 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 {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018, 2019, 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.
|
||||
|
@ -15,6 +14,27 @@ See the License for the specific language governing permissions and
|
|||
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 {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
|
@ -26,6 +46,13 @@ limitations under the License.
|
|||
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 {
|
||||
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
|
||||
}
|
||||
|
|
|
@ -14,139 +14,108 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This has got to be the most fragile piece of CSS ever written.
|
||||
But empirically it works on Chrome/FF/Safari
|
||||
*/
|
||||
|
||||
.mx_ImageView {
|
||||
display: flex;
|
||||
width: 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;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
min-height: 100%;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.mx_ImageView_image_wrapper {
|
||||
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;
|
||||
}
|
||||
|
||||
.mx_ImageView_cancel {
|
||||
position: absolute;
|
||||
// hack for mx_Dialog having a top padding of 40px
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
padding-top: 35px;
|
||||
padding-right: 35px;
|
||||
cursor: pointer;
|
||||
.mx_ImageView_info {
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_ImageView_rotateClockwise {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 70px;
|
||||
padding-top: 35px;
|
||||
cursor: pointer;
|
||||
.mx_ImageView_info_sender {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mx_ImageView_rotateCounterClockwise {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 105px;
|
||||
padding-top: 35px;
|
||||
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_toolbar {
|
||||
padding-right: 16px;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_ImageView_button {
|
||||
font-size: $font-15px;
|
||||
opacity: 0.5;
|
||||
margin-top: 18px;
|
||||
cursor: pointer;
|
||||
margin-left: 24px;
|
||||
display: block;
|
||||
|
||||
&::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 {
|
||||
height: 30px;
|
||||
.mx_ImageView_button_rotateCW::before {
|
||||
mask-image: url('$(res)/img/image-view/rotate-cw.svg');
|
||||
}
|
||||
|
||||
.mx_ImageView_rhs {
|
||||
order: 3;
|
||||
flex: 1 1 10%;
|
||||
min-width: 300px;
|
||||
// background-color: #800;
|
||||
// height: 20px;
|
||||
.mx_ImageView_button_rotateCCW::before {
|
||||
mask-image: url('$(res)/img/image-view/rotate-ccw.svg');
|
||||
}
|
||||
|
||||
.mx_ImageView_button_zoomOut::before {
|
||||
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 {
|
||||
// Ignore all user input to avoid accidentally triggering the composer
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -159,6 +159,7 @@ $left-gutter: 64px;
|
|||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
|
||||
.mx_IRCLayout .mx_EventTile_last > 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.focus-visible:focus-within > a > .mx_MessageTimestamp {
|
||||
visibility: visible;
|
||||
|
@ -282,6 +283,10 @@ $left-gutter: 64px;
|
|||
display: inline-block;
|
||||
height: $font-14px;
|
||||
width: $font-14px;
|
||||
|
||||
transition:
|
||||
left var(--transition-short) ease-out,
|
||||
top var(--transition-standard) ease-out;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -53,7 +53,8 @@ limitations under the License.
|
|||
font-size: $font-14px;
|
||||
|
||||
&::before {
|
||||
// TODO: @@ TravisR: Animate
|
||||
animation: recording-pulse 2s infinite;
|
||||
|
||||
content: '';
|
||||
background-color: $voice-record-live-circle-color;
|
||||
width: 10px;
|
||||
|
@ -74,3 +75,26 @@ limitations under the License.
|
|||
width: 42px; // we're not using a monospace font, so fake it
|
||||
}
|
||||
}
|
||||
|
||||
// The keyframes are slightly weird here to help make a ramping/punch effect
|
||||
// for the recording dot. We start and end at 100% opacity to help make the
|
||||
// dot feel a bit like a real lamp that is blinking: the animation ends up
|
||||
// spending a lot of its time showing a steady state without a fade effect.
|
||||
// This lamp effect extends into why the 0% opacity keyframe is not in the
|
||||
// midpoint: lamps take longer to turn off than they do to turn on, and the
|
||||
// extra frames give it a bit of a realistic punch for when the animation is
|
||||
// ramping back up to 100% opacity.
|
||||
//
|
||||
// Target animation timings: steady for 1.5s, fade out for 0.3s, fade in for 0.2s
|
||||
// (intended to be used in a loop for 2s animation speed)
|
||||
@keyframes recording-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
35% {
|
||||
opacity: 0;
|
||||
}
|
||||
65% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,12 @@ limitations under the License.
|
|||
.mx_CallView_large {
|
||||
padding-bottom: 10px;
|
||||
margin: 5px 5px 5px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.mx_CallView_voice {
|
||||
height: 360px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +58,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
.mx_CallView_holdTransferContent {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
@ -82,7 +85,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_hold {
|
||||
.mx_CallView_voice .mx_CallView_holdTransferContent {
|
||||
// This masks the avatar image so when it's blurred, the edge is still crisp
|
||||
.mx_CallView_voice_avatarContainer {
|
||||
border-radius: 2000px;
|
||||
|
@ -91,7 +94,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
.mx_CallView_holdTransferContent {
|
||||
height: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 15px;
|
||||
|
@ -104,6 +107,7 @@ limitations under the License.
|
|||
|
||||
.mx_CallView_video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
border-radius: 8px;
|
||||
|
@ -142,7 +146,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video_holdContent {
|
||||
.mx_CallView_video .mx_CallView_holdTransferContent {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
@ -177,6 +181,7 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mx_CallView_header_callType {
|
||||
|
|
46
res/css/views/voip/_CallViewForRoom.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallViewForRoom {
|
||||
overflow: hidden;
|
||||
|
||||
.mx_CallViewForRoom_ResizeWrapper {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover .mx_CallViewForRoom_ResizeHandle {
|
||||
// Need to use important to override element style attributes
|
||||
// set by re-resizable
|
||||
width: 100% !important;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
margin-top: 3px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
max-width: 64px;
|
||||
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
.mx_VideoFeed_remote {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 1</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#ffffff" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1 KiB |
3
res/img/image-view/close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4684 2.04056C11.8589 1.65003 11.8589 1.01687 11.4684 0.626342C11.0779 0.235818 10.4447 0.235818 10.0542 0.626342L6.04718 4.63333L1.81137 0.397522C1.42084 0.00699783 0.78768 0.00699781 0.397156 0.397522C0.0066314 0.788046 0.00663096 1.42121 0.397155 1.81174L4.63297 6.04755L0.62608 10.0544C0.235557 10.445 0.235556 11.0781 0.626081 11.4686C1.0166 11.8592 1.64977 11.8592 2.04029 11.4686L6.04718 7.46176L9.82525 11.2398C10.2158 11.6303 10.8489 11.6303 11.2395 11.2398C11.63 10.8493 11.63 10.2161 11.2395 9.82561L7.46139 6.04755L11.4684 2.04056Z" fill="#A9B2BC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 717 B |
3
res/img/image-view/download.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1C9 0.447715 8.55229 0 8 0C7.44772 0 7 0.447715 7 1L7 12.5858L2.20711 7.79289C1.81658 7.40237 1.18342 7.40237 0.792893 7.79289C0.402369 8.18342 0.402369 8.81658 0.792893 9.20711L7.29289 15.7071C7.68342 16.0976 8.31658 16.0976 8.70711 15.7071L15.2071 9.20711C15.5976 8.81658 15.5976 8.18342 15.2071 7.79289C14.8166 7.40237 14.1834 7.40237 13.7929 7.79289L9 12.5858L9 1Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 542 B |
3
res/img/image-view/more.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 12C6.66699 13.1046 5.77156 14 4.66699 14C3.56242 14 2.66699 13.1046 2.66699 12C2.66699 10.8954 3.56242 10 4.66699 10C5.77156 10 6.66699 10.8954 6.66699 12ZM14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12ZM19.333 14C20.4376 14 21.333 13.1046 21.333 12C21.333 10.8954 20.4376 10 19.333 10C18.2284 10 17.333 10.8954 17.333 12C17.333 13.1046 18.2284 14 19.333 14Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 609 B |
3
res/img/image-view/rotate-ccw.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0.25C14.2811 0.25 16.3824 1.03493 18.0435 2.34854L18.0645 2.36549C19.294 3.38165 21.75 6.28172 21.75 10C21.75 15.3848 17.3848 19.75 12 19.75C11.3096 19.75 10.75 19.1904 10.75 18.5C10.75 17.8096 11.3096 17.25 12 17.25C16.0041 17.25 19.25 14.0041 19.25 10C19.25 7.32797 17.4103 5.07339 16.4819 4.30089C15.2482 3.32907 13.6934 2.75 12 2.75C8.33522 2.75 5.30553 5.46916 4.8184 9H6.50851C6.9004 9 7.13415 9.43723 6.91677 9.76366L3.90826 14.2813C3.71404 14.5729 3.28596 14.5729 3.09174 14.2813L0.083231 9.76366C-0.134151 9.43723 0.0995979 9 0.491489 9H2.30066C2.80139 4.085 6.95284 0.25 12 0.25Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 764 B |
3
res/img/image-view/rotate-cw.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0.25C7.71886 0.25 5.61758 1.03493 3.95646 2.34854L3.93549 2.36549C2.70597 3.38165 0.25 6.28172 0.25 10C0.25 15.3848 4.61522 19.75 10 19.75C10.6904 19.75 11.25 19.1904 11.25 18.5C11.25 17.8096 10.6904 17.25 10 17.25C5.99594 17.25 2.75 14.0041 2.75 10C2.75 7.32797 4.58973 5.07339 5.51806 4.30089C6.7518 3.32907 8.30655 2.75 10 2.75C13.6648 2.75 16.6945 5.46916 17.1816 9H15.4915C15.0996 9 14.8658 9.43723 15.0832 9.76366L18.0917 14.2813C18.286 14.5729 18.714 14.5729 18.9083 14.2813L21.9168 9.76366C22.1342 9.43723 21.9004 9 21.5085 9H19.6993C19.1986 4.085 15.0472 0.25 10 0.25Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 752 B |
3
res/img/image-view/zoom-in.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3293 13.5616C16.379 12.1476 17 10.3963 17 8.5C17 3.80558 13.1944 0 8.5 0C3.80558 0 0 3.80558 0 8.5C0 13.1944 3.80558 17 8.5 17C10.3963 17 12.1476 16.379 13.5616 15.3293L18.1161 19.8839C18.6043 20.372 19.3957 20.372 19.8839 19.8839C20.372 19.3957 20.372 18.6043 19.8839 18.1161L15.3293 13.5616ZM9.5 4C9.5 3.44772 9.05228 3 8.5 3C7.94772 3 7.5 3.44772 7.5 4V7.5H4C3.44772 7.5 3 7.94772 3 8.5C3 9.05228 3.44772 9.5 4 9.5H7.5V13C7.5 13.5523 7.94771 14 8.5 14C9.05228 14 9.5 13.5523 9.5 13V9.5H13C13.5523 9.5 14 9.05228 14 8.5C14 7.94772 13.5523 7.5 13 7.5H9.5V4Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 733 B |
3
res/img/image-view/zoom-out.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3293 13.5616C16.379 12.1476 17 10.3963 17 8.5C17 3.80558 13.1944 0 8.5 0C3.80558 0 0 3.80558 0 8.5C0 13.1944 3.80558 17 8.5 17C10.3963 17 12.1476 16.379 13.5616 15.3293L18.1161 19.8839C18.6043 20.372 19.3957 20.372 19.8839 19.8839C20.372 19.3957 20.372 18.6043 19.8839 18.1161L15.3293 13.5616ZM3 8.5C3 7.94772 3.44772 7.5 4 7.5H13C13.5523 7.5 14 7.94772 14 8.5C14 9.05229 13.5523 9.5 13 9.5H4C3.44772 9.5 3 9.05229 3 8.5Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 596 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
|
Before Width: | Height: | Size: 311 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-cw"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
Before Width: | Height: | Size: 315 B |
|
@ -85,6 +85,7 @@ $dialog-close-fg-color: #9fa9ba;
|
|||
|
||||
$dialog-background-bg-color: $header-panel-bg-color;
|
||||
$lightbox-background-bg-color: #000;
|
||||
$lightbox-background-bg-opacity: 85%;
|
||||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #21262c;
|
||||
|
@ -242,7 +243,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
|||
@define-mixin mx_DialogButton_secondary {
|
||||
// flip colours for the secondary ones
|
||||
font-weight: 600;
|
||||
border: 1px solid $accent-color ! important;
|
||||
border: 1px solid $accent-color !important;
|
||||
color: $accent-color;
|
||||
background-color: $button-secondary-bg-color;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ $dialog-close-fg-color: #9fa9ba;
|
|||
|
||||
$dialog-background-bg-color: $header-panel-bg-color;
|
||||
$lightbox-background-bg-color: #000;
|
||||
$lightbox-background-bg-opacity: 85%;
|
||||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||
|
|
|
@ -127,6 +127,7 @@ $dialog-close-fg-color: #c1c1c1;
|
|||
|
||||
$dialog-background-bg-color: #e9e9e9;
|
||||
$lightbox-background-bg-color: #000;
|
||||
$lightbox-background-bg-opacity: 95%;
|
||||
|
||||
$imagebody-giflabel: rgba(0, 0, 0, 0.7);
|
||||
$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;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$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-fg-color: $muted-fg-color;
|
||||
$voice-record-live-circle-color: $warning-color;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -15,8 +15,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
|
|||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff");
|
||||
}
|
||||
|
||||
@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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff");
|
||||
}
|
||||
|
||||
@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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff");
|
||||
}
|
||||
|
||||
@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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
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-display: swap;
|
||||
unicode-range: $inter-unicode-range;
|
||||
src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.13") format("woff");
|
||||
src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"),
|
||||
url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff");
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
|
|
|
@ -118,6 +118,7 @@ $dialog-close-fg-color: #c1c1c1;
|
|||
|
||||
$dialog-background-bg-color: #e9e9e9;
|
||||
$lightbox-background-bg-color: #000;
|
||||
$lightbox-background-bg-opacity: 95%;
|
||||
|
||||
$imagebody-giflabel: rgba(0, 0, 0, 0.7);
|
||||
$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;
|
||||
|
||||
$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-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-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -22,15 +22,26 @@ clone() {
|
|||
}
|
||||
|
||||
# Try the PR author's branch in case it exists on the deps as well.
|
||||
# If BUILDKITE_BRANCH is set, it will contain either:
|
||||
# First we check if BUILDKITE_BRANCH is defined,
|
||||
# if it isn't we can assume this is a Netlify build
|
||||
if [ -z ${BUILDKITE_BRANCH+x} ]; then
|
||||
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
|
||||
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
|
||||
apiEndpoint+=$REVIEW_ID
|
||||
head=$(curl $apiEndpoint | jq -r '.head.label')
|
||||
else
|
||||
head=$BUILDKITE_BRANCH
|
||||
fi
|
||||
|
||||
# If head is set, it will contain either:
|
||||
# * "branch" when the author's branch and target branch are in the same repo
|
||||
# * "author:branch" when the author's branch is in their fork
|
||||
# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build
|
||||
# We can split on `:` into an array to check.
|
||||
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
|
||||
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "1" ]]; then
|
||||
BRANCH_ARRAY=(${head//:/ })
|
||||
if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then
|
||||
clone $deforg $defrepo $BUILDKITE_BRANCH
|
||||
elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
|
||||
clone ${BUILDKITE_BRANCH_ARRAY[0]} $defrepo ${BUILDKITE_BRANCH_ARRAY[1]}
|
||||
elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then
|
||||
clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]}
|
||||
fi
|
||||
# Try the target branch of the push or PR.
|
||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||
|
|
4
src/@types/global.d.ts
vendored
|
@ -39,7 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
|||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import VoipUserMapper from "../VoipUserMapper";
|
||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||
import {VoiceRecorder} from "../voice/VoiceRecorder";
|
||||
import {VoiceRecording} from "../voice/VoiceRecording";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -71,7 +71,7 @@ declare global {
|
|||
mxModalWidgetStore: ModalWidgetStore;
|
||||
mxVoipUserMapper: VoipUserMapper;
|
||||
mxSpaceStore: SpaceStoreClass;
|
||||
mxVoiceRecorder: typeof VoiceRecorder;
|
||||
mxVoiceRecorder: typeof VoiceRecording;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
|
|
@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
|
|||
|
||||
export default class CallHandler {
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||
// call with a different party to this one.
|
||||
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
private dispatcherRef: string = null;
|
||||
private supportsPstnProtocol = null;
|
||||
|
@ -325,6 +328,10 @@ export default class CallHandler {
|
|||
return callsNotInThatRoom;
|
||||
}
|
||||
|
||||
getTransfereeForCallId(callId: string): MatrixCall {
|
||||
return this.transferees[callId];
|
||||
}
|
||||
|
||||
play(audioId: AudioID) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
|
@ -622,6 +629,7 @@ export default class CallHandler {
|
|||
private async placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
transferee: MatrixCall,
|
||||
) {
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||
|
@ -634,6 +642,9 @@ export default class CallHandler {
|
|||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
if (transferee) {
|
||||
this.transferees[call.callId] = transferee;
|
||||
}
|
||||
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
@ -723,7 +734,10 @@ export default class CallHandler {
|
|||
} else if (members.length === 2) {
|
||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||
|
||||
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
|
||||
this.placeCall(
|
||||
payload.room_id, payload.type, payload.local_element, payload.remote_element,
|
||||
payload.transferee,
|
||||
);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
|
|
|
@ -97,7 +97,7 @@ export function formatFullDateNoTime(date: Date): string {
|
|||
});
|
||||
}
|
||||
|
||||
export function formatFullDate(date: Date, showTwelveHour = false): string {
|
||||
export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
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()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
time: formatFullTime(date, showTwelveHour),
|
||||
time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -360,11 +360,11 @@ const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
|||
action: NavigationAction.GoToHome,
|
||||
keyCombo: {
|
||||
key: Key.H,
|
||||
ctrlOrCmd: true,
|
||||
altKey: true,
|
||||
ctrlKey: true,
|
||||
altKey: !isMac,
|
||||
shiftKey: isMac,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
action: NavigationAction.SelectPrevRoom,
|
||||
keyCombo: {
|
||||
|
|
|
@ -36,6 +36,7 @@ export interface IModal<T extends any[]> {
|
|||
onBeforeClose?(reason?: string): Promise<boolean>;
|
||||
onFinished(...args: T): void;
|
||||
close(...args: T): void;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface IHandle<T extends any[]> {
|
||||
|
@ -93,6 +94,12 @@ export class ModalManager {
|
|||
return container;
|
||||
}
|
||||
|
||||
public toggleCurrentDialogVisibility() {
|
||||
const modal = this.getCurrentModal();
|
||||
if (!modal) return;
|
||||
modal.hidden = !modal.hidden;
|
||||
}
|
||||
|
||||
public hasDialogs() {
|
||||
return this.priorityModal || this.staticModal || this.modals.length > 0;
|
||||
}
|
||||
|
@ -364,7 +371,7 @@ export class ModalManager {
|
|||
}
|
||||
|
||||
const modal = this.getCurrentModal();
|
||||
if (modal !== this.staticModal) {
|
||||
if (modal !== this.staticModal && !modal.hidden) {
|
||||
const classes = classNames("mx_Dialog_wrapper", modal.className, {
|
||||
mx_Dialog_wrapperWithStaticUnder: this.staticModal,
|
||||
});
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
import Velocity from "velocity-animate";
|
||||
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
|
||||
* 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
|
||||
* 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 = {
|
||||
// either a list of child nodes, or a single child.
|
||||
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
|
||||
startStyles: PropTypes.array,
|
||||
|
||||
// a list of transition options from the corresponding startStyle
|
||||
enterTransitionOpts: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
startStyles: [],
|
||||
enterTransitionOpts: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -41,6 +36,18 @@ export default class Velociraptor extends React.Component {
|
|||
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) {
|
||||
const oldChildren = this.children || {};
|
||||
this.children = {};
|
||||
|
@ -50,17 +57,8 @@ export default class Velociraptor extends React.Component {
|
|||
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
||||
|
||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
||||
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => {
|
||||
// special case visibility because it's nonsensical to animate an invisible element
|
||||
// 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;
|
||||
this._applyStyles(oldNode, { left: c.props.style.left });
|
||||
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
// clone the old element with the props (and children) of the new element
|
||||
// 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
|
||||
) {
|
||||
const startStyles = this.props.startStyles;
|
||||
const transitionOpts = this.props.enterTransitionOpts;
|
||||
const domNode = ReactDom.findDOMNode(node);
|
||||
// start from startStyle 1: 0 is the one we gave it
|
||||
// to start with, so now we animate 1 etc.
|
||||
for (var i = 1; i < startStyles.length; ++i) {
|
||||
Velocity(domNode, startStyles[i], transitionOpts[i-1]);
|
||||
/*
|
||||
console.log("start:",
|
||||
JSON.stringify(transitionOpts[i-1]),
|
||||
"->",
|
||||
JSON.stringify(startStyles[i]),
|
||||
);
|
||||
*/
|
||||
for (let i = 1; i < startStyles.length; ++i) {
|
||||
this._applyStyles(domNode, startStyles[i]);
|
||||
// console.log("start:"
|
||||
// JSON.stringify(startStyles[i]),
|
||||
// );
|
||||
}
|
||||
|
||||
// and then we animate to the resting state
|
||||
Velocity(domNode, restingStyle,
|
||||
transitionOpts[i-1])
|
||||
.then(() => {
|
||||
// once we've reached the resting state, hide the element if
|
||||
// appropriate
|
||||
domNode.style.visibility = restingStyle.visibility;
|
||||
});
|
||||
setTimeout(() => {
|
||||
this._applyStyles(domNode, restingStyle);
|
||||
}, 0);
|
||||
|
||||
// console.log("enter:",
|
||||
// JSON.stringify(transitionOpts[i-1]),
|
||||
// "->",
|
||||
// JSON.stringify(restingStyle));
|
||||
}
|
||||
this.nodes[k] = node;
|
||||
|
@ -128,9 +115,7 @@ export default class Velociraptor extends React.Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
{ Object.values(this.children) }
|
||||
</span>
|
||||
<>{ Object.values(this.children) }</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -383,6 +383,10 @@ export const Notifier = {
|
|||
// don't bother notifying as user was recently active in this room
|
||||
return;
|
||||
}
|
||||
if (SettingsStore.getValue("doNotDisturb")) {
|
||||
// Don't bother the user if they didn't ask to be bothered
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEnabled()) {
|
||||
this._displayPopupNotification(ev, room);
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
|
||||
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 dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
|
|
|
@ -95,9 +95,10 @@ function textForMemberEvent(ev) {
|
|||
senderName,
|
||||
targetName,
|
||||
}) + ' ' + reason;
|
||||
} else {
|
||||
// sender is not target and made the target leave, if not from invite/ban then this is a kick
|
||||
} else if (prevContent.membership === "join") {
|
||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import Velocity from "velocity-animate";
|
||||
|
||||
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
||||
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
||||
function bounce( p ) {
|
||||
let pow2;
|
||||
let bounce = 4;
|
||||
|
||||
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
||||
// just sets pow2
|
||||
}
|
||||
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
|
||||
}
|
||||
|
||||
Velocity.Easings.easeOutBounce = function(p) {
|
||||
return 1 - bounce(1 - p);
|
||||
};
|
|
@ -265,7 +265,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
|||
description: _td("Toggle this dialog"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
modifiers: [CMD_OR_CTRL, Modifiers.ALT],
|
||||
modifiers: [Modifiers.CONTROL, isMac ? Modifiers.SHIFT : Modifiers.ALT],
|
||||
key: Key.H,
|
||||
}],
|
||||
description: _td("Go to Home View"),
|
||||
|
|
|
@ -23,7 +23,6 @@ interface IOptions<T extends {}> {
|
|||
keys: Array<string | keyof T>;
|
||||
funcs?: Array<(T) => string>;
|
||||
shouldMatchWordsOnly?: boolean;
|
||||
shouldMatchPrefix?: boolean;
|
||||
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
|
||||
fuzzy?: boolean;
|
||||
}
|
||||
|
@ -56,12 +55,6 @@ export default class QueryMatcher<T extends Object> {
|
|||
if (this._options.shouldMatchWordsOnly === undefined) {
|
||||
this._options.shouldMatchWordsOnly = true;
|
||||
}
|
||||
|
||||
// By default, match anywhere in the string being searched. If enabled, only return
|
||||
// matches that are prefixed with the query.
|
||||
if (this._options.shouldMatchPrefix === undefined) {
|
||||
this._options.shouldMatchPrefix = false;
|
||||
}
|
||||
}
|
||||
|
||||
setObjects(objects: T[]) {
|
||||
|
@ -112,7 +105,7 @@ export default class QueryMatcher<T extends Object> {
|
|||
resultKey = resultKey.replace(/[^\w]/g, '');
|
||||
}
|
||||
const index = resultKey.indexOf(query);
|
||||
if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) {
|
||||
if (index !== -1) {
|
||||
matches.push(
|
||||
...candidates.map((candidate) => ({index, ...candidate})),
|
||||
);
|
||||
|
|
|
@ -56,7 +56,6 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
this.matcher = new QueryMatcher([], {
|
||||
keys: ['name'],
|
||||
funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@'
|
||||
shouldMatchPrefix: true,
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -981,7 +981,7 @@ export default class GroupView extends React.Component {
|
|||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
const httpInviterAvatar = this.state.inviterProfile
|
||||
const httpInviterAvatar = this.state.inviterProfile && this.state.inviterProfile.avatarUrl
|
||||
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import {RoomUpdateCause} from "../../stores/room-list/models";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import SecurityCustomisations from "../../customisations/Security";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -395,7 +396,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
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")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
|
|
|
@ -659,6 +659,7 @@ export default class MessagePanel extends React.Component {
|
|||
showReactions={this.props.showReactions}
|
||||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
</li>,
|
||||
|
|
|
@ -1137,10 +1137,16 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this.setState({
|
||||
dragCounter: this.state.dragCounter + 1,
|
||||
draggingFile: true,
|
||||
});
|
||||
// We always increment the counter no matter the types, because dragging is
|
||||
// still happening. If we didn't, the drag counter would get out of sync.
|
||||
this.setState({dragCounter: this.state.dragCounter + 1});
|
||||
|
||||
// See:
|
||||
// https://docs.w3cub.com/dom/datatransfer/types
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
|
||||
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
|
||||
this.setState({draggingFile: true});
|
||||
}
|
||||
};
|
||||
|
||||
private onDragLeave = ev => {
|
||||
|
@ -1164,6 +1170,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
|
||||
// See:
|
||||
// https://docs.w3cub.com/dom/datatransfer/types
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
|
||||
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ interface IState {
|
|||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private dndWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
private tagStoreRef: fbEmitter.EventSubscription;
|
||||
|
||||
|
@ -89,6 +90,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
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 {
|
||||
|
@ -103,6 +107,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
|
||||
public componentWillUnmount() {
|
||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
this.tagStoreRef.remove();
|
||||
|
@ -288,6 +293,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
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 => {
|
||||
if (!this.state.contextMenuPosition) return null;
|
||||
|
||||
|
@ -534,6 +545,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
let dnd;
|
||||
if (this.state.selectedSpace) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
|
@ -560,6 +572,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
);
|
||||
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) {
|
||||
name = null;
|
||||
|
@ -595,6 +617,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</span>
|
||||
{name}
|
||||
{dnd}
|
||||
{buttons}
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
@ -27,7 +27,9 @@ import classNames from 'classnames';
|
|||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
|
||||
// Phases
|
||||
// Show the forgot password inputs
|
||||
|
@ -137,10 +139,14 @@ export default class ForgotPassword extends React.Component {
|
|||
// refresh the server errors, just in case the server came back online
|
||||
await this._checkServerLiveliness(this.props.serverConfig);
|
||||
|
||||
await this['password_field'].validate({ allowEmpty: false });
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
this.showErrorDialog(_t('A new password must be entered.'));
|
||||
} else if (!this.state.passwordFieldValid) {
|
||||
this.showErrorDialog(_t('Please choose a strong password'));
|
||||
} else if (this.state.password !== this.state.password2) {
|
||||
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||
} else {
|
||||
|
@ -186,6 +192,12 @@ export default class ForgotPassword extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onPasswordValidate(result) {
|
||||
this.setState({
|
||||
passwordFieldValid: result.valid,
|
||||
});
|
||||
}
|
||||
|
||||
renderForgot() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
|
@ -230,12 +242,15 @@ export default class ForgotPassword extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
<PassphraseField
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
label={_td('New Password')}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
fieldRef={field => this['password_field'] = field}
|
||||
onValidate={(result) => this.onPasswordValidate(result)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
|
||||
autoComplete="new-password"
|
||||
|
|
|
@ -436,6 +436,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
// ok fine, there's still no session: really go to the login page
|
||||
this.props.onLoginClick();
|
||||
}
|
||||
|
||||
return sessionLoaded;
|
||||
};
|
||||
|
||||
private renderRegisterComponent() {
|
||||
|
@ -557,7 +559,12 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
loggedInUserId: this.state.differentLoggedInUserId,
|
||||
},
|
||||
)}</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")}
|
||||
</AccessibleButton></p>
|
||||
</div>;
|
||||
|
|
|
@ -40,7 +40,7 @@ enum RegistrationField {
|
|||
PasswordConfirm = "field_password_confirm",
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
export const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
interface IProps {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
|
|
|
@ -129,7 +129,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
name: this.props.room.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -52,6 +52,9 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
||||
onCloseDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -141,6 +144,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
|
@ -190,6 +194,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onForwardClick = () => {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
event: this.props.mxEvent,
|
||||
|
|
|
@ -29,7 +29,10 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
IInvite3PID,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
@ -332,6 +335,7 @@ interface IInviteDialogState {
|
|||
threepidResultsMixin: { user: Member, userId: string}[];
|
||||
canUseIdentityServer: boolean;
|
||||
tryingIdentityServer: boolean;
|
||||
consultFirst: boolean;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean,
|
||||
|
@ -380,6 +384,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
threepidResultsMixin: [],
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
consultFirst: false,
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: false,
|
||||
|
@ -395,6 +400,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev) => {
|
||||
this.setState({consultFirst: ev.target.checked});
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
|
@ -610,13 +619,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
_startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const client = MatrixClientPeg.get();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
let existingRoom: Room;
|
||||
if (targetIds.length === 1) {
|
||||
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]);
|
||||
existingRoom = findDMForUser(client, targetIds[0]);
|
||||
} else {
|
||||
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
}
|
||||
|
@ -638,7 +648,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
// If so, enable encryption in the new room.
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
|
@ -648,35 +657,41 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>;
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
}
|
||||
|
||||
// the createRoom call will show the room for us, so we don't need to worry about that.
|
||||
createRoomPromise.then(abort => {
|
||||
if (abort === true) return; // only abort on true booleans, not roomIds or something
|
||||
if (targetIds.length > 1) {
|
||||
createRoomOptions.createOpts = targetIds.reduce(
|
||||
(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();
|
||||
}).catch(err => {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("We couldn't create your DM."),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_inviteUsers = async () => {
|
||||
|
@ -704,8 +719,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.props.onFinished();
|
||||
}
|
||||
|
||||
if (cli.isRoomEncrypted(this.props.roomId) &&
|
||||
SettingsStore.getValue("feature_room_history_key_sharing")) {
|
||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||
const visibilityEvent = room.currentState.getStateEvents(
|
||||
"m.room.history_visibility", "",
|
||||
);
|
||||
|
@ -745,16 +759,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
}
|
||||
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
if (this.state.consultFirst) {
|
||||
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: this.props.call.type,
|
||||
room_id: dmRoomId,
|
||||
transferee: this.props.call,
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: dmRoomId,
|
||||
should_peek: false,
|
||||
joining: false,
|
||||
});
|
||||
this.props.onFinished();
|
||||
} else {
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1215,6 +1247,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let consultSection;
|
||||
let keySharingWarning = <span />;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
@ -1317,8 +1350,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
|
||||
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
|
||||
cli.isRoomEncrypted(this.props.roomId)) {
|
||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
const visibilityEvent = room.currentState.getStateEvents(
|
||||
"m.room.history_visibility", "",
|
||||
|
@ -1339,6 +1371,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
consultSection = <div>
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
{_t("Consult first")}
|
||||
</label>
|
||||
</div>;
|
||||
} else {
|
||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||
}
|
||||
|
@ -1375,6 +1413,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
</div>
|
||||
{consultSection}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
54
src/components/views/dialogs/SeshatResetDialog.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
|
||||
@replaceableComponent("views.dialogs.SeshatResetDialog")
|
||||
export default class SeshatResetDialog extends React.PureComponent<IDialogProps> {
|
||||
render() {
|
||||
return (
|
||||
<BaseDialog
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished.bind(null, false)}
|
||||
title={_t("Reset event store?")}>
|
||||
<div>
|
||||
<p>
|
||||
{_t("You most likely do not want to reset your event index store")}
|
||||
<br />
|
||||
{_t("If you do, please note that none of your messages will be deleted, " +
|
||||
"but the search experience might be degraded for a few moments" +
|
||||
"whilst the index is recreated",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Reset event store")}
|
||||
onPrimaryButtonClick={this.props.onFinished.bind(null, true)}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this.props.onFinished.bind(null, false)}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ import Field from '../../elements/Field';
|
|||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import {_t} from '../../../../languageHandler';
|
||||
import {IDialogProps} from "../IDialogProps";
|
||||
import {accessSecretStorage} from "../../../../SecurityManager";
|
||||
import Modal from "../../../../Modal";
|
||||
|
||||
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
|
||||
// so this should be plenty and allow for people putting extra whitespace in the file because
|
||||
|
@ -47,6 +49,7 @@ interface IState {
|
|||
forceRecoveryKey: boolean;
|
||||
passPhrase: string;
|
||||
keyMatches: boolean | null;
|
||||
resetting: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -66,10 +69,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
forceRecoveryKey: false,
|
||||
passPhrase: '',
|
||||
keyMatches: null,
|
||||
resetting: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onCancel = () => {
|
||||
if (this.state.resetting) {
|
||||
this.setState({resetting: 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 {
|
||||
if (this.state.recoveryKeyFileError) {
|
||||
return _t("Wrong file type");
|
||||
|
@ -216,8 +272,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
}
|
||||
|
||||
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 DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
const hasPassphrase = (
|
||||
this.props.keyInfo &&
|
||||
|
@ -226,11 +283,36 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
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 title;
|
||||
let titleClass;
|
||||
if (hasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
if (this.state.resetting) {
|
||||
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');
|
||||
title = _t("Security Phrase");
|
||||
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
|
||||
|
@ -278,13 +360,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={this.state.passPhrase.length === 0}
|
||||
additive={resetButton}
|
||||
/>
|
||||
</form>
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Security Key");
|
||||
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const feedbackClasses = classNames({
|
||||
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
|
||||
|
@ -339,6 +421,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
additive={resetButton}
|
||||
/>
|
||||
</form>
|
||||
</div>;
|
||||
|
|
|
@ -19,7 +19,6 @@ import classnames from 'classnames';
|
|||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
|
@ -41,15 +40,26 @@ interface IProps {
|
|||
* classnames to apply to the wrapper of the preview
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* The ID of the displayed user
|
||||
*/
|
||||
userId: string;
|
||||
|
||||
/**
|
||||
* The display name of the displayed user
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* The mxc:// avatar URL of the displayed user
|
||||
*/
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IState {
|
||||
userId: string;
|
||||
displayname: string;
|
||||
avatar_url: string;
|
||||
message: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
|
@ -57,45 +67,28 @@ const AVATAR_SIZE = 32;
|
|||
export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
userId: "@erim:fink.fink",
|
||||
displayname: "Erimayas Fink",
|
||||
avatar_url: null,
|
||||
message: props.message,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Fetch current user data
|
||||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
const avatarUrl = profileInfo.avatar_url;
|
||||
|
||||
this.setState({
|
||||
userId,
|
||||
displayname: profileInfo.displayname,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
}
|
||||
|
||||
private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
|
||||
private fakeEvent({message}: IState) {
|
||||
// Fake it till we make it
|
||||
/* eslint-disable quote-props */
|
||||
const rawEvent = {
|
||||
type: "m.room.message",
|
||||
sender: userId,
|
||||
sender: this.props.userId,
|
||||
content: {
|
||||
"m.new_content": {
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
body: message,
|
||||
displayname: this.props.displayName,
|
||||
avatar_url: this.props.avatarUrl,
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
body: message,
|
||||
displayname: this.props.displayName,
|
||||
avatar_url: this.props.avatarUrl,
|
||||
},
|
||||
unsigned: {
|
||||
age: 97,
|
||||
|
@ -108,12 +101,15 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: displayname,
|
||||
userId: userId,
|
||||
name: this.props.displayName,
|
||||
userId: this.props.userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
return Avatar.avatarUrlForUser(
|
||||
{ avatarUrl: this.props.avatarUrl },
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => avatarUrl,
|
||||
getMxcAvatarUrl: () => this.props.avatarUrl,
|
||||
};
|
||||
|
||||
return event;
|
||||
|
|
|
@ -1,235 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import * as sdk from "../../../index";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.ImageView")
|
||||
export default class ImageView extends React.Component {
|
||||
static propTypes = {
|
||||
src: PropTypes.string.isRequired, // the source of the image being displayed
|
||||
name: PropTypes.string, // the main title ('name') for the image
|
||||
link: PropTypes.string, // the link (if any) applied to the name of the image
|
||||
width: PropTypes.number, // width of the image src in pixels
|
||||
height: PropTypes.number, // height of the image src in pixels
|
||||
fileSize: PropTypes.number, // size of the image src in bytes
|
||||
onFinished: PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { rotationDegrees: 0 };
|
||||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
onRedactClick = () => {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.props.onFinished();
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
|
||||
).catch(function(e) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
const code = e.errcode || e.statusCode;
|
||||
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getName() {
|
||||
let name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
rotateCounterClockwise = () => {
|
||||
const cur = this.state.rotationDegrees;
|
||||
const rotationDegrees = (cur - 90) % 360;
|
||||
this.setState({ rotationDegrees });
|
||||
};
|
||||
|
||||
rotateClockwise = () => {
|
||||
const cur = this.state.rotationDegrees;
|
||||
const rotationDegrees = (cur + 90) % 360;
|
||||
this.setState({ rotationDegrees });
|
||||
};
|
||||
|
||||
render() {
|
||||
/*
|
||||
// In theory max-width: 80%, max-height: 80% on the CSS should work
|
||||
// but in practice, it doesn't, so do it manually:
|
||||
|
||||
var width = this.props.width || 500;
|
||||
var height = this.props.height || 500;
|
||||
|
||||
var maxWidth = document.documentElement.clientWidth * 0.8;
|
||||
var maxHeight = document.documentElement.clientHeight * 0.8;
|
||||
|
||||
var widthFrac = width / maxWidth;
|
||||
var heightFrac = height / maxHeight;
|
||||
|
||||
var displayWidth;
|
||||
var displayHeight;
|
||||
if (widthFrac > heightFrac) {
|
||||
displayWidth = Math.min(width, maxWidth);
|
||||
displayHeight = (displayWidth / width) * height;
|
||||
} else {
|
||||
displayHeight = Math.min(height, maxHeight);
|
||||
displayWidth = (displayHeight / height) * width;
|
||||
}
|
||||
|
||||
var style = {
|
||||
width: displayWidth,
|
||||
height: displayHeight
|
||||
};
|
||||
*/
|
||||
let style = {};
|
||||
let res;
|
||||
|
||||
if (this.props.width && this.props.height) {
|
||||
style = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
};
|
||||
res = style.width + "x" + style.height + "px";
|
||||
}
|
||||
|
||||
let size;
|
||||
if (this.props.fileSize) {
|
||||
size = filesize(this.props.fileSize);
|
||||
}
|
||||
|
||||
let sizeRes;
|
||||
if (size && res) {
|
||||
sizeRes = size + ", " + res;
|
||||
} else {
|
||||
sizeRes = size || res;
|
||||
}
|
||||
|
||||
let mayRedact = false;
|
||||
const showEventMeta = !!this.props.mxEvent;
|
||||
|
||||
let eventMeta;
|
||||
if (showEventMeta) {
|
||||
// Figure out the sender, defaulting to mxid
|
||||
let sender = this.props.mxEvent.getSender();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (room) {
|
||||
mayRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
|
||||
const member = room.getMember(sender);
|
||||
if (member) sender = member.name;
|
||||
}
|
||||
|
||||
eventMeta = (<div className="mx_ImageView_metadata">
|
||||
{ _t('Uploaded on %(date)s by %(user)s', {
|
||||
date: formatDate(new Date(this.props.mxEvent.getTs())),
|
||||
user: sender,
|
||||
}) }
|
||||
</div>);
|
||||
}
|
||||
|
||||
let eventRedact;
|
||||
if (mayRedact) {
|
||||
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</div>);
|
||||
}
|
||||
|
||||
const rotationDegrees = this.state.rotationDegrees;
|
||||
const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style};
|
||||
|
||||
return (
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this.onKeyDown,
|
||||
role: "dialog",
|
||||
}}
|
||||
className="mx_ImageView"
|
||||
>
|
||||
<div className="mx_ImageView_lhs">
|
||||
</div>
|
||||
<div className="mx_ImageView_content">
|
||||
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
|
||||
<div className="mx_ImageView_labelWrapper">
|
||||
<div className="mx_ImageView_label">
|
||||
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
|
||||
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
|
||||
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
|
||||
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
|
||||
</AccessibleButton>
|
||||
<div className="mx_ImageView_shim">
|
||||
</div>
|
||||
<div className="mx_ImageView_name">
|
||||
{ this.getName() }
|
||||
</div>
|
||||
{ eventMeta }
|
||||
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
|
||||
<div className="mx_ImageView_download">
|
||||
{ _t('Download this file') }<br />
|
||||
<span className="mx_ImageView_size">{ sizeRes }</span>
|
||||
</div>
|
||||
</a>
|
||||
{ eventRedact }
|
||||
<div className="mx_ImageView_shim">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_ImageView_rhs">
|
||||
</div>
|
||||
</FocusLock>
|
||||
);
|
||||
}
|
||||
}
|
439
src/components/views/elements/ImageView.tsx
Normal file
|
@ -0,0 +1,439 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020, 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {formatFullDate} from "../../../DateUtils";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
const MIN_ZOOM = 100;
|
||||
const MAX_ZOOM = 300;
|
||||
// This is used for the buttons
|
||||
const ZOOM_STEP = 10;
|
||||
// This is used for mouse wheel events
|
||||
const ZOOM_COEFFICIENT = 10;
|
||||
// If we have moved only this much we can zoom
|
||||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
|
||||
interface IProps {
|
||||
src: string, // the source of the image being displayed
|
||||
name?: string, // the main title ('name') for the image
|
||||
link?: string, // the link (if any) applied to the name of the image
|
||||
width?: number, // width of the image src in pixels
|
||||
height?: number, // height of the image src in pixels
|
||||
fileSize?: number, // size of the image src in bytes
|
||||
onFinished(): void, // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
rotation: number,
|
||||
zoom: number,
|
||||
translationX: number,
|
||||
translationY: number,
|
||||
moving: boolean,
|
||||
contextMenuDisplayed: boolean,
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.ImageView")
|
||||
export default class ImageView extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
rotation: 0,
|
||||
zoom: MIN_ZOOM,
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
moving: false,
|
||||
contextMenuDisplayed: false,
|
||||
};
|
||||
}
|
||||
|
||||
// XXX: Refs to functional components
|
||||
private contextMenuButton = createRef<any>();
|
||||
private focusLock = createRef<any>();
|
||||
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private lastX = 0;
|
||||
private lastY = 0;
|
||||
private previousX = 0;
|
||||
private previousY = 0;
|
||||
|
||||
componentDidMount() {
|
||||
// We have to use addEventListener() because the listener
|
||||
// needs to be passive in order to work with Chromium
|
||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
}
|
||||
|
||||
private onKeyDown = (ev: KeyboardEvent) => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
private onWheel = (ev: WheelEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const newZoom = this.state.zoom - (ev.deltaY * ZOOM_COEFFICIENT);
|
||||
|
||||
if (newZoom <= MIN_ZOOM) {
|
||||
this.setState({
|
||||
zoom: MIN_ZOOM,
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newZoom >= MAX_ZOOM) {
|
||||
this.setState({zoom: MAX_ZOOM});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
zoom: newZoom,
|
||||
});
|
||||
};
|
||||
|
||||
private onRotateCounterClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur - 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
};
|
||||
|
||||
private onRotateClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur + 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
};
|
||||
|
||||
private onZoomInClick = () => {
|
||||
if (this.state.zoom >= MAX_ZOOM) {
|
||||
this.setState({zoom: MAX_ZOOM});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
zoom: this.state.zoom + ZOOM_STEP,
|
||||
});
|
||||
};
|
||||
|
||||
private onZoomOutClick = () => {
|
||||
if (this.state.zoom <= MIN_ZOOM) {
|
||||
this.setState({
|
||||
zoom: MIN_ZOOM,
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
zoom: this.state.zoom - ZOOM_STEP,
|
||||
});
|
||||
};
|
||||
|
||||
private onDownloadClick = () => {
|
||||
const a = document.createElement("a");
|
||||
a.href = this.props.src;
|
||||
a.download = this.props.name;
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
};
|
||||
|
||||
private onOpenContextMenu = () => {
|
||||
this.setState({
|
||||
contextMenuDisplayed: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onCloseContextMenu = () => {
|
||||
this.setState({
|
||||
contextMenuDisplayed: false,
|
||||
});
|
||||
};
|
||||
|
||||
private onPermalinkClicked = (ev: React.MouseEvent) => {
|
||||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Element when clicked.
|
||||
ev.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
private onStartMoving = (ev: React.MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
// Zoom in if we are completely zoomed out
|
||||
if (this.state.zoom === MIN_ZOOM) {
|
||||
this.setState({zoom: MAX_ZOOM});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({moving: true});
|
||||
this.previousX = this.state.translationX;
|
||||
this.previousY = this.state.translationY;
|
||||
this.initX = ev.pageX - this.lastX;
|
||||
this.initY = ev.pageY - this.lastY;
|
||||
};
|
||||
|
||||
private onMoving = (ev: React.MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.state.moving) return;
|
||||
|
||||
this.lastX = ev.pageX - this.initX;
|
||||
this.lastY = ev.pageY - this.initY;
|
||||
this.setState({
|
||||
translationX: this.lastX,
|
||||
translationY: this.lastY,
|
||||
});
|
||||
};
|
||||
|
||||
private onEndMoving = () => {
|
||||
// Zoom out if we haven't moved much
|
||||
if (
|
||||
this.state.moving === true &&
|
||||
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
|
||||
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
||||
) {
|
||||
this.setState({
|
||||
zoom: MIN_ZOOM,
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
}
|
||||
this.setState({moving: false});
|
||||
};
|
||||
|
||||
private renderContextMenu() {
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuDisplayed) {
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
|
||||
onFinished={this.onCloseContextMenu}
|
||||
>
|
||||
<MessageContextMenu
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFinished={this.onCloseContextMenu}
|
||||
onCloseDialog={this.props.onFinished}
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ contextMenu }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const showEventMeta = !!this.props.mxEvent;
|
||||
|
||||
let cursor;
|
||||
if (this.state.moving) {
|
||||
cursor= "grabbing";
|
||||
} else if (this.state.zoom === MIN_ZOOM) {
|
||||
cursor = "zoom-in";
|
||||
} else {
|
||||
cursor = "zoom-out";
|
||||
}
|
||||
const rotationDegrees = this.state.rotation + "deg";
|
||||
const zoomPercentage = this.state.zoom/100;
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
const translatePixelsY = this.state.translationY + "px";
|
||||
// The order of the values is important!
|
||||
// First, we translate and only then we rotate, otherwise
|
||||
// we would apply the translation to an already rotated
|
||||
// image causing it translate in the wrong direction.
|
||||
const style = {
|
||||
cursor: cursor,
|
||||
transition: this.state.moving ? null : "transform 200ms ease 0s",
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})
|
||||
scale(${zoomPercentage})
|
||||
rotate(${rotationDegrees})`,
|
||||
};
|
||||
|
||||
let info;
|
||||
if (showEventMeta) {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||
let permalink = "#";
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const sender = (
|
||||
<div className="mx_ImageView_info_sender">
|
||||
{senderName}
|
||||
</div>
|
||||
);
|
||||
const messageTimestamp = (
|
||||
<a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatFullDate(new Date(this.props.mxEvent.getTs()), showTwelveHour, false)}
|
||||
>
|
||||
<MessageTimestamp
|
||||
showFullDate={true}
|
||||
showTwelveHour={showTwelveHour}
|
||||
ts={mxEvent.getTs()}
|
||||
showSeconds={false}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
const avatar = (
|
||||
<MemberAvatar
|
||||
member={mxEvent.sender}
|
||||
width={32} height={32}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
);
|
||||
|
||||
info = (
|
||||
<div className="mx_ImageView_info_wrapper">
|
||||
{avatar}
|
||||
<div className="mx_ImageView_info">
|
||||
{sender}
|
||||
{messageTimestamp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// If there is no event - we're viewing an avatar, we set
|
||||
// an empty div here, since the panel uses space-between
|
||||
// and we want the same placement of elements
|
||||
info = (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
let contextMenuButton;
|
||||
if (this.props.mxEvent) {
|
||||
contextMenuButton = (
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_more"
|
||||
title={_t("Options")}
|
||||
onClick={this.onOpenContextMenu}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.contextMenuDisplayed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this.onKeyDown,
|
||||
role: "dialog",
|
||||
}}
|
||||
className="mx_ImageView"
|
||||
ref={this.focusLock}
|
||||
>
|
||||
<div className="mx_ImageView_panel">
|
||||
{info}
|
||||
<div className="mx_ImageView_toolbar">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("Rotate Left")}
|
||||
onClick={ this.onRotateCounterClockwiseClick }>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||
title={_t("Zoom out")}
|
||||
onClick={ this.onZoomOutClick }>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("Zoom in")}
|
||||
onClick={ this.onZoomInClick }>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={ this.onDownloadClick }>
|
||||
</AccessibleTooltipButton>
|
||||
{contextMenuButton}
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={ this.props.onFinished }>
|
||||
</AccessibleTooltipButton>
|
||||
{this.renderContextMenu()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_ImageView_image_wrapper">
|
||||
<img
|
||||
src={this.props.src}
|
||||
title={this.props.name}
|
||||
style={style}
|
||||
className="mx_ImageView_image"
|
||||
draggable={true}
|
||||
onMouseDown={this.onStartMoving}
|
||||
onMouseMove={this.onMoving}
|
||||
onMouseUp={this.onEndMoving}
|
||||
onMouseLeave={this.onEndMoving}
|
||||
/>
|
||||
</div>
|
||||
</FocusLock>
|
||||
);
|
||||
}
|
||||
}
|
62
src/components/views/elements/InviteReason.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.InviteReason")
|
||||
export default class InviteReason extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
// We hide the reason for invitation by default, since it can be a
|
||||
// vector for spam/harassment.
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
|
||||
onViewClick = () => {
|
||||
this.setState({
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = classNames({
|
||||
"mx_InviteReason": true,
|
||||
"mx_InviteReason_hidden": this.state.hidden,
|
||||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_InviteReason_reason">{this.props.reason}</div>
|
||||
<div className="mx_InviteReason_view"
|
||||
onClick={this.onViewClick}
|
||||
>
|
||||
{_t("View message")}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -41,6 +41,9 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight: PropTypes.number,
|
||||
|
||||
/* the permalinkCreator */
|
||||
permalinkCreator: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -106,6 +109,7 @@ export default class MImageBody extends React.Component {
|
|||
src: httpUrl,
|
||||
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
|
||||
mxEvent: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
};
|
||||
|
||||
if (content.info) {
|
||||
|
@ -114,7 +118,7 @@ export default class MImageBody extends React.Component {
|
|||
params.fileSize = content.info.size;
|
||||
}
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
// enable the play button. Firefox does not seem to care either
|
||||
// way, so it's fine to do for all browsers.
|
||||
decryptedUrl: `data:${content?.info?.mimetype},`,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedThumbnailUrl: thumbnailUrl || `data:${content?.info?.mimetype},`,
|
||||
decryptedBlob: null,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ export default class MessageEvent extends React.Component {
|
|||
|
||||
/* the maximum image height to use, if the event is an image */
|
||||
maxImageHeight: PropTypes.number,
|
||||
|
||||
/* the permalinkCreator */
|
||||
permalinkCreator: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -126,6 +129,7 @@ export default class MessageEvent extends React.Component {
|
|||
editState={this.props.editState}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
onMessageAllowed={this.onTileUpdate}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {formatFullDate, formatTime} from '../../../DateUtils';
|
||||
import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MessageTimestamp")
|
||||
|
@ -25,13 +25,24 @@ export default class MessageTimestamp extends React.Component {
|
|||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
showTwelveHour: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
showSeconds: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
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 (
|
||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
||||
{ formatTime(date, this.props.showTwelveHour) }
|
||||
{timestamp}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
src: httpUrl,
|
||||
name: text,
|
||||
};
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
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 {Room} from 'matrix-js-sdk/src/models/room';
|
||||
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 Modal from '../../../Modal';
|
||||
|
@ -496,11 +497,11 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
|||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
|
||||
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
} else {
|
||||
|
@ -511,7 +512,7 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
|||
};
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.members", update);
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
|
@ -1431,7 +1432,7 @@ const UserInfoHeader: React.FC<{
|
|||
name: member.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
}, [member]);
|
||||
|
||||
const avatarElement = (
|
||||
|
@ -1494,7 +1495,7 @@ const UserInfoHeader: React.FC<{
|
|||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const displayName = member.name || member.displayname;
|
||||
const displayName = member.rawDisplayName || member.displayname;
|
||||
return <React.Fragment>
|
||||
{ avatarElement }
|
||||
|
||||
|
|
|
@ -149,8 +149,8 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
const callView = (
|
||||
<CallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
onResize={this.props.onResize}
|
||||
maxVideoHeight={this.props.maxHeight}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -140,7 +140,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps) {
|
||||
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;
|
||||
if (isEmpty) {
|
||||
this.showPlaceholder();
|
||||
|
@ -670,8 +675,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
});
|
||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||
"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,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
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 * as TextForEvent from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
|
@ -43,39 +42,56 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
import Tooltip from "../elements/Tooltip";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.sticker': 'messages.MessageEvent',
|
||||
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
|
||||
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.call.invite': 'messages.TextualEvent',
|
||||
'm.call.answer': 'messages.TextualEvent',
|
||||
'm.call.hangup': 'messages.TextualEvent',
|
||||
'm.call.reject': 'messages.TextualEvent',
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
[EventType.Sticker]: 'messages.MessageEvent',
|
||||
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
||||
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
||||
[EventType.CallInvite]: 'messages.TextualEvent',
|
||||
[EventType.CallAnswer]: 'messages.TextualEvent',
|
||||
[EventType.CallHangup]: 'messages.TextualEvent',
|
||||
[EventType.CallReject]: 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
||||
'm.room.create': 'messages.RoomCreate',
|
||||
'm.room.member': 'messages.TextualEvent',
|
||||
'm.room.name': 'messages.TextualEvent',
|
||||
'm.room.avatar': 'messages.RoomAvatarEvent',
|
||||
'm.room.third_party_invite': 'messages.TextualEvent',
|
||||
'm.room.history_visibility': 'messages.TextualEvent',
|
||||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
[EventType.RoomEncryption]: 'messages.EncryptionEvent',
|
||||
[EventType.RoomCanonicalAlias]: 'messages.TextualEvent',
|
||||
[EventType.RoomCreate]: 'messages.RoomCreate',
|
||||
[EventType.RoomMember]: 'messages.TextualEvent',
|
||||
[EventType.RoomName]: 'messages.TextualEvent',
|
||||
[EventType.RoomAvatar]: 'messages.RoomAvatarEvent',
|
||||
[EventType.RoomThirdPartyInvite]: 'messages.TextualEvent',
|
||||
[EventType.RoomHistoryVisibility]: 'messages.TextualEvent',
|
||||
[EventType.RoomTopic]: 'messages.TextualEvent',
|
||||
[EventType.RoomPowerLevels]: 'messages.TextualEvent',
|
||||
[EventType.RoomPinnedEvents]: 'messages.TextualEvent',
|
||||
[EventType.RoomServerAcl]: 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
'm.room.related_groups': 'messages.TextualEvent',
|
||||
[EventType.RoomTombstone]: 'messages.TextualEvent',
|
||||
[EventType.RoomJoinRules]: 'messages.TextualEvent',
|
||||
[EventType.RoomGuestAccess]: '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
|
||||
for (const evType of ALL_RULE_TYPES) {
|
||||
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;
|
||||
|
@ -239,6 +260,9 @@ export default class EventTile extends React.Component {
|
|||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
|
||||
// whether or not to show read receipts
|
||||
showReadReceipts: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -837,8 +861,6 @@ export default class EventTile extends React.Component {
|
|||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
const readAvatars = this.getReadAvatars();
|
||||
|
||||
let avatar;
|
||||
let sender;
|
||||
let avatarSize;
|
||||
|
@ -967,6 +989,16 @@ export default class EventTile extends React.Component {
|
|||
const groupPadlock = !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) {
|
||||
case 'notif': {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
@ -1080,14 +1112,13 @@ export default class EventTile extends React.Component {
|
|||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onHeightChanged={this.props.onHeightChanged} />
|
||||
{ keyRequestInfo }
|
||||
{ reactionsRow }
|
||||
{ actionBar }
|
||||
</div>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{msgOption}
|
||||
{
|
||||
// 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
|
||||
|
|
|
@ -96,7 +96,7 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
link: this.props.link,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -29,11 +29,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -178,15 +177,12 @@ export default class MessageComposer extends React.Component {
|
|||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
|
||||
VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate);
|
||||
this._dispatcherRef = null;
|
||||
|
||||
this.state = {
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
||||
isComposerEmpty: true,
|
||||
haveRecording: false,
|
||||
};
|
||||
|
@ -204,14 +200,6 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onWidgetUpdate = () => {
|
||||
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
|
||||
};
|
||||
|
||||
_onActiveWidgetUpdate = () => {
|
||||
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
|
@ -238,8 +226,7 @@ export default class MessageComposer extends React.Component {
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
|
||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
|
@ -327,8 +314,8 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onVoiceUpdate = (haveRecording: boolean) => {
|
||||
this.setState({haveRecording});
|
||||
_onVoiceStoreUpdate = () => {
|
||||
this.setState({haveRecording: !!VoiceRecordingStore.instance.activeRecording});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -352,7 +339,6 @@ export default class MessageComposer extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
// TODO: @@ TravisR - Disabling the composer doesn't work
|
||||
disabled={this.state.haveRecording}
|
||||
/>,
|
||||
);
|
||||
|
@ -373,8 +359,7 @@ export default class MessageComposer extends React.Component {
|
|||
if (SettingsStore.getValue("feature_voice_messages")) {
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
room={this.props.room}
|
||||
onRecording={this.onVoiceUpdate} />);
|
||||
room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
|
|
|
@ -17,22 +17,13 @@ limitations under the License.
|
|||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import '../../../VelocityBounce';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import Velociraptor from "../../../Velociraptor";
|
||||
import NodeAnimator from "../../../NodeAnimator";
|
||||
import * as sdk from "../../../index";
|
||||
import {toPx} from "../../../utils/units";
|
||||
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")
|
||||
export default class ReadReceiptMarker extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
// we've already done our display - nothing more to do.
|
||||
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
|
||||
let oldTop = -15;
|
||||
|
||||
|
@ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
}
|
||||
|
||||
const startStyles = [];
|
||||
const enterTransitionOpts = [];
|
||||
|
||||
if (oldInfo && oldInfo.left) {
|
||||
// start at the old height and in the old h pos
|
||||
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: toPx(oldInfo.left) });
|
||||
|
||||
const reorderTransitionOpts = {
|
||||
duration: 100,
|
||||
easing: 'easeOut',
|
||||
};
|
||||
|
||||
enterTransitionOpts.push(reorderTransitionOpts);
|
||||
}
|
||||
|
||||
// then shift to the rightmost column,
|
||||
// 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',
|
||||
});
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||
|
||||
this.setState({
|
||||
suppressDisplay: false,
|
||||
startStyles: startStyles,
|
||||
enterTransitionOpts: enterTransitionOpts,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
const style = {
|
||||
left: toPx(this.props.leftOffset),
|
||||
top: '0px',
|
||||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
||||
};
|
||||
|
||||
let title;
|
||||
|
@ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<Velociraptor
|
||||
startStyles={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<NodeAnimator
|
||||
startStyles={this.state.startStyles} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
|
@ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
onClick={this.props.onClick}
|
||||
inputRef={this._avatar}
|
||||
/>
|
||||
</Velociraptor>
|
||||
</NodeAnimator>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -27,7 +25,8 @@ import SdkConfig from "../../../SdkConfig";
|
|||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
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({
|
||||
NotLoggedIn: "NotLoggedIn",
|
||||
|
@ -306,6 +305,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
let showSpinner = false;
|
||||
let title;
|
||||
let subTitle;
|
||||
let reasonElement;
|
||||
let primaryActionHandler;
|
||||
let primaryActionLabel;
|
||||
let secondaryActionHandler;
|
||||
|
@ -491,6 +491,12 @@ export default class RoomPreviewBar extends React.Component {
|
|||
primaryActionLabel = _t("Accept");
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
|
||||
if (reason) {
|
||||
reasonElement = <InviteReason reason={reason} />;
|
||||
}
|
||||
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Reject");
|
||||
secondaryActionHandler = this.props.onRejectClick;
|
||||
|
@ -582,6 +588,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
{ titleElement }
|
||||
{ subTitleElements }
|
||||
</div>
|
||||
{ reasonElement }
|
||||
<div className="mx_RoomPreviewBar_actions">
|
||||
{ secondaryButton }
|
||||
{ extraComponents }
|
||||
|
|
|
@ -563,7 +563,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
let messagePreview = null;
|
||||
if (this.showMessagePreview && this.state.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}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -477,6 +477,10 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
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) {
|
||||
case 'reply_to_event':
|
||||
case Action.FocusComposer:
|
||||
|
|
|
@ -17,21 +17,21 @@ limitations under the License.
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import React from "react";
|
||||
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
|
||||
import {VoiceRecording} from "../../../voice/VoiceRecording";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
onRecording: (haveRecording: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
recorder?: VoiceRecorder;
|
||||
recorder?: VoiceRecording;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,13 +57,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
msgtype: "org.matrix.msc2516.voice",
|
||||
url: mxc,
|
||||
});
|
||||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
this.setState({recorder: null});
|
||||
this.props.onRecording(false);
|
||||
return;
|
||||
}
|
||||
const recorder = new VoiceRecorder(MatrixClientPeg.get());
|
||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||
await recorder.start();
|
||||
this.props.onRecording(true);
|
||||
this.setState({recorder});
|
||||
};
|
||||
|
||||
|
|
|
@ -28,13 +28,12 @@ import Modal from "../../../Modal";
|
|||
import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||
|
||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
@replaceableComponent("views.settings.ChangePassword")
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
|
|