Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into pr/only-member-warning

This commit is contained in:
Michael Telatynski 2021-04-19 12:01:32 +01:00
commit c953b1b6bb
153 changed files with 4650 additions and 1535 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,57 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_InviteReason {
position: relative;
margin-bottom: 1em;
.mx_InviteReason_reason {
visibility: visible;
}
.mx_InviteReason_view {
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
justify-content: center;
align-items: center;
cursor: pointer;
color: $secondary-fg-color;
&::before {
content: "";
margin-right: 8px;
background-color: $secondary-fg-color;
mask-image: url('$(res)/img/feather-customised/eye.svg');
display: inline-block;
width: 18px;
height: 14px;
}
}
}
.mx_InviteReason_hidden {
.mx_InviteReason_reason {
visibility: hidden;
}
.mx_InviteReason_view {
display: flex;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,46 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CallViewForRoom {
overflow: hidden;
.mx_CallViewForRoom_ResizeWrapper {
display: flex;
margin-bottom: 8px;
&:hover .mx_CallViewForRoom_ResizeHandle {
// Need to use important to override element style attributes
// set by re-resizable
width: 100% !important;
display: flex;
justify-content: center;
&::after {
content: '';
margin-top: 3px;
border-radius: 4px;
height: 4px;
width: 100%;
max-width: 64px;
background-color: $primary-fg-color;
}
}
}
}

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

Before

Width:  |  Height:  |  Size: 1 KiB

View file

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

After

Width:  |  Height:  |  Size: 717 B

View file

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

After

Width:  |  Height:  |  Size: 542 B

View file

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

After

Width:  |  Height:  |  Size: 609 B

View file

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

After

Width:  |  Height:  |  Size: 764 B

View file

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

After

Width:  |  Height:  |  Size: 752 B

View file

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

After

Width:  |  Height:  |  Size: 733 B

View file

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

After

Width:  |  Height:  |  Size: 596 B

View file

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

Before

Width:  |  Height:  |  Size: 311 B

View file

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

Before

Width:  |  Height:  |  Size: 315 B

View file

@ -85,6 +85,7 @@ $dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: $header-panel-bg-color;
$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;
}

View file

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

View file

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

View file

@ -15,8 +15,8 @@ $inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-266
font-weight: 400;
font-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 */

View file

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

View file

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

View file

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

View file

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

View file

@ -97,7 +97,7 @@ export function formatFullDateNoTime(date: Date): string {
});
}
export function formatFullDate(date: Date, showTwelveHour = false): string {
export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string {
const days = getDaysArray();
const 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),
});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -436,6 +436,8 @@ export default class Registration extends React.Component<IProps, IState> {
// ok fine, there's still no session: really go to the login page
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>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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