Merge remote-tracking branch 'upstream/develop' into ts/address-stuff

This commit is contained in:
Šimon Brandner 2021-07-20 12:31:18 +02:00
commit b1f4ba28d7
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
281 changed files with 2816 additions and 2309 deletions

View file

@ -46,6 +46,7 @@
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style", "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 src test", "lint:js": "eslint --max-warnings 0 src test",
"lint:js-fix": "eslint --fix src test",
"lint:types": "tsc --noEmit --jsx react", "lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'", "lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest", "test": "jest",

View file

@ -201,6 +201,7 @@
@import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EntityTile.scss";
@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_EventBubbleTile.scss";
@import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_JumpToBottomButton.scss";

View file

@ -27,6 +27,7 @@ limitations under the License.
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139 // https://bugzilla.mozilla.org/show_bug.cgi?id=255139
display: inline-block; display: inline-block;
user-select: none; user-select: none;
line-height: 1;
} }
.mx_BaseAvatar_initial { .mx_BaseAvatar_initial {

View file

@ -18,7 +18,6 @@ $timelineImageBorderRadius: 4px;
.mx_MImageBody { .mx_MImageBody {
display: block; display: block;
margin-right: 34px;
} }
.mx_MImageBody_thumbnail { .mx_MImageBody_thumbnail {

View file

@ -26,6 +26,7 @@ limitations under the License.
height: 24px; height: 24px;
vertical-align: middle; vertical-align: middle;
margin-left: 4px; margin-left: 4px;
margin-right: 4px;
&::before { &::before {
content: ''; content: '';

View file

@ -0,0 +1,323 @@
/*
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_EventTile[data-layout=bubble],
.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
--avatarSize: 32px;
--gutterSize: 11px;
--cornerRadius: 12px;
--maxWidth: 70%;
}
.mx_EventTile[data-layout=bubble] {
position: relative;
margin-top: var(--gutterSize);
margin-left: 50px;
margin-right: 100px;
&.mx_EventTile_continuation {
margin-top: 2px;
}
/* For replies */
.mx_EventTile {
padding-top: 0;
}
&:hover {
&::before {
content: '';
position: absolute;
top: -1px;
bottom: -1px;
left: -60px;
right: -60px;
z-index: -1;
background: $eventbubble-bg-hover;
border-radius: 4px;
}
.mx_EventTile_avatar {
img {
box-shadow: 0 0 0 3px $eventbubble-bg-hover;
}
}
}
.mx_SenderProfile,
.mx_EventTile_line {
width: fit-content;
max-width: 70%;
}
.mx_SenderProfile {
position: relative;
top: -2px;
left: 2px;
}
&[data-self=false] {
.mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
}
.mx_EventTile_avatar {
left: -34px;
}
.mx_MessageActionBar {
right: 0;
transform: translate3d(50%, 50%, 0);
}
--backgroundColor: $eventbubble-others-bg;
}
&[data-self=true] {
.mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
float: right;
> a {
left: auto;
right: -48px;
}
}
.mx_SenderProfile {
display: none;
}
.mx_ReactionsRow {
float: right;
clear: right;
display: flex;
/* Moving the "add reaction button" before the reactions */
> :last-child {
order: -1;
}
}
.mx_EventTile_avatar {
top: -19px; // height of the sender block
right: -35px;
}
--backgroundColor: $eventbubble-self-bg;
}
.mx_EventTile_line {
position: relative;
padding: var(--gutterSize);
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
background: var(--backgroundColor);
display: flex;
gap: 5px;
margin: 0 -12px 0 -9px;
> a {
position: absolute;
left: -48px;
}
}
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
border-top-left-radius: 0;
}
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
}
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
border-top-right-radius: 0;
}
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
}
.mx_EventTile_avatar {
position: absolute;
top: 0;
line-height: 1;
img {
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
border-radius: 50%;
}
}
&[data-has-reply=true] {
> .mx_EventTile_line {
flex-direction: column;
}
.mx_ReplyThread_show {
order: 99999;
}
.mx_ReplyThread {
margin: 0 calc(-1 * var(--gutterSize));
.mx_EventTile_reply {
max-width: 90%;
padding: 0;
> a {
display: none !important;
}
}
.mx_EventTile {
display: flex;
gap: var(--gutterSize);
.mx_EventTile_avatar {
position: static;
}
.mx_SenderProfile {
display: none;
}
}
}
}
.mx_EditMessageComposer_buttons {
position: static;
padding: 0;
margin: 0;
background: transparent;
}
.mx_ReactionsRow {
margin-right: -18px;
margin-left: -9px;
}
.mx_ReplyThread {
border-left-width: 2px;
border-left-color: $eventbubble-reply-color;
}
&.mx_EventTile_bubbleContainer,
&.mx_EventTile_info,
& ~ .mx_EventListSummary[data-expanded=false] {
--backgroundColor: transparent;
--gutterSize: 0;
display: flex;
align-items: center;
justify-content: center;
.mx_EventTile_avatar {
position: static;
order: -1;
margin-right: 5px;
}
}
& ~ .mx_EventListSummary {
--maxWidth: 80%;
margin-left: calc(var(--avatarSize) + var(--gutterSize));
margin-right: calc(var(--gutterSize) + var(--avatarSize));
.mx_EventListSummary_toggle {
float: none;
margin: 0;
order: 9;
margin-left: 5px;
}
.mx_EventListSummary_avatars {
padding-top: 0;
}
&::after {
content: "";
clear: both;
}
.mx_EventTile {
margin: 0 6px;
}
.mx_EventTile_line {
margin: 0 5px;
> a {
left: auto;
right: 0;
transform: translateX(calc(100% + 5px));
}
}
.mx_MessageActionBar {
transform: translate3d(50%, 0, 0);
}
}
& ~ .mx_EventListSummary[data-expanded=false] {
padding: 0 34px;
}
/* events that do not require bubble layout */
& ~ .mx_EventListSummary,
&.mx_EventTile_bad {
.mx_EventTile_line {
background: transparent;
}
&:hover {
&::before {
background: transparent;
}
}
}
& + .mx_EventListSummary {
.mx_EventTile {
margin-top: 0;
padding: 0;
}
}
.mx_EventListSummary_toggle {
margin-right: 55px;
}
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
&.mx_EventTile_bad > .mx_EventTile_line {
display: grid;
grid-template:
"reply reply" auto
"shield body" auto
"shield link" auto
/ auto 1fr;
.mx_EventTile_e2eIcon {
grid-area: shield;
}
.mx_UnknownBody {
grid-area: body;
}
.mx_EventTile_keyRequestInfo {
grid-area: link;
}
.mx_ReplyThread_wrapper {
grid-area: reply;
}
}
.mx_EventTile_readAvatars {
position: absolute;
right: -110px;
bottom: 0;
top: auto;
}
.mx_MTextBody {
max-width: 100%;
}
}

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,15 +18,14 @@ limitations under the License.
$left-gutter: 64px; $left-gutter: 64px;
$hover-select-border: 4px; $hover-select-border: 4px;
.mx_EventTile { .mx_EventTile:not([data-layout=bubble]) {
max-width: 100%; max-width: 100%;
clear: both; clear: both;
padding-top: 18px; padding-top: 18px;
font-size: $font-14px; font-size: $font-14px;
position: relative; position: relative;
}
.mx_EventTile.mx_EventTile_info { &.mx_EventTile_info {
padding-top: 1px; padding-top: 1px;
} }
@ -37,12 +36,12 @@ $hover-select-border: 4px;
user-select: none; user-select: none;
} }
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { &.mx_EventTile_info .mx_EventTile_avatar {
top: $font-6px; top: $font-6px;
left: $left-gutter; left: $left-gutter;
} }
.mx_EventTile_continuation { &.mx_EventTile_continuation {
padding-top: 0px !important; padding-top: 0px !important;
&.mx_EventTile_isEditing { &.mx_EventTile_isEditing {
@ -51,11 +50,11 @@ $hover-select-border: 4px;
} }
} }
.mx_EventTile_isEditing { &.mx_EventTile_isEditing {
background-color: $header-panel-bg-color; background-color: $header-panel-bg-color;
} }
.mx_EventTile .mx_SenderProfile { .mx_SenderProfile {
color: $primary-fg-color; color: $primary-fg-color;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */ display: inline-block; /* anti-zalgo, with overflow hidden */
@ -70,7 +69,7 @@ $hover-select-border: 4px;
max-width: calc(100% - $left-gutter); max-width: calc(100% - $left-gutter);
} }
.mx_EventTile .mx_SenderProfile .mx_Flair { .mx_SenderProfile .mx_Flair {
opacity: 0.7; opacity: 0.7;
margin-left: 5px; margin-left: 5px;
display: inline-block; display: inline-block;
@ -85,11 +84,11 @@ $hover-select-border: 4px;
} }
} }
.mx_EventTile_isEditing .mx_MessageTimestamp { &.mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden; visibility: hidden;
} }
.mx_EventTile .mx_MessageTimestamp { .mx_MessageTimestamp {
display: block; display: block;
white-space: nowrap; white-space: nowrap;
left: 0px; left: 0px;
@ -97,7 +96,7 @@ $hover-select-border: 4px;
user-select: none; user-select: none;
} }
.mx_EventTile_continuation .mx_EventTile_line { &.mx_EventTile_continuation .mx_EventTile_line {
clear: both; clear: both;
} }
@ -107,63 +106,25 @@ $hover-select-border: 4px;
border-radius: 8px; border-radius: 8px;
} }
.mx_RoomView_timeline_rr_enabled,
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
.mx_EventListSummary {
.mx_EventTile_line {
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
margin-right: 110px;
}
}
.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;
.mx_EventTile_line {
margin-right: 0;
grid-column: 1 / 3;
// override default padding of mx_EventTile_line so that we can be centered
padding: 0 !important;
}
.mx_EventTile_msgOption {
grid-column: 2;
}
}
.mx_EventTile_reply { .mx_EventTile_reply {
margin-right: 10px; margin-right: 10px;
} }
/* HACK to override line-height which is already marked important elsewhere */ &.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px !important;
line-height: 57px !important;
}
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border); left: calc(-$hover-select-border);
} }
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
/* this is used for the tile for the event which is selected via the URL. /* this is used for the tile for the event which is selected via the URL.
* TODO: ultimately we probably want some transition on here. * TODO: ultimately we probably want some transition on here.
*/ */
.mx_EventTile_selected > .mx_EventTile_line { &.mx_EventTile_selected > .mx_EventTile_line {
border-left: $accent-color 4px solid; border-left: $accent-color 4px solid;
padding-left: calc($left-gutter - $hover-select-border); padding-left: calc($left-gutter - $hover-select-border);
background-color: $event-selected-color; background-color: $event-selected-color;
} }
.mx_EventTile_highlight, &.mx_EventTile_highlight,
.mx_EventTile_highlight .markdown-body { &.mx_EventTile_highlight .markdown-body {
color: $event-highlight-fg-color; color: $event-highlight-fg-color;
.mx_EventTile_line { .mx_EventTile_line {
@ -171,17 +132,17 @@ $hover-select-border: 4px;
} }
} }
.mx_EventTile_info .mx_EventTile_line { &.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px); padding-left: calc($left-gutter + 18px);
} }
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border); padding-left: calc($left-gutter + 18px - $hover-select-border);
} }
.mx_EventTile:hover .mx_EventTile_line, &.mx_EventTile:hover .mx_EventTile_line,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, &.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { &.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
background-color: $event-selected-color; background-color: $event-selected-color;
} }
@ -225,7 +186,7 @@ $hover-select-border: 4px;
mask-image: url('$(res)/img/element-icons/circle-sending.svg'); mask-image: url('$(res)/img/element-icons/circle-sending.svg');
} }
.mx_EventTile_contextual { &.mx_EventTile_contextual {
opacity: 0.4; opacity: 0.4;
} }
@ -247,36 +208,6 @@ $hover-select-border: 4px;
text-decoration: none; text-decoration: none;
} }
.mx_EventTile_readAvatars {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
// This aligns the avatar with the last line of the
// message. We want to move it one line up - 2.2rem
top: -2.2rem;
user-select: none;
z-index: 1;
}
.mx_EventTile_readAvatars .mx_BaseAvatar {
position: absolute;
display: inline-block;
height: $font-14px;
width: $font-14px;
will-change: left, top;
transition:
left var(--transition-short) ease-out,
top var(--transition-standard) ease-out;
}
.mx_EventTile_readAvatarRemainder {
color: $event-timestamp-color;
font-size: $font-11px;
position: absolute;
}
/* all the overflow-y: hidden; are to trap Zalgos - /* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto. but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random so make that explicitly hidden too to avoid random
@ -314,15 +245,147 @@ $hover-select-border: 4px;
filter: none; filter: none;
} }
&:hover.mx_EventTile_verified .mx_EventTile_line,
&:hover.mx_EventTile_unverified .mx_EventTile_line,
&:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: calc($left-gutter - $hover-select-border);
}
&:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
/* End to end encryption stuff */
&:hover .mx_EventTile_e2eIcon {
opacity: 1;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
&:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
&:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
&:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
&:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
display: block;
left: 41px;
}
.mx_MImageBody {
margin-right: 34px;
}
.mx_EventTile_e2eIcon { .mx_EventTile_e2eIcon {
position: absolute; position: absolute;
top: 6px; top: 6px;
left: 44px; left: 44px;
bottom: 0;
right: 0;
}
.mx_ReactionsRow {
margin: 0;
padding: 6px 60px;
}
}
.mx_RoomView_timeline_rr_enabled {
.mx_EventTile:not([data-layout=bubble]) {
.mx_EventTile_line {
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
margin-right: 110px;
}
}
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
}
.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;
.mx_EventTile_line {
margin-right: 0;
grid-column: 1 / 3;
// override default padding of mx_EventTile_line so that we can be centered
padding: 0 !important;
}
.mx_EventTile_msgOption {
grid-column: 2;
}
}
.mx_EventTile_readAvatars {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
// This aligns the avatar with the last line of the
// message. We want to move it one line up - 2.2rem
top: -2.2rem;
user-select: none;
z-index: 1;
}
.mx_EventTile_readAvatars .mx_BaseAvatar {
position: absolute;
display: inline-block;
height: $font-14px;
width: $font-14px;
will-change: left, top;
transition:
left var(--transition-short) ease-out,
top var(--transition-standard) ease-out;
}
.mx_EventTile_readAvatarRemainder {
color: $event-timestamp-color;
font-size: $font-11px;
position: absolute;
}
/* HACK to override line-height which is already marked important elsewhere */
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px !important;
line-height: 57px !important;
}
.mx_EventTile_content .mx_EventTile_edited {
user-select: none;
font-size: $font-12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
cursor: pointer;
}
.mx_EventTile_e2eIcon {
position: relative;
width: 14px; width: 14px;
height: 14px; height: 14px;
display: block; display: block;
bottom: 0;
right: 0;
opacity: 0.2; opacity: 0.2;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
@ -381,87 +444,6 @@ $hover-select-border: 4px;
opacity: 1; opacity: 1;
} }
.mx_EventTile_keyRequestInfo {
font-size: $font-12px;
}
.mx_EventTile_keyRequestInfo_text {
opacity: 0.5;
}
.mx_EventTile_keyRequestInfo_text a {
color: $primary-fg-color;
text-decoration: underline;
cursor: pointer;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p {
text-align: auto;
margin-left: 3px;
margin-right: 3px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
margin-top: 0px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
margin-bottom: 0px;
}
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: calc($left-gutter - $hover-select-border);
}
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
/* End to end encryption stuff */
.mx_EventTile:hover .mx_EventTile_e2eIcon {
opacity: 1;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
display: block;
left: 41px;
}
.mx_EventTile_content .mx_EventTile_edited {
user-select: none;
font-size: $font-12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
cursor: pointer;
}
/* Various markdown overrides */ /* Various markdown overrides */
.mx_EventTile_body pre { .mx_EventTile_body pre {
@ -595,6 +577,35 @@ $hover-select-border: 4px;
/* end of overrides */ /* end of overrides */
.mx_EventTile_keyRequestInfo {
font-size: $font-12px;
}
.mx_EventTile_keyRequestInfo_text {
opacity: 0.5;
}
.mx_EventTile_keyRequestInfo_text a {
color: $primary-fg-color;
text-decoration: underline;
cursor: pointer;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p {
text-align: auto;
margin-left: 3px;
margin-right: 3px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
margin-top: 0px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
margin-bottom: 0px;
}
.mx_EventTile_tileError { .mx_EventTile_tileError {
color: red; color: red;
text-align: center; text-align: center;
@ -615,6 +626,13 @@ $hover-select-border: 4px;
} }
} }
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
padding-left: 0; padding-left: 0;

View file

@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px;
$composer-shadow-color: rgba(0, 0, 0, 0.28); $composer-shadow-color: rgba(0, 0, 0, 0.28);
// Bubble tiles
$eventbubble-self-bg: #143A34;
$eventbubble-others-bg: #394049;
$eventbubble-bg-hover: #433C23;
$eventbubble-avatar-outline: $bg-color;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View file

@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color;
$composer-shadow-color: tranparent; $composer-shadow-color: tranparent;
// Bubble tiles
$eventbubble-self-bg: #F8FDFC;
$eventbubble-others-bg: #F7F8F9;
$eventbubble-bg-hover: rgb(242, 242, 242);
$eventbubble-avatar-outline: #fff;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View file

@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px;
$composer-shadow-color: rgba(0, 0, 0, 0.04); $composer-shadow-color: rgba(0, 0, 0, 0.04);
// Bubble tiles
$eventbubble-self-bg: #F8FDFC;
$eventbubble-others-bg: #F7F8F9;
$eventbubble-bg-hover: #FEFCF5;
$eventbubble-avatar-outline: $primary-bg-color;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View file

@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
/* eslint-disable @typescript-eslint/naming-convention */
declare global { declare global {
interface Window { interface Window {
matrixChat: ReturnType<Renderer>; matrixChat: ReturnType<Renderer>;
@ -186,3 +188,5 @@ declare global {
} }
); );
} }
/* eslint-enable @typescript-eslint/naming-convention */

View file

@ -270,7 +270,7 @@ export class Analytics {
localStorage.removeItem(LAST_VISIT_TS_KEY); localStorage.removeItem(LAST_VISIT_TS_KEY);
} }
private async _track(data: IData) { private async track(data: IData) {
if (this.disabled) return; if (this.disabled) return;
const now = new Date(); const now = new Date();
@ -304,7 +304,7 @@ export class Analytics {
} }
public ping() { public ping() {
this._track({ this.track({
ping: "1", ping: "1",
}); });
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
@ -324,14 +324,14 @@ export class Analytics {
// But continue anyway because we still want to track the change // But continue anyway because we still want to track the change
} }
this._track({ this.track({
gt_ms: String(generationTimeMs), gt_ms: String(generationTimeMs),
}); });
} }
public trackEvent(category: string, action: string, name?: string, value?: string) { public trackEvent(category: string, action: string, name?: string, value?: string) {
if (this.disabled) return; if (this.disabled) return;
this._track({ this.track({
e_c: category, e_c: category,
e_a: action, e_a: action,
e_n: name, e_n: name,

View file

@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
import { isSecureBackupRequired } from './utils/WellKnownUtils'; import { isSecureBackupRequired } from './utils/WellKnownUtils';
import { isLoggedIn } from './components/structures/MatrixChat'; import { isLoggedIn } from './components/structures/MatrixChat';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ActionPayload } from "./dispatcher/payloads";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
@ -58,28 +59,28 @@ export default class DeviceListener {
} }
start() { start() {
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices); MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this._onAccountData); MatrixClientPeg.get().on('accountData', this.onAccountData);
MatrixClientPeg.get().on('sync', this._onSync); MatrixClientPeg.get().on('sync', this.onSync);
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents); MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
this._recheck(); this.recheck();
} }
stop() { stop() {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices); MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync); MatrixClientPeg.get().removeListener('sync', this.onSync);
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents); MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
} }
if (this.dispatcherRef) { if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
@ -103,15 +104,15 @@ export default class DeviceListener {
this.dismissed.add(d); this.dismissed.add(d);
} }
this._recheck(); this.recheck();
} }
dismissEncryptionSetup() { dismissEncryptionSetup() {
this.dismissedThisDeviceToast = true; this.dismissedThisDeviceToast = true;
this._recheck(); this.recheck();
} }
_ensureDeviceIdsAtStartPopulated() { private ensureDeviceIdsAtStartPopulated() {
if (this.ourDeviceIdsAtStart === null) { if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this.ourDeviceIdsAtStart = new Set( this.ourDeviceIdsAtStart = new Set(
@ -120,39 +121,39 @@ export default class DeviceListener {
} }
} }
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
// If we didn't know about *any* devices before (ie. it's fresh login), // If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the // then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch. // devicesAtStart list to the devices that we see after the fetch.
if (initialFetch) return; if (initialFetch) return;
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated(); if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated();
// No need to do a recheck here: we just need to get a snapshot of our devices // No need to do a recheck here: we just need to get a snapshot of our devices
// before we download any new ones. // before we download any new ones.
}; };
_onDevicesUpdated = (users: string[]) => { private onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return; if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck(); this.recheck();
}; };
_onDeviceVerificationChanged = (userId: string) => { private onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this.recheck();
}; };
_onUserTrustStatusChanged = (userId: string) => { private onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this.recheck();
}; };
_onCrossSingingKeysChanged = () => { private onCrossSingingKeysChanged = () => {
this._recheck(); this.recheck();
}; };
_onAccountData = (ev) => { private onAccountData = (ev: MatrixEvent) => {
// User may have: // User may have:
// * migrated SSSS to symmetric // * migrated SSSS to symmetric
// * uploaded keys to secret storage // * uploaded keys to secret storage
@ -163,32 +164,32 @@ export default class DeviceListener {
ev.getType().startsWith('m.cross_signing.') || ev.getType().startsWith('m.cross_signing.') ||
ev.getType() === 'm.megolm_backup.v1' ev.getType() === 'm.megolm_backup.v1'
) { ) {
this._recheck(); this.recheck();
} }
}; };
_onSync = (state, prevState) => { private onSync = (state, prevState) => {
if (state === 'PREPARED' && prevState === null) this._recheck(); if (state === 'PREPARED' && prevState === null) this.recheck();
}; };
_onRoomStateEvents = (ev: MatrixEvent) => { private onRoomStateEvents = (ev: MatrixEvent) => {
if (ev.getType() !== "m.room.encryption") { if (ev.getType() !== "m.room.encryption") {
return; return;
} }
// If a room changes to encrypted, re-check as it may be our first // If a room changes to encrypted, re-check as it may be our first
// encrypted room. This also catches encrypted room creation as well. // encrypted room. This also catches encrypted room creation as well.
this._recheck(); this.recheck();
}; };
_onAction = ({ action }) => { private onAction = ({ action }: ActionPayload) => {
if (action !== "on_logged_in") return; if (action !== "on_logged_in") return;
this._recheck(); this.recheck();
}; };
// The server doesn't tell us when key backup is set up, so we poll // The server doesn't tell us when key backup is set up, so we poll
// & cache the result // & cache the result
async _getKeyBackupInfo() { private async getKeyBackupInfo() {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
@ -206,7 +207,7 @@ export default class DeviceListener {
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId)); return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
} }
async _recheck() { private async recheck() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
@ -235,7 +236,7 @@ export default class DeviceListener {
// Cross-signing on account but this device doesn't trust the master key (verify this session) // Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else { } else {
const backupInfo = await this._getKeyBackupInfo(); const backupInfo = await this.getKeyBackupInfo();
if (backupInfo) { if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption) // No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
@ -256,7 +257,7 @@ export default class DeviceListener {
// This needs to be done after awaiting on downloadKeys() above, so // This needs to be done after awaiting on downloadKeys() above, so
// we make sure we get the devices after the fetch is done. // we make sure we get the devices after the fetch is done.
this._ensureDeviceIdsAtStartPopulated(); this.ensureDeviceIdsAtStartPopulated();
// Unverified devices that were there last time the app ran // Unverified devices that were there last time the app ran
// (technically could just be a boolean: we don't actually // (technically could just be a boolean: we don't actually

View file

@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
* This module provides a singleton instance of this class so the 'current' * This module provides a singleton instance of this class so the 'current'
* Matrix Client object is available easily. * Matrix Client object is available easily.
*/ */
class _MatrixClientPeg implements IMatrixClientPeg { class MatrixClientPegClass implements IMatrixClientPeg {
// These are the default options used when when the // These are the default options used when when the
// client is started in 'start'. These can be altered // client is started in 'start'. These can be altered
// at any time up to after the 'will_start_client' // at any time up to after the 'will_start_client'
@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
} }
if (!window.mxMatrixClientPeg) { if (!window.mxMatrixClientPeg) {
window.mxMatrixClientPeg = new _MatrixClientPeg(); window.mxMatrixClientPeg = new MatrixClientPegClass();
} }
export const MatrixClientPeg = window.mxMatrixClientPeg; export const MatrixClientPeg = window.mxMatrixClientPeg;

View file

@ -522,7 +522,7 @@ export const Commands = [
aliases: ['j', 'goto'], aliases: ['j', 'goto'],
args: '<room-address>', args: '<room-address>',
description: _td('Joins room with given address'), description: _td('Joins room with given address'),
runFn: function(_, args) { runFn: function(roomId, args) {
if (args) { if (args) {
// Note: we support 2 versions of this command. The first is // Note: we support 2 versions of this command. The first is
// the public-facing one for most users and the other is a // the public-facing one for most users and the other is a
@ -1069,7 +1069,7 @@ export const Commands = [
command: "msg", command: "msg",
description: _td("Sends a message to the given user"), description: _td("Sends a message to the given user"),
args: "<user-id> <message>", args: "<user-id> <message>",
runFn: function(_, args) { runFn: function(roomId, args) {
if (args) { if (args) {
// matches the first whitespace delimited group and then the rest of the string // matches the first whitespace delimited group and then the rest of the string
const matches = args.match(/^(\S+?)(?: +(.*))?$/s); const matches = args.match(/^(\S+?)(?: +(.*))?$/s);

View file

@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
limit = -1, limit = -1,
): Promise<ICompletion[]> { ): Promise<ICompletion[]> {
// lazy-load user list into matcher // lazy-load user list into matcher
if (!this.users) this._makeUsers(); if (!this.users) this.makeUsers();
let completions = []; let completions = [];
const { command, range } = this.getCurrentCommand(rawQuery, selection, force); const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
return _t('Users'); return _t('Users');
} }
_makeUsers() { private makeUsers() {
const events = this.room.getLiveTimeline().getEvents(); const events = this.room.getLiveTimeline().getEvents();
const lastSpoken = {}; const lastSpoken = {};

View file

@ -17,8 +17,8 @@ limitations under the License.
*/ */
import * as React from 'react'; import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
interface IProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>; onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
@ -140,18 +142,6 @@ interface IState {
class LoggedInView extends React.Component<IProps, IState> { class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView'; static displayName = 'LoggedInView';
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: PropTypes.func,
// and lots and lots of other stuff.
};
protected readonly _matrixClient: MatrixClient; protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>; protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>; protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
componentDidMount() { componentDidMount() {
document.addEventListener('keydown', this._onNativeKeyDown, false); document.addEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._updateServerNoticeEvents(); this.updateServerNoticeEvents();
this._matrixClient.on("accountData", this.onAccountData); this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync); this._matrixClient.on("sync", this.onSync);
@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
"useCompactLayout", null, this.onCompactLayoutChanged, "useCompactLayout", null, this.onCompactLayoutChanged,
); );
this.resizer = this._createResizer(); this.resizer = this.createResizer();
this.resizer.attach(); this.resizer.attach();
this._loadResizerPreferences(); this.loadResizerPreferences();
} }
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
@ -221,37 +211,37 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
canResetTimelineInRoom = (roomId) => { public canResetTimelineInRoom = (roomId: string) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;
} }
return this._roomView.current.canResetTimeline(); return this._roomView.current.canResetTimeline();
}; };
_createResizer() { private createResizer() {
let size; let panelSize;
let collapsed; let panelCollapsed;
const collapseConfig: ICollapseConfig = { const collapseConfig: ICollapseConfig = {
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
toggleSize: 206 - 50, toggleSize: 206 - 50,
onCollapsed: (_collapsed) => { onCollapsed: (collapsed) => {
collapsed = _collapsed; panelCollapsed = collapsed;
if (_collapsed) { if (collapsed) {
dis.dispatch({ action: "hide_left_panel" }); dis.dispatch({ action: "hide_left_panel" });
window.localStorage.setItem("mx_lhs_size", '0'); window.localStorage.setItem("mx_lhs_size", '0');
} else { } else {
dis.dispatch({ action: "show_left_panel" }); dis.dispatch({ action: "show_left_panel" });
} }
}, },
onResized: (_size) => { onResized: (size) => {
size = _size; panelSize = size;
this.props.resizeNotifier.notifyLeftHandleResized(); this.props.resizeNotifier.notifyLeftHandleResized();
}, },
onResizeStart: () => { onResizeStart: () => {
this.props.resizeNotifier.startResizing(); this.props.resizeNotifier.startResizing();
}, },
onResizeStop: () => { onResizeStop: () => {
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size); if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
this.props.resizeNotifier.stopResizing(); this.props.resizeNotifier.stopResizing();
}, },
isItemCollapsed: domNode => { isItemCollapsed: domNode => {
@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
return resizer; return resizer;
} }
_loadResizerPreferences() { private loadResizerPreferences() {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (isNaN(lhsSize)) { if (isNaN(lhsSize)) {
lhsSize = 350; lhsSize = 350;
@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer.forHandleAt(0).resize(lhsSize); this.resizer.forHandleAt(0).resize(lhsSize);
} }
onAccountData = (event) => { private onAccountData = (event: MatrixEvent) => {
if (event.getType() === "m.ignored_user_list") { if (event.getType() === "m.ignored_user_list") {
dis.dispatch({ action: "ignore_state_changed" }); dis.dispatch({ action: "ignore_state_changed" });
} }
@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this.updateServerNoticeEvents();
} else { } else {
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
} }
}; };
onRoomStateEvents = (ev, state) => { onRoomStateEvents = (ev, state) => {
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this.updateServerNoticeEvents();
} }
}; };
@ -326,7 +316,7 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) { if (error) {
usageLimitEventContent = syncError.error.data; usageLimitEventContent = syncError.error.data;
@ -346,7 +336,7 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
} }
_updateServerNoticeEvents = async () => { private updateServerNoticeEvents = async () => {
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (!serverNoticeList) return []; if (!serverNoticeList) return [];
@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
); );
}); });
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
this.setState({ this.setState({
usageLimitEventContent, usageLimitEventContent,
usageLimitEventTs: pinnedEventTs, usageLimitEventTs: pinnedEventTs,
@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
_onPaste = (ev) => { private onPaste = (ev) => {
let canReceiveInput = false; let canReceiveInput = false;
let element = ev.target; let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element // test for all parents because the target can be a child of a contenteditable element
@ -425,22 +415,22 @@ class LoggedInView extends React.Component<IProps, IState> {
We also listen with a native listener on the document to get keydown events when no element is focused. We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element. Bubbling is irrelevant here as the target is the body element.
*/ */
_onReactKeyDown = (ev) => { private onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element // events caught while bubbling up on the root element
// of this component, so something must be focused. // of this component, so something must be focused.
this._onKeyDown(ev); this.onKeyDown(ev);
}; };
_onNativeKeyDown = (ev) => { private onNativeKeyDown = (ev) => {
// only pass this if there is no focused element. // only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the // if there is, onKeyDown will be called by the
// react keydown handler that respects the react bubbling order. // react keydown handler that respects the react bubbling order.
if (ev.target === document.body) { if (ev.target === document.body) {
this._onKeyDown(ev); this.onKeyDown(ev);
} }
}; };
_onKeyDown = (ev) => { private onKeyDown = (ev) => {
let handled = false; let handled = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev); const roomAction = getKeyBindingsManager().getRoomAction(ev);
@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
case RoomAction.JumpToFirstMessage: case RoomAction.JumpToFirstMessage:
case RoomAction.JumpToLatestMessage: case RoomAction.JumpToLatestMessage:
// pass the event down to the scroll panel // pass the event down to the scroll panel
this._onScrollKeyPressed(ev); this.onScrollKeyPressed(ev);
handled = true; handled = true;
break; break;
case RoomAction.FocusSearch: case RoomAction.FocusSearch:
@ -565,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
* dispatch a page-up/page-down/etc to the appropriate component * dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event * @param {Object} ev The key event
*/ */
_onScrollKeyPressed = (ev) => { private onScrollKeyPressed = (ev) => {
if (this._roomView.current) { if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev); this._roomView.current.handleScrollKey(ev);
} }
@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
return ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>
<div <div
onPaste={this._onPaste} onPaste={this.onPaste}
onKeyDown={this._onReactKeyDown} onKeyDown={this.onReactKeyDown}
className='mx_MatrixChat_wrapper' className='mx_MatrixChat_wrapper'
aria-hidden={this.props.hideToSRUsers} aria-hidden={this.props.hideToSRUsers}
> >

View file

@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillUpdate(props, state) { UNSAFE_componentWillUpdate(props, state) {
if (this.shouldTrackPageChange(this.state, state)) { if (this.shouldTrackPageChange(this.state, state)) {
this.startPageChangeTimer(); this.startPageChangeTimer();
@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: 'timeline_resize' }); dis.dispatch({ action: 'timeline_resize' });
} }
onRoomCreated(roomId: string) {
dis.dispatch({
action: "view_room",
room_id: roomId,
});
}
onRegisterClick = () => { onRegisterClick = () => {
this.showScreen("register"); this.showScreen("register");
}; };
@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
{...this.state} {...this.state}
ref={this.loggedInView} ref={this.loggedInView}
matrixClient={MatrixClientPeg.get()} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId} currentRoomId={this.state.currentRoomId}
/> />

View file

@ -653,8 +653,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
let willWantDateSeparator = false; let willWantDateSeparator = false;
let lastInSection = true;
if (nextEvent) { if (nextEvent) {
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
} }
// is this a continuation of the previous message? // is this a continuation of the previous message?
@ -712,7 +714,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
isTwelveHour={this.props.isTwelveHour} isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
last={last} last={last}
lastInSection={willWantDateSeparator} lastInSection={lastInSection}
lastSuccessful={isLastSuccessful} lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight} isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent} getRelationsForEvent={this.props.getRelationsForEvent}
@ -720,6 +722,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
layout={this.props.layout} layout={this.props.layout}
enableFlair={this.props.enableFlair} enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts} showReadReceipts={this.props.showReadReceipts}
hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
/> />
</TileErrorBoundary>, </TileErrorBoundary>,
); );

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
if (newProps.groupId !== this.props.groupId) { if (newProps.groupId !== this.props.groupId) {
this.unregisterGroupStore(); this.unregisterGroupStore();
this.initGroupStore(newProps.groupId); this.initGroupStore(newProps.groupId);
@ -174,7 +175,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
}); });
}; };
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => { private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => {
if (!this.props.room || member.roomId !== this.props.room.roomId) { if (!this.props.room || member.roomId !== this.props.room.roomId) {
return; return;
} }

View file

@ -814,7 +814,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}) : _t("Explore rooms"); }) : _t("Explore rooms");
return ( return (
<BaseDialog <BaseDialog
className={'mx_RoomDirectory_dialog'} className="mx_RoomDirectory_dialog"
hasCancel={true} hasCancel={true}
onFinished={this.onFinished} onFinished={this.onFinished}
title={title} title={title}

View file

@ -458,7 +458,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const numFields = 3; const numFields = 3;
const placeholders = [_t("General"), _t("Random"), _t("Support")]; const placeholders = [_t("General"), _t("Random"), _t("Support")];
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]); const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
const fields = new Array(numFields).fill(0).map((_, i) => { const fields = new Array(numFields).fill(0).map((x, i) => {
const name = "roomName" + i; const name = "roomName" + i;
return <Field return <Field
key={name} key={name}
@ -625,7 +625,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
const numFields = 3; const numFields = 3;
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()]; const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
const [emailAddresses, setEmailAddress] = useStateArray(numFields, ""); const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
const fields = new Array(numFields).fill(0).map((_, i) => { const fields = new Array(numFields).fill(0).map((x, i) => {
const name = "emailAddress" + i; const name = "emailAddress" + i;
return <Field return <Field
key={name} key={name}

View file

@ -74,7 +74,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
tabLocation: TabLocation.LEFT, tabLocation: TabLocation.LEFT,
}; };
private _getActiveTabIndex() { private getActiveTabIndex() {
if (!this.state || !this.state.activeTabIndex) return 0; if (!this.state || !this.state.activeTabIndex) return 0;
return this.state.activeTabIndex; return this.state.activeTabIndex;
} }
@ -84,7 +84,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
* @param {Tab} tab the tab to show * @param {Tab} tab the tab to show
* @private * @private
*/ */
private _setActiveTab(tab: Tab) { private setActiveTab(tab: Tab) {
const idx = this.props.tabs.indexOf(tab); const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) { if (idx !== -1) {
if (this.props.onChange) this.props.onChange(tab.id); if (this.props.onChange) this.props.onChange(tab.id);
@ -94,18 +94,18 @@ export default class TabbedView extends React.Component<IProps, IState> {
} }
} }
private _renderTabLabel(tab: Tab) { private renderTabLabel(tab: Tab) {
let classes = "mx_TabbedView_tabLabel "; let classes = "mx_TabbedView_tabLabel ";
const idx = this.props.tabs.indexOf(tab); const idx = this.props.tabs.indexOf(tab);
if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active"; if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
let tabIcon = null; let tabIcon = null;
if (tab.icon) { if (tab.icon) {
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />; tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
} }
const onClickHandler = () => this._setActiveTab(tab); const onClickHandler = () => this.setActiveTab(tab);
const label = _t(tab.label); const label = _t(tab.label);
return ( return (
@ -118,7 +118,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
); );
} }
private _renderTabPanel(tab: Tab): React.ReactNode { private renderTabPanel(tab: Tab): React.ReactNode {
return ( return (
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}> <div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'> <AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
@ -129,8 +129,8 @@ export default class TabbedView extends React.Component<IProps, IState> {
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
const tabbedViewClasses = classNames({ const tabbedViewClasses = classNames({
'mx_TabbedView': true, 'mx_TabbedView': true,

View file

@ -277,7 +277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Move into constructor // TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
if (this.props.manageReadReceipts) { if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity(); this.updateReadReceiptOnUserActivity();
@ -290,7 +290,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillReceiveProps(newProps) { UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.timelineSet !== this.props.timelineSet) { if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported"); // throw new Error("changing timelineSet on a TimelinePanel is not supported");

View file

@ -37,14 +37,14 @@ export default class ToastContainer extends React.Component<{}, IState> {
// toasts may dismiss themselves in their didMount if they find // toasts may dismiss themselves in their didMount if they find
// they're already irrelevant by the time they're mounted, and // they're already irrelevant by the time they're mounted, and
// our own componentDidMount is too late. // our own componentDidMount is too late.
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate); ToastStore.sharedInstance().on('update', this.onToastStoreUpdate);
} }
componentWillUnmount() { componentWillUnmount() {
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate); ToastStore.sharedInstance().removeListener('update', this.onToastStoreUpdate);
} }
_onToastStoreUpdate = () => { private onToastStoreUpdate = () => {
this.setState({ this.setState({
toasts: ToastStore.sharedInstance().getToasts(), toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(), countSeen: ToastStore.sharedInstance().getCountSeen(),

View file

@ -101,7 +101,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
public UNSAFE_componentWillReceiveProps(newProps: IProps): void { public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;

View file

@ -144,7 +144,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.initLoginLogic(this.props.serverConfig); this.initLoginLogic(this.props.serverConfig);
} }
@ -154,7 +154,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillReceiveProps(newProps) { UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;

View file

@ -141,7 +141,7 @@ export default class Registration extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillReceiveProps(newProps) { UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;

View file

@ -60,8 +60,8 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
<div id='mx_Dialog_content'> <div id='mx_Dialog_content'>
{/* eslint-disable-next-line */} <p>{ _t("Unable to find profiles for the Matrix IDs listed below - " +
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p> "would you like to invite them anyway?") }</p>
<ul> <ul>
{ errorList } { errorList }
</ul> </ul>

View file

@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
emailAddresses.push(( emailAddresses.push((
<Field <Field
key={emailAddresses.length} key={emailAddresses.length}
value={""} value=""
onChange={(e) => this.onAddressChange(e, emailAddresses.length)} onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}

View file

@ -102,7 +102,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
}); });
}; };
_onCancel = () => { private onCancel = () => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
@ -167,7 +167,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" /> <input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
<button onClick={this._onCancel}> <button onClick={this.onCancel}>
{ _t("Cancel") } { _t("Cancel") }
</button> </button>
</div> </div>

View file

@ -337,7 +337,7 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return; if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
this.setState({ this.setState({
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query), filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),

View file

@ -40,7 +40,7 @@ interface IState {
busy: boolean; busy: boolean;
err?: string; err?: string;
// If we know it, the nature of the abuse, as specified by MSC3215. // If we know it, the nature of the abuse, as specified by MSC3215.
nature?: EXTENDED_NATURE; nature?: ExtendedNature;
} }
const MODERATED_BY_STATE_EVENT_TYPE = [ const MODERATED_BY_STATE_EVENT_TYPE = [
@ -55,22 +55,22 @@ const MODERATED_BY_STATE_EVENT_TYPE = [
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report"; const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
// Standard abuse natures. // Standard abuse natures.
enum NATURE { enum Nature {
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement", Disagreement = "org.matrix.msc3215.abuse.nature.disagreement",
TOXIC = "org.matrix.msc3215.abuse.nature.toxic", Toxic = "org.matrix.msc3215.abuse.nature.toxic",
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal", Illegal = "org.matrix.msc3215.abuse.nature.illegal",
SPAM = "org.matrix.msc3215.abuse.nature.spam", Spam = "org.matrix.msc3215.abuse.nature.spam",
OTHER = "org.matrix.msc3215.abuse.nature.other", Other = "org.matrix.msc3215.abuse.nature.other",
} }
enum NON_STANDARD_NATURE { enum NonStandardValue {
// Non-standard abuse nature. // Non-standard abuse nature.
// It should never leave the client - we use it to fallback to // It should never leave the client - we use it to fallback to
// server-wide abuse reporting. // server-wide abuse reporting.
ADMIN = "non-standard.abuse.nature.admin" Admin = "non-standard.abuse.nature.admin"
} }
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE; type ExtendedNature = Nature | NonStandardValue;
type Moderation = { type Moderation = {
// The id of the moderation room. // The id of the moderation room.
@ -170,7 +170,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
// The user has clicked on a nature. // The user has clicked on a nature.
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => { private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE }); this.setState({ nature: e.currentTarget.value as ExtendedNature });
}; };
// The user has clicked "cancel". // The user has clicked "cancel".
@ -187,7 +187,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
// We need a nature. // We need a nature.
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`. // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
if (!this.state.nature || if (!this.state.nature ||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN) ((this.state.nature == Nature.Other || this.state.nature == NonStandardValue.Admin)
&& !reason) && !reason)
) { ) {
this.setState({ this.setState({
@ -214,8 +214,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
try { try {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) { if (this.moderation && this.state.nature != NonStandardValue.Admin) {
const nature: NATURE = this.state.nature; const nature: Nature = this.state.nature;
// Report to moderators through to the dedicated bot, // Report to moderators through to the dedicated bot,
// as configured in the room's state events. // as configured in the room's state events.
@ -274,27 +274,27 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
const homeServerName = SdkConfig.get()["validated_server_config"].hsName; const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
let subtitle; let subtitle;
switch (this.state.nature) { switch (this.state.nature) {
case NATURE.DISAGREEMENT: case Nature.Disagreement:
subtitle = _t("What this user is writing is wrong.\n" + subtitle = _t("What this user is writing is wrong.\n" +
"This will be reported to the room moderators."); "This will be reported to the room moderators.");
break; break;
case NATURE.TOXIC: case Nature.Toxic:
subtitle = _t("This user is displaying toxic behaviour, " + subtitle = _t("This user is displaying toxic behaviour, " +
"for instance by insulting other users or sharing " + "for instance by insulting other users or sharing " +
" adult-only content in a family-friendly room " + " adult-only content in a family-friendly room " +
" or otherwise violating the rules of this room.\n" + " or otherwise violating the rules of this room.\n" +
"This will be reported to the room moderators."); "This will be reported to the room moderators.");
break; break;
case NATURE.ILLEGAL: case Nature.Illegal:
subtitle = _t("This user is displaying illegal behaviour, " + subtitle = _t("This user is displaying illegal behaviour, " +
"for instance by doxing people or threatening violence.\n" + "for instance by doxing people or threatening violence.\n" +
"This will be reported to the room moderators who may escalate this to legal authorities."); "This will be reported to the room moderators who may escalate this to legal authorities.");
break; break;
case NATURE.SPAM: case Nature.Spam:
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" + subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
"This will be reported to the room moderators."); "This will be reported to the room moderators.");
break; break;
case NON_STANDARD_NATURE.ADMIN: case NonStandardValue.Admin:
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) { if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
subtitle = _t("This room is dedicated to illegal or toxic content " + subtitle = _t("This room is dedicated to illegal or toxic content " +
"or the moderators fail to moderate illegal or toxic content.\n" + "or the moderators fail to moderate illegal or toxic content.\n" +
@ -308,7 +308,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
{ homeserver: homeServerName }); { homeserver: homeServerName });
} }
break; break;
case NATURE.OTHER: case Nature.Other:
subtitle = _t("Any other reason. Please describe the problem.\n" + subtitle = _t("Any other reason. Please describe the problem.\n" +
"This will be reported to the room moderators."); "This will be reported to the room moderators.");
break; break;
@ -327,48 +327,48 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
<div> <div>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NATURE.DISAGREEMENT } value={Nature.Disagreement}
checked = { this.state.nature == NATURE.DISAGREEMENT } checked={this.state.nature == Nature.Disagreement}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Disagree') } { _t('Disagree') }
</StyledRadioButton> </StyledRadioButton>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NATURE.TOXIC } value={Nature.Toxic}
checked = { this.state.nature == NATURE.TOXIC } checked={this.state.nature == Nature.Toxic}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Toxic Behaviour') } { _t('Toxic Behaviour') }
</StyledRadioButton> </StyledRadioButton>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NATURE.ILLEGAL } value={Nature.Illegal}
checked = { this.state.nature == NATURE.ILLEGAL } checked={this.state.nature == Nature.Illegal}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Illegal Content') } { _t('Illegal Content') }
</StyledRadioButton> </StyledRadioButton>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NATURE.SPAM } value={Nature.Spam}
checked = { this.state.nature == NATURE.SPAM } checked={this.state.nature == Nature.Spam}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Spam or propaganda') } { _t('Spam or propaganda') }
</StyledRadioButton> </StyledRadioButton>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NON_STANDARD_NATURE.ADMIN } value={NonStandardValue.Admin}
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN } checked={this.state.nature == NonStandardValue.Admin}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Report the entire room') } { _t('Report the entire room') }
</StyledRadioButton> </StyledRadioButton>
<StyledRadioButton <StyledRadioButton
name="nature" name="nature"
value = { NATURE.OTHER } value={Nature.Other}
checked = { this.state.nature == NATURE.OTHER } checked={this.state.nature == Nature.Other}
onChange={this.onNatureChosen} onChange={this.onNatureChosen}
> >
{ _t('Other') } { _t('Other') }

View file

@ -81,7 +81,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
this.setState({ mjolnirEnabled: newValue }); this.setState({ mjolnirEnabled: newValue });
}; };
_getTabs() { private getTabs() {
const tabs = []; const tabs = [];
tabs.push(new Tab( tabs.push(new Tab(
@ -170,7 +170,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
title={_t("Settings")} title={_t("Settings")}
> >
<div className='mx_SettingsDialog_content'> <div className='mx_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} /> <TabbedView tabs={this.getTabs()} initialTabId={this.props.initialTabId} />
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -453,13 +453,13 @@ export default class AppTile extends React.Component {
title={_t('Popout widget')} title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick} onClick={this._onPopoutWidgetClick}
/> } /> }
{ <ContextMenuButton <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
label={_t("Options")} label={_t("Options")}
isExpanded={this.state.menuDisplayed} isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton} inputRef={this._contextMenuButton}
onClick={this._onContextMenuClick} onClick={this._onContextMenuClick}
/> } />
</span> </span>
</div> } </div> }
{ appTileBody } { appTileBody }

View file

@ -63,7 +63,7 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through // If we are only given few events then just pass them through
if (events.length < threshold) { if (events.length < threshold) {
return ( return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}> <li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
{ children } { children }
</li> </li>
); );
@ -92,7 +92,7 @@ const EventListSummary: React.FC<IProps> = ({
} }
return ( return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}> <li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}> <AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') } { expanded ? _t('collapse') : _t('expand') }
</AccessibleButton> </AccessibleButton>
@ -101,4 +101,8 @@ const EventListSummary: React.FC<IProps> = ({
); );
}; };
EventListSummary.defaultProps = {
startExpanded: false,
};
export default EventListSummary; export default EventListSummary;

View file

@ -83,7 +83,7 @@ export default class PersistedElement extends React.Component {
// for this, so we bodge it by listening for document resize and // for this, so we bodge it by listening for document resize and
// the timeline_resize action. // the timeline_resize action.
window.addEventListener('resize', this._repositionChild); window.addEventListener('resize', this._repositionChild);
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this._onAction);
} }
/** /**

View file

@ -45,7 +45,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
SpellCheckLanguagesDropdownIState> { SpellCheckLanguagesDropdownIState> {
constructor(props) { constructor(props) {
super(props); super(props);
this._onSearchChange = this._onSearchChange.bind(this); this.onSearchChange = this.onSearchChange.bind(this);
this.state = { this.state = {
searchQuery: '', searchQuery: '',
@ -76,10 +76,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
} }
} }
_onSearchChange(search) { private onSearchChange(searchQuery: string) {
this.setState({ this.setState({ searchQuery });
searchQuery: search,
});
} }
render() { render() {
@ -117,7 +115,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
id="mx_LanguageDropdown" id="mx_LanguageDropdown"
className={this.props.className} className={this.props.className}
onOptionChange={this.props.onOptionChange} onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange} onSearchChange={this.onSearchChange}
searchEnabled={true} searchEnabled={true}
value={value} value={value}
label={_t("Language Dropdown")}> label={_t("Language Dropdown")}>

View file

@ -56,7 +56,7 @@ export default class TextWithTooltip extends React.Component {
{...tooltipProps} {...tooltipProps}
label={tooltip} label={tooltip}
tooltipClassName={tooltipClass} tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} className="mx_TextWithTooltip_tooltip"
/> } /> }
</span> </span>
); );

View file

@ -304,13 +304,6 @@ export default class MImageBody extends React.Component<IProps, IState> {
this.downloadImage(); this.downloadImage();
this.setState({ showImage: true }); this.setState({ showImage: true });
} }
this._afterComponentDidMount();
}
// To be overridden by subclasses (e.g. MStickerBody) for further
// initialisation after componentDidMount
_afterComponentDidMount() {
} }
componentWillUnmount() { componentWillUnmount() {

View file

@ -16,12 +16,19 @@ limitations under the License.
*/ */
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src";
export default forwardRef(({ mxEvent }, ref) => { interface IProps {
mxEvent: MatrixEvent;
children?: React.ReactNode;
}
export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject<HTMLSpanElement>) => {
const text = mxEvent.getContent().body; const text = mxEvent.getContent().body;
return ( return (
<span className="mx_UnknownBody" ref={ref}> <span className="mx_UnknownBody" ref={ref}>
{ text } { text }
{ children }
</span> </span>
); );
}); });

View file

@ -385,7 +385,7 @@ const UserOptionsSection: React.FC<{
} }
insertPillButton = ( insertPillButton = (
<AccessibleButton onClick={onInsertPillButton} className={"mx_UserInfo_field"}> <AccessibleButton onClick={onInsertPillButton} className="mx_UserInfo_field">
{ _t('Mention') } { _t('Mention') }
</AccessibleButton> </AccessibleButton>
); );

View file

@ -106,7 +106,7 @@ export default class RelatedGroupSettings extends React.Component {
<EditableItemList <EditableItemList
id="relatedGroups" id="relatedGroups"
items={this.state.newGroupsList} items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"} className="mx_RelatedGroupSettings"
newItem={this.state.newGroupId} newItem={this.state.newGroupId}
canRemove={this.props.canSetRelatedGroups} canRemove={this.props.canSetRelatedGroups}
canEdit={this.props.canSetRelatedGroups} canEdit={this.props.canSetRelatedGroups}

View file

@ -170,8 +170,6 @@ export function getHandlerTile(ev) {
return eventTileTypes[type]; return eventTileTypes[type];
} }
const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is: // Our component structure for EventTiles on the timeline is:
// //
// .-EventTile------------------------------------------------. // .-EventTile------------------------------------------------.
@ -297,6 +295,9 @@ interface IProps {
// whether or not to always show timestamps // whether or not to always show timestamps
alwaysShowTimestamps?: boolean; alwaysShowTimestamps?: boolean;
// whether or not to display the sender
hideSender?: boolean;
} }
interface IState { interface IState {
@ -430,7 +431,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Move into constructor // TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
@ -452,7 +453,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
// re-check the sender verification as outgoing events progress through // re-check the sender verification as outgoing events progress through
// the send process. // the send process.
@ -656,6 +657,10 @@ export default class EventTile extends React.Component<IProps, IState> {
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />; return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
} }
const MAX_READ_AVATARS = this.props.layout == Layout.Bubble
? 2
: 5;
// return early if there are no read receipts // return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) { if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
// We currently must include `mx_EventTile_readAvatars` in the DOM // We currently must include `mx_EventTile_readAvatars` in the DOM
@ -951,7 +956,7 @@ export default class EventTile extends React.Component<IProps, IState> {
); );
} }
if (needsSenderProfile) { if (needsSenderProfile && this.props.hideSender !== true) {
if (!this.props.tileShape) { if (!this.props.tileShape) {
sender = <SenderProfile onClick={this.onSenderProfileClick} sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
@ -971,8 +976,12 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange} onFocusChange={this.onActionBarFocusChange}
/> : undefined; /> : undefined;
const showTimestamp = this.props.mxEvent.getTs() && const showTimestamp = this.props.mxEvent.getTs()
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); && (this.props.alwaysShowTimestamps
|| this.props.last
|| this.state.hover
|| this.state.actionBarFocused);
const timestamp = showTimestamp ? const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null; <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -1112,6 +1121,8 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.alwaysShowTimestamps || this.state.hover, this.props.alwaysShowTimestamps || this.state.hover,
); );
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return ( return (
React.createElement(this.props.as || "li", { React.createElement(this.props.as || "li", {
@ -1121,6 +1132,9 @@ export default class EventTile extends React.Component<IProps, IState> {
"aria-live": ariaLive, "aria-live": ariaLive,
"aria-atomic": "true", "aria-atomic": "true",
"data-scroll-tokens": scrollToken, "data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
"data-has-reply": !!thread,
"onMouseEnter": () => this.setState({ hover: true }), "onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }), "onMouseLeave": () => this.setState({ hover: false }),
}, <> }, <>
@ -1142,9 +1156,9 @@ export default class EventTile extends React.Component<IProps, IState> {
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
/> />
{ keyRequestInfo } { keyRequestInfo }
{ reactionsRow }
{ actionBar } { actionBar }
</div> </div>
{ reactionsRow }
{ msgOption } { msgOption }
{ avatar } { avatar }
</>) </>)

View file

@ -93,7 +93,7 @@ export default class MemberList extends React.Component<IProps, IState> {
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true; this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this.mounted = true; this.mounted = true;

View file

@ -441,7 +441,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
} }
// TODO: [REACT-WARNING] Move this to constructor // TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line camelcase UNSAFE_componentWillMount() { // eslint-disable-line
const partCreator = new CommandPartCreator(this.props.room, this.context); const partCreator = new CommandPartCreator(this.props.room, this.context);
const parts = this.restoreStoredEditorState(partCreator) || []; const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator); this.model = new EditorModel(parts, partCreator);

View file

@ -64,8 +64,8 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
} }
componentDidUpdate(_, prevState) { componentDidUpdate(_, prevState) {
const wasVisible = this._isVisible(prevState); const wasVisible = WhoIsTypingTile.isVisible(prevState);
const isVisible = this._isVisible(this.state); const isVisible = WhoIsTypingTile.isVisible(this.state);
if (this.props.onShown && !wasVisible && isVisible) { if (this.props.onShown && !wasVisible && isVisible) {
this.props.onShown(); this.props.onShown();
} else if (this.props.onHidden && wasVisible && !isVisible) { } else if (this.props.onHidden && wasVisible && !isVisible) {
@ -83,12 +83,12 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort()); Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
} }
private _isVisible(state: IState): boolean { private static isVisible(state: IState): boolean {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0; return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
} }
public isVisible = (): boolean => { public isVisible = (): boolean => {
return this._isVisible(this.state); return WhoIsTypingTile.isVisible(this.state);
}; };
private onRoomTimeline = (event: MatrixEvent, room: Room): void => { private onRoomTimeline = (event: MatrixEvent, room: Room): void => {

View file

@ -35,7 +35,7 @@ interface SpellCheckLanguagesIState {
} }
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> { export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
_onRemove = (e) => { private onRemove = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -46,7 +46,7 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
return ( return (
<div className="mx_ExistingSpellCheckLanguage"> <div className="mx_ExistingSpellCheckLanguage">
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span> <span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm"> <AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -63,12 +63,12 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
}; };
} }
_onRemoved = (language) => { private onRemoved = (language: string) => {
const languages = this.props.languages.filter((e) => e !== language); const languages = this.props.languages.filter((e) => e !== language);
this.props.onLanguagesChange(languages); this.props.onLanguagesChange(languages);
}; };
_onAddClick = (e) => { private onAddClick = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -81,18 +81,18 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
this.props.onLanguagesChange(this.props.languages); this.props.onLanguagesChange(this.props.languages);
}; };
_onNewLanguageChange = (language: string) => { private onNewLanguageChange = (language: string) => {
if (this.state.newLanguage === language) return; if (this.state.newLanguage === language) return;
this.setState({ newLanguage: language }); this.setState({ newLanguage: language });
}; };
render() { render() {
const existingSpellCheckLanguages = this.props.languages.map((e) => { const existingSpellCheckLanguages = this.props.languages.map((e) => {
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />; return <ExistingSpellCheckLanguage language={e} onRemoved={this.onRemoved} key={e} />;
}); });
const addButton = ( const addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary"> <AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") } { _t("Add") }
</AccessibleButton> </AccessibleButton>
); );
@ -100,11 +100,11 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
return ( return (
<div className="mx_SpellCheckLanguages"> <div className="mx_SpellCheckLanguages">
{ existingSpellCheckLanguages } { existingSpellCheckLanguages }
<form onSubmit={this._onAddClick} noValidate={true}> <form onSubmit={this.onAddClick} noValidate={true}>
<SpellCheckLanguagesDropdown <SpellCheckLanguagesDropdown
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput" className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
value={this.state.newLanguage} value={this.state.newLanguage}
onOptionChange={this._onNewLanguageChange} /> onOptionChange={this.onNewLanguageChange} />
{ addButton } { addButton }
</form> </form>
</div> </div>

View file

@ -78,7 +78,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
} }
// TODO: [REACT-WARNING] Move this to constructor // TODO: [REACT-WARNING] Move this to constructor
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase async UNSAFE_componentWillMount() { // eslint-disable-line
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent); MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const room = MatrixClientPeg.get().getRoom(this.props.roomId);

View file

@ -37,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/Layout"; import { Layout } from "../../../../../settings/Layout";
import classNames from 'classnames';
import StyledRadioButton from '../../../elements/StyledRadioButton';
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { compare } from "../../../../../utils/strings"; import { compare } from "../../../../../utils/strings";
@ -241,6 +243,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
this.setState({ customThemeUrl: e.target.value }); this.setState({ customThemeUrl: e.target.value });
}; };
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
let layout;
switch (e.target.value) {
case "irc": layout = Layout.IRC; break;
case "group": layout = Layout.Group; break;
case "bubble": layout = Layout.Bubble; break;
}
this.setState({ layout: layout });
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
};
private onIRCLayoutChange = (enabled: boolean) => { private onIRCLayoutChange = (enabled: boolean) => {
if (enabled) { if (enabled) {
this.setState({ layout: Layout.IRC }); this.setState({ layout: Layout.IRC });
@ -373,6 +388,77 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</div>; </div>;
} }
private renderLayoutSection = () => {
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.IRC}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="irc"
checked={this.state.layout === Layout.IRC}
onChange={this.onLayoutChange}
>
{ _t("IRC") }
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.Group}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="group"
checked={this.state.layout == Layout.Group}
onChange={this.onLayoutChange}
>
{ _t("Modern") }
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.Bubble}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="bubble"
checked={this.state.layout == Layout.Bubble}
onChange={this.onLayoutChange}
>
{ _t("Message bubbles") }
</StyledRadioButton>
</div>
</div>
</div>;
};
private renderAdvancedSection() { private renderAdvancedSection() {
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null; if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
@ -396,14 +482,17 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
name="useCompactLayout" name="useCompactLayout"
level={SettingLevel.DEVICE} level={SettingLevel.DEVICE}
useCheckbox={true} useCheckbox={true}
disabled={this.state.layout == Layout.IRC} disabled={this.state.layout !== Layout.Group}
/> />
{ !SettingsStore.getValue("feature_new_layout_switcher") ?
<StyledCheckbox <StyledCheckbox
checked={this.state.layout == Layout.IRC} checked={this.state.layout == Layout.IRC}
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)} onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
> >
{ _t("Enable experimental, compact IRC style layout") } { _t("Enable experimental, compact IRC style layout") }
</StyledCheckbox> </StyledCheckbox> : null
}
<SettingsFlag <SettingsFlag
name="useSystemFont" name="useSystemFont"
@ -444,6 +533,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) } { _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
</div> </div>
{ this.renderThemeSection() } { this.renderThemeSection() }
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
{ this.renderFontSection() } { this.renderFontSection() }
{ this.renderAdvancedSection() } { this.renderAdvancedSection() }
</div> </div>

View file

@ -60,14 +60,14 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
this.setState({ counter }); this.setState({ counter });
}, 1000); }, 1000);
} }
request.on("change", this._checkRequestIsPending); request.on("change", this.checkRequestIsPending);
// We should probably have a separate class managing the active verification toasts, // We should probably have a separate class managing the active verification toasts,
// rather than monitoring this in the toast component itself, since we'll get problems // rather than monitoring this in the toast component itself, since we'll get problems
// like the toasdt not going away when the verification is cancelled unless it's the // like the toasdt not going away when the verification is cancelled unless it's the
// one on the top (ie. the one that's mounted). // one on the top (ie. the one that's mounted).
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents // As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
// a toast hanging around after logging in if you did a verification as part of login). // a toast hanging around after logging in if you did a verification as part of login).
this._checkRequestIsPending(); this.checkRequestIsPending();
if (request.isSelfVerification) { if (request.isSelfVerification) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -83,10 +83,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
componentWillUnmount() { componentWillUnmount() {
clearInterval(this.intervalHandle); clearInterval(this.intervalHandle);
const { request } = this.props; const { request } = this.props;
request.off("change", this._checkRequestIsPending); request.off("change", this.checkRequestIsPending);
} }
_checkRequestIsPending = () => { private checkRequestIsPending = () => {
const { request } = this.props; const { request } = this.props;
if (!request.canAccept) { if (!request.canAccept) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey); ToastStore.sharedInstance().dismissToast(this.props.toastKey);

View file

@ -513,7 +513,9 @@ export default class CallView extends React.Component<IProps, IState> {
transferee: transfereeName, transferee: transfereeName,
}, },
{ {
a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>, a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>
{ sub }
</AccessibleButton>,
}, },
) } ) }
</div>; </div>;

View file

@ -144,7 +144,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
</div> </div>
<div className="mx_IncomingCallBox_buttons"> <div className="mx_IncomingCallBox_buttons">
<AccessibleButton <AccessibleButton
className={"mx_IncomingCallBox_decline"} className="mx_IncomingCallBox_decline"
onClick={this.onRejectClick} onClick={this.onRejectClick}
kind="danger" kind="danger"
> >
@ -152,7 +152,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
</AccessibleButton> </AccessibleButton>
<div className="mx_IncomingCallBox_spacer" /> <div className="mx_IncomingCallBox_spacer" />
<AccessibleButton <AccessibleButton
className={"mx_IncomingCallBox_accept"} className="mx_IncomingCallBox_accept"
onClick={this.onAnswerClick} onClick={this.onAnswerClick}
kind="primary" kind="primary"
> >

View file

@ -17,6 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
@ -247,11 +248,11 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
* NOTE: this assumes you've just created the room and there's not been an opportunity * NOTE: this assumes you've just created the room and there's not been an opportunity
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by. * for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
*/ */
export async function _waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) { export async function waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
const { timeout } = opts; const { timeout } = opts;
let handler; let handler;
return new Promise((resolve) => { return new Promise((resolve) => {
handler = function(_event, _roomstate, member) { handler = function(_, __, member: RoomMember) { // eslint-disable-line @typescript-eslint/naming-convention
if (member.userId !== userId) return; if (member.userId !== userId) return;
if (member.roomId !== roomId) return; if (member.roomId !== roomId) return;
resolve(true); resolve(true);
@ -324,7 +325,7 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom
} }
roomId = await createRoom({ encryption, dmUserId: userId, spinner: false, andView: false }); roomId = await createRoom({ encryption, dmUserId: userId, spinner: false, andView: false });
await _waitForMember(client, roomId, userId); await waitForMember(client, roomId, userId);
} }
return roomId; return roomId;
} }

View file

@ -274,7 +274,7 @@ abstract class PillPart extends BasePart implements IPillPart {
} }
// helper method for subclasses // helper method for subclasses
_setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) { protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
const avatarBackground = `url('${avatarUrl}')`; const avatarBackground = `url('${avatarUrl}')`;
const avatarLetter = `'${initialLetter}'`; const avatarLetter = `'${initialLetter}'`;
// check if the value is changing, // check if the value is changing,
@ -354,7 +354,7 @@ class RoomPillPart extends PillPart {
initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId); initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId); avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
} }
this._setAvatarVars(node, avatarUrl, initialLetter); this.setAvatarVars(node, avatarUrl, initialLetter);
} }
get type(): IPillPart["type"] { get type(): IPillPart["type"] {
@ -399,7 +399,7 @@ class UserPillPart extends PillPart {
if (avatarUrl === defaultAvatarUrl) { if (avatarUrl === defaultAvatarUrl) {
initialLetter = Avatar.getInitialLetter(name); initialLetter = Avatar.getInitialLetter(name);
} }
this._setAvatarVars(node, avatarUrl, initialLetter); this.setAvatarVars(node, avatarUrl, initialLetter);
} }
get type(): IPillPart["type"] { get type(): IPillPart["type"] {

View file

@ -823,6 +823,7 @@
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
"Show info about bridges in room settings": "Show info about bridges in room settings", "Show info about bridges in room settings": "Show info about bridges in room settings",
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
"Font size": "Font size", "Font size": "Font size",
"Use custom size": "Use custom size", "Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
@ -1245,6 +1246,10 @@
"Custom theme URL": "Custom theme URL", "Custom theme URL": "Custom theme URL",
"Add theme": "Add theme", "Add theme": "Add theme",
"Theme": "Theme", "Theme": "Theme",
"Message layout": "Message layout",
"IRC": "IRC",
"Modern": "Modern",
"Message bubbles": "Message bubbles",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout", "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
"Customise your appearance": "Customise your appearance", "Customise your appearance": "Customise your appearance",

View file

@ -67,7 +67,7 @@ export function getUserLanguage(): string {
// Function which only purpose is to mark that a string is translatable // Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings // Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s: string): string { export function _td(s: string): string { // eslint-disable-line @typescript-eslint/naming-convention
return s; return s;
} }
@ -132,6 +132,8 @@ export type TranslatedString = string | React.ReactNode;
* *
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/ */
// eslint-next-line @typescript-eslint/naming-convention
// eslint-nexline @typescript-eslint/naming-convention
export function _t(text: string, variables?: IVariables): string; export function _t(text: string, variables?: IVariables): string;
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode; export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString { export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";
import { ALL_RULE_TYPES, BanList } from "./BanList"; import { ALL_RULE_TYPES, BanList } from "./BanList";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
@ -21,19 +22,17 @@ import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher"; import dis from "../dispatcher/dispatcher";
import { SettingLevel } from "../settings/SettingLevel"; import { SettingLevel } from "../settings/SettingLevel";
import { Preset } from "matrix-js-sdk/src/@types/partials"; import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ActionPayload } from "../dispatcher/payloads";
// TODO: Move this and related files to the js-sdk or something once finalized. // TODO: Move this and related files to the js-sdk or something once finalized.
export class Mjolnir { export class Mjolnir {
static _instance: Mjolnir = null; private static instance: Mjolnir = null;
_lists: BanList[] = []; private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention
_roomIds: string[] = []; private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention
_mjolnirWatchRef = null; private mjolnirWatchRef: string = null;
_dispatcherRef = null; private dispatcherRef: string = null;
constructor() {
}
get roomIds(): string[] { get roomIds(): string[] {
return this._roomIds; return this._roomIds;
@ -44,16 +43,16 @@ export class Mjolnir {
} }
start() { start() {
this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged.bind(this));
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
dis.dispatch({ dis.dispatch({
action: 'do_after_sync_prepared', action: 'do_after_sync_prepared',
deferred_action: { action: 'setup_mjolnir' }, deferred_action: { action: 'setup_mjolnir' },
}); });
} }
_onAction = (payload) => { private onAction = (payload: ActionPayload) => {
if (payload['action'] === 'setup_mjolnir') { if (payload['action'] === 'setup_mjolnir') {
console.log("Setting up Mjolnir: after sync"); console.log("Setting up Mjolnir: after sync");
this.setup(); this.setup();
@ -62,23 +61,23 @@ export class Mjolnir {
setup() { setup() {
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
this._updateLists(SettingsStore.getValue("mjolnirRooms")); this.updateLists(SettingsStore.getValue("mjolnirRooms"));
MatrixClientPeg.get().on("RoomState.events", this._onEvent); MatrixClientPeg.get().on("RoomState.events", this.onEvent);
} }
stop() { stop() {
if (this._mjolnirWatchRef) { if (this.mjolnirWatchRef) {
SettingsStore.unwatchSetting(this._mjolnirWatchRef); SettingsStore.unwatchSetting(this.mjolnirWatchRef);
this._mjolnirWatchRef = null; this.mjolnirWatchRef = null;
} }
if (this._dispatcherRef) { if (this.dispatcherRef) {
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
this._dispatcherRef = null; this.dispatcherRef = null;
} }
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent); MatrixClientPeg.get().removeListener("RoomState.events", this.onEvent);
} }
async getOrCreatePersonalList(): Promise<BanList> { async getOrCreatePersonalList(): Promise<BanList> {
@ -132,20 +131,20 @@ export class Mjolnir {
this._lists = this._lists.filter(b => b.roomId !== roomId); this._lists = this._lists.filter(b => b.roomId !== roomId);
} }
_onEvent = (event) => { private onEvent = (event: MatrixEvent) => {
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
if (!this._roomIds.includes(event.getRoomId())) return; if (!this._roomIds.includes(event.getRoomId())) return;
if (!ALL_RULE_TYPES.includes(event.getType())) return; if (!ALL_RULE_TYPES.includes(event.getType())) return;
this._updateLists(this._roomIds); this.updateLists(this._roomIds);
}; };
_onListsChanged(settingName, roomId, atLevel, newValue) { private onListsChanged(settingName: string, roomId: string, atLevel: SettingLevel, newValue: string[]) {
// We know that ban lists are only recorded at one level so we don't need to re-eval them // We know that ban lists are only recorded at one level so we don't need to re-eval them
this._updateLists(newValue); this.updateLists(newValue);
} }
_updateLists(listRoomIds: string[]) { private updateLists(listRoomIds: string[]) {
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
console.log("Updating Mjolnir ban lists to: " + listRoomIds); console.log("Updating Mjolnir ban lists to: " + listRoomIds);
@ -182,10 +181,10 @@ export class Mjolnir {
} }
static sharedInstance(): Mjolnir { static sharedInstance(): Mjolnir {
if (!Mjolnir._instance) { if (!Mjolnir.instance) {
Mjolnir._instance = new Mjolnir(); Mjolnir.instance = new Mjolnir();
} }
return Mjolnir._instance; return Mjolnir.instance;
} }
} }

View file

@ -203,7 +203,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
const body = await collectBugReport(opts); const body = await collectBugReport(opts);
progressCallback(_t("Uploading logs")); progressCallback(_t("Uploading logs"));
await _submitReport(bugReportEndpoint, body, progressCallback); await submitReport(bugReportEndpoint, body, progressCallback);
} }
/** /**
@ -289,10 +289,10 @@ export async function submitFeedback(
body.append(k, extraData[k]); body.append(k, extraData[k]);
} }
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {}); await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
} }
function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) { function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.open("POST", endpoint); req.open("POST", endpoint);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Copyright 2021 Quirin Götz <codeworks@supercable.onl>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,7 +20,8 @@ import PropTypes from 'prop-types';
/* TODO: This should be later reworked into something more generic */ /* TODO: This should be later reworked into something more generic */
export enum Layout { export enum Layout {
IRC = "irc", IRC = "irc",
Group = "group" Group = "group",
Bubble = "bubble",
} }
/* We need this because multiple components are still using JavaScript */ /* We need this because multiple components are still using JavaScript */

View file

@ -41,6 +41,7 @@ import { Layout } from "./Layout";
import ReducedMotionController from './controllers/ReducedMotionController'; import ReducedMotionController from './controllers/ReducedMotionController';
import IncompatibleController from "./controllers/IncompatibleController"; import IncompatibleController from "./controllers/IncompatibleController";
import SdkConfig from "../SdkConfig"; import SdkConfig from "../SdkConfig";
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = [ const LEVELS_ROOM_SETTINGS = [
@ -321,6 +322,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Show info about bridges in room settings"), displayName: _td("Show info about bridges in room settings"),
default: false, default: false,
}, },
"feature_new_layout_switcher": {
isFeature: true,
supportedLevels: LEVELS_FEATURE,
displayName: _td("New layout switcher (with message bubbles)"),
default: false,
controller: new NewLayoutSwitcherController(),
},
"RoomList.backgroundImage": { "RoomList.backgroundImage": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null, default: null,

View file

@ -0,0 +1,26 @@
/*
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 SettingController from "./SettingController";
import { SettingLevel } from "../SettingLevel";
import SettingsStore from "../SettingsStore";
import { Layout } from "../Layout";
export default class NewLayoutSwitcherController extends SettingController {
public onChange(level: SettingLevel, roomId: string, newValue: any) {
// On disabling switch back to Layout.Group if Layout.Bubble
if (!newValue && SettingsStore.getValue("layout") == Layout.Bubble) {
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
}
}
}

View file

@ -49,7 +49,7 @@ class GroupFilterOrderStore extends Store {
this.__emitChange(); this.__emitChange();
} }
__onDispatch(payload) { __onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) { switch (payload.action) {
// Initialise state after initial sync // Initialise state after initial sync
case 'view_room': { case 'view_room': {

View file

@ -44,7 +44,7 @@ class LifecycleStore extends Store<ActionPayload> {
this.__emitChange(); this.__emitChange();
} }
protected __onDispatch(payload: ActionPayload) { protected __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) { switch (payload.action) {
case 'do_after_sync_prepared': case 'do_after_sync_prepared':
this.setState({ this.setState({

View file

@ -144,7 +144,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
this.__emitChange(); this.__emitChange();
} }
__onDispatch(payload: ActionPayload) { __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) { switch (payload.action) {
case 'view_room': case 'view_room':
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink

View file

@ -96,7 +96,7 @@ class RoomViewStore extends Store<ActionPayload> {
this.__emitChange(); this.__emitChange();
} }
__onDispatch(payload) { __onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) { switch (payload.action) {
// view_room: // view_room:
// - room_alias: '#somealias:matrix.org' // - room_alias: '#somealias:matrix.org'

View file

@ -63,7 +63,7 @@ const PREVIEWS = {
const MAX_EVENTS_BACKWARDS = 50; const MAX_EVENTS_BACKWARDS = 50;
// type merging ftw // type merging ftw
type TAG_ANY = "im.vector.any"; type TAG_ANY = "im.vector.any"; // eslint-disable-line @typescript-eslint/naming-convention
const TAG_ANY: TAG_ANY = "im.vector.any"; const TAG_ANY: TAG_ANY = "im.vector.any";
interface IState { interface IState {

View file

@ -20,7 +20,7 @@ import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities"; import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
import React from "react"; import React from "react";
type GENERIC_WIDGET_KIND = "generic"; type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention
const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic"; const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
interface ISendRecvStaticCapText { interface ISendRecvStaticCapText {

View file

@ -1,5 +1,5 @@
import './skinned-sdk'; // Must be first for skinning to work import './skinned-sdk'; // Must be first for skinning to work
import { _waitForMember, canEncryptToAllUsers } from '../src/createRoom'; import { waitForMember, canEncryptToAllUsers } from '../src/createRoom';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
/* Shorter timeout, we've got tests to run */ /* Shorter timeout, we've got tests to run */
@ -13,7 +13,7 @@ describe("waitForMember", () => {
}); });
it("resolves with false if the timeout is reached", (done) => { it("resolves with false if the timeout is reached", (done) => {
_waitForMember(client, "", "", { timeout: 0 }).then((r) => { waitForMember(client, "", "", { timeout: 0 }).then((r) => {
expect(r).toBe(false); expect(r).toBe(false);
done(); done();
}); });
@ -22,7 +22,7 @@ describe("waitForMember", () => {
it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", (done) => { it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", (done) => {
const roomId = "!roomId:domain"; const roomId = "!roomId:domain";
const userId = "@clientId:domain"; const userId = "@clientId:domain";
_waitForMember(client, roomId, userId, { timeout }).then((r) => { waitForMember(client, roomId, userId, { timeout }).then((r) => {
expect(r).toBe(false); expect(r).toBe(false);
done(); done();
}); });
@ -32,7 +32,7 @@ describe("waitForMember", () => {
it("resolves with true if RoomState.newMember fires", (done) => { it("resolves with true if RoomState.newMember fires", (done) => {
const roomId = "!roomId:domain"; const roomId = "!roomId:domain";
const userId = "@clientId:domain"; const userId = "@clientId:domain";
_waitForMember(client, roomId, userId, { timeout }).then((r) => { waitForMember(client, roomId, userId, { timeout }).then((r) => {
expect(r).toBe(true); expect(r).toBe(true);
expect(client.listeners("RoomState.newMember").length).toBe(0); expect(client.listeners("RoomState.newMember").length).toBe(0);
done(); done();

View file

@ -3242,7 +3242,7 @@ eslint-config-google@^0.14.0:
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main": "eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
version "0.3.2" version "0.3.2"
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/383a1e4a9ef7944c921efda0de2ac9635d45cb5c" resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/8529f1d77863db6327cf1a1a4fa65d06cc26f91b"
eslint-plugin-react-hooks@^4.2.0: eslint-plugin-react-hooks@^4.2.0:
version "4.2.0" version "4.2.0"