Merge branch 'develop' into t3chguy/fix/18071

This commit is contained in:
Michael Telatynski 2021-07-27 14:47:18 +01:00 committed by GitHub
commit 8dd11cd2a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
463 changed files with 10031 additions and 6251 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",
@ -64,8 +65,8 @@
"counterpart": "^0.18.6", "counterpart": "^0.18.6",
"diff-dom": "^4.2.2", "diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"emojibase-data": "^5.1.1", "emojibase-data": "^6.2.0",
"emojibase-regex": "^4.1.1", "emojibase-regex": "^5.1.3",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "6.1.0", "filesize": "6.1.0",

View file

@ -87,6 +87,7 @@
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_LeaveSpaceDialog.scss"; @import "./views/dialogs/_LeaveSpaceDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss";
@ -160,6 +161,7 @@
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupRoomList.scss";
@import "./views/groups/_GroupUserSettings.scss"; @import "./views/groups/_GroupUserSettings.scss";
@import "./views/messages/_CallEvent.scss";
@import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_EventTileBubble.scss";
@ -172,7 +174,6 @@
@import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss"; @import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MediaBody.scss";
@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";

View file

@ -45,9 +45,14 @@ limitations under the License.
/* Overrides for the attachment body tiles */ /* Overrides for the attachment body tiles */
.mx_FilePanel .mx_EventTile { .mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
word-break: break-word; word-break: break-word;
margin-top: 32px; margin-top: 10px;
padding-top: 0;
.mx_EventTile_line {
padding-left: 0;
}
} }
.mx_FilePanel .mx_EventTile .mx_MImageBody { .mx_FilePanel .mx_EventTile .mx_MImageBody {
@ -118,10 +123,6 @@ limitations under the License.
padding-left: 0px; padding-left: 0px;
} }
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
background-color: $primary-bg-color;
}
.mx_FilePanel_empty::before { .mx_FilePanel_empty::before {
mask-image: url('$(res)/img/element-icons/room/files.svg'); mask-image: url('$(res)/img/element-icons/room/files.svg');
} }

View file

@ -84,7 +84,7 @@ limitations under the License.
display: inline; display: inline;
} }
.mx_NotificationPanel .mx_EventTile_senderDetails { .mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails {
padding-left: 36px; // align with the room name padding-left: 36px; // align with the room name
position: relative; position: relative;
@ -105,7 +105,7 @@ limitations under the License.
padding-left: 5px; padding-left: 5px;
} }
.mx_NotificationPanel .mx_EventTile_line { .mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
margin-right: 0px; margin-right: 0px;
padding-left: 36px; // align with the room name padding-left: 36px; // align with the room name
padding-top: 0px; padding-top: 0px;

View file

@ -190,7 +190,6 @@ limitations under the License.
position: relative; position: relative;
padding: 8px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
min-height: 56px;
box-sizing: border-box; box-sizing: border-box;
display: grid; display: grid;

View file

@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
} }
.mx_SpaceRoomView_landing { .mx_SpaceRoomView_landing {
display: flex;
flex-direction: column;
> .mx_BaseAvatar_image, > .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image { > .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SearchBox { .mx_SearchBox {
margin: 0 0 20px; margin: 0 0 20px;
flex: 0;
} }
.mx_SpaceFeedbackPrompt { .mx_SpaceFeedbackPrompt {
@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px;
display: none; display: none;
} }
} }
.mx_SpaceRoomDirectory_list {
// we don't want this container to get forced into the flexbox layout
display: contents;
}
} }
.mx_SpaceRoomView_privateScope { .mx_SpaceRoomView_privateScope {

View file

@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_AudioPlayer_container { .mx_MediaBody.mx_AudioPlayer_container {
padding: 16px 12px 12px 12px; padding: 16px 12px 12px 12px;
max-width: 267px; // use max to make the control fit in the files/pinned panels
.mx_AudioPlayer_primaryContainer { .mx_AudioPlayer_primaryContainer {
display: flex; display: flex;

View file

@ -18,10 +18,10 @@ limitations under the License.
// are shared amongst multiple voice message components. // are shared amongst multiple voice message components.
// Container for live recording and playback controls // Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer { .mx_MediaBody.mx_VoiceMessagePrimaryContainer {
// 7px top and bottom for visual design. 12px left & right, but the waveform (right) // The waveform (right) has a 1px padding on it that we want to account for, otherwise
// has a 1px padding on it that we want to account for. // inherit from mx_MediaBody
padding: 7px 12px 7px 11px; padding-right: 11px;
// Cheat at alignment a bit // Cheat at alignment a bit
display: flex; display: flex;

View file

@ -27,7 +27,6 @@ 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

@ -65,7 +65,7 @@ limitations under the License.
.mx_CreateRoomDialog_aliasContainer { .mx_CreateRoomDialog_aliasContainer {
display: flex; display: flex;
// put margin on container so it can collapse with siblings // put margin on container so it can collapse with siblings
margin: 10px 0; margin: 24px 0 10px;
.mx_RoomAliasField { .mx_RoomAliasField {
margin: 0; margin: 0;
@ -101,10 +101,6 @@ limitations under the License.
margin-left: 30px; margin-left: 30px;
} }
.mx_CreateRoomDialog_topic {
margin-bottom: 36px;
}
.mx_Dialog_content > .mx_SettingsFlag { .mx_Dialog_content > .mx_SettingsFlag {
margin-top: 24px; margin-top: 24px;
} }
@ -113,5 +109,56 @@ limitations under the License.
margin: 0 85px 0 0; margin: 0 85px 0 0;
font-size: $font-12px; font-size: $font-12px;
} }
.mx_Dropdown {
margin-bottom: 8px;
font-weight: normal;
font-family: $font-family;
font-size: $font-14px;
color: $primary-fg-color;
.mx_Dropdown_input {
border: 1px solid $input-border-color;
}
.mx_Dropdown_option {
font-size: $font-14px;
line-height: $font-32px;
height: 32px;
min-height: 32px;
> div {
padding-left: 30px;
position: relative;
&::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
left: 6px;
top: 8px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $secondary-fg-color;
}
}
}
.mx_CreateRoomDialog_dropdown_invite::before {
mask-image: url('$(res)/img/element-icons/lock.svg');
mask-size: contain;
}
.mx_CreateRoomDialog_dropdown_public::before {
mask-image: url('$(res)/img/globe.svg');
mask-size: 12px;
}
.mx_CreateRoomDialog_dropdown_restricted::before {
mask-image: url('$(res)/img/element-icons/community-members.svg');
mask-size: contain;
}
}
} }

View file

@ -36,6 +36,10 @@ limitations under the License.
flex-shrink: 0; flex-shrink: 0;
overflow-y: auto; overflow-y: auto;
.mx_EventTile[data-layout=bubble] {
margin-top: 20px;
}
div { div {
pointer-events: none; pointer-events: none;
} }

View file

@ -0,0 +1,150 @@
/*
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_ManageRestrictedJoinRuleDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
}
}
.mx_ManageRestrictedJoinRuleDialog {
width: 480px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 60vh;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ManageRestrictedJoinRuleDialog_content {
flex-grow: 1;
}
.mx_ManageRestrictedJoinRuleDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_ManageRestrictedJoinRuleDialog_section {
&:not(:first-child) {
margin-top: 24px;
}
> h3 {
margin: 0;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
.mx_ManageRestrictedJoinRuleDialog_entry {
display: flex;
margin-top: 12px;
> div {
flex-grow: 1;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 4px;
}
.mx_ManageRestrictedJoinRuleDialog_entry_name {
margin: 0 8px;
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mx_ManageRestrictedJoinRuleDialog_entry_description {
margin-top: 8px;
font-size: $font-12px;
line-height: $font-15px;
color: $tertiary-fg-color;
}
.mx_Checkbox {
align-items: center;
}
}
}
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_ManageRestrictedJoinRuleDialog_section_info {
position: relative;
border-radius: 8px;
margin: 12px 0;
padding: 8px 8px 8px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_ManageRestrictedJoinRuleDialog_footer {
margin-top: 20px;
.mx_ManageRestrictedJoinRuleDialog_footer_buttons {
display: flex;
width: max-content;
margin-left: auto;
.mx_AccessibleButton {
display: inline-block;
& + .mx_AccessibleButton {
margin-left: 24px;
}
}
}
}
}

View file

@ -27,7 +27,7 @@ limitations under the License.
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
border-radius: 3px; border-radius: 4px;
border: 1px solid $strong-input-border-color; border: 1px solid $strong-input-border-color;
font-size: $font-12px; font-size: $font-12px;
user-select: none; user-select: none;
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0px; padding: 0px;
border-radius: 3px; border-radius: 4px;
border: 1px solid $input-focused-border-color; border: 1px solid $input-focused-border-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
max-height: 200px; max-height: 200px;

View file

@ -30,5 +30,12 @@ limitations under the License.
mask-position: center; mask-position: center;
content: ''; content: '';
vertical-align: middle; vertical-align: middle;
}
.mx_InfoTooltip_icon_info::before {
mask-image: url('$(res)/img/element-icons/info.svg'); mask-image: url('$(res)/img/element-icons/info.svg');
} }
.mx_InfoTooltip_icon_warning::before {
mask-image: url('$(res)/img/element-icons/warning.svg');
}

View file

@ -19,8 +19,9 @@ limitations under the License.
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
margin-bottom: 8px; margin-bottom: 8px;
padding-left: 10px; padding: 0 10px;
border-left: 4px solid $button-bg-color; border-left: 2px solid $button-bg-color;
border-radius: 2px;
.mx_ReplyThread_show { .mx_ReplyThread_show {
cursor: pointer; cursor: pointer;

View file

@ -0,0 +1,162 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CallEvent {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: $dark-panel-bg-color;
border-radius: 8px;
margin: 10px auto;
max-width: 75%;
box-sizing: border-box;
height: 60px;
&.mx_CallEvent_voice {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
&.mx_CallEvent_video {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}
&.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-voice.svg');
}
&.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-video.svg');
}
.mx_CallEvent_info {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 12px;
.mx_CallEvent_info_basic {
display: flex;
flex-direction: column;
margin-left: 10px; // To match mx_CallEvent
.mx_CallEvent_sender {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.8rem;
margin-bottom: 3px;
}
.mx_CallEvent_type {
font-weight: 400;
color: $secondary-fg-color;
font-size: 1.2rem;
line-height: $font-13px;
display: flex;
align-items: center;
.mx_CallEvent_type_icon {
height: 13px;
width: 13px;
margin-right: 5px;
&::before {
content: '';
position: absolute;
height: 13px;
width: 13px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
}
}
}
}
.mx_CallEvent_content {
display: flex;
flex-direction: row;
align-items: center;
color: $secondary-fg-color;
margin-right: 16px;
.mx_CallEvent_content_button {
height: 24px;
padding: 0px 12px;
margin-left: 8px;
span {
padding: 8px 0;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
background-color: $button-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
margin-right: 8px;
}
}
}
.mx_CallEvent_content_button_reject span::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_CallEvent_content_tooltip {
margin-right: 5px;
}
.mx_CallEvent_iconButton {
display: inline-flex;
margin-right: 8px;
&::before {
content: '';
height: 16px;
width: 16px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.mx_CallEvent_silence::before {
mask-image: url('$(res)/img/voip/silence.svg');
}
.mx_CallEvent_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg');
}
}
}

View file

@ -60,12 +60,6 @@ limitations under the License.
} }
.mx_MFileBody_info { .mx_MFileBody_info {
background-color: $message-body-panel-bg-color;
border-radius: 12px;
width: 243px; // same width as a playable voice message, accounting for padding
padding: 6px 12px;
color: $message-body-panel-fg-color;
.mx_MFileBody_info_icon { .mx_MFileBody_info_icon {
background-color: $message-body-panel-icon-bg-color; background-color: $message-body-panel-icon-bg-color;
border-radius: 20px; border-radius: 20px;

View file

@ -16,23 +16,15 @@ limitations under the License.
$timelineImageBorderRadius: 4px; $timelineImageBorderRadius: 4px;
.mx_MImageBody {
display: block;
}
.mx_MImageBody_thumbnail { .mx_MImageBody_thumbnail {
position: absolute; object-fit: contain;
width: 100%;
height: 100%;
left: 0;
top: 0;
border-radius: $timelineImageBorderRadius; border-radius: $timelineImageBorderRadius;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
> canvas { > div > canvas {
border-radius: $timelineImageBorderRadius; border-radius: $timelineImageBorderRadius;
} }
} }

View file

@ -20,9 +20,11 @@ limitations under the License.
.mx_MediaBody { .mx_MediaBody {
background-color: $message-body-panel-bg-color; background-color: $message-body-panel-bg-color;
border-radius: 12px; border-radius: 12px;
max-width: 243px; // use max-width instead of width so it fits within right panels
color: $message-body-panel-fg-color; color: $message-body-panel-fg-color;
font-size: $font-14px; font-size: $font-14px;
line-height: $font-24px; line-height: $font-24px;
}
padding: 6px 12px;
}

View file

@ -107,3 +107,12 @@ limitations under the License.
.mx_MessageActionBar_cancelButton::after { .mx_MessageActionBar_cancelButton::after {
mask-image: url('$(res)/img/element-icons/trashcan.svg'); mask-image: url('$(res)/img/element-icons/trashcan.svg');
} }
.mx_MessageActionBar_downloadButton::after {
mask-size: 14px;
mask-image: url('$(res)/img/download.svg');
}
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
background-color: transparent; // hide the download icon mask
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
.mx_EventTile[data-layout=bubble], .mx_EventTile[data-layout=bubble],
.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { .mx_EventListSummary[data-layout=bubble] {
--avatarSize: 32px; --avatarSize: 32px;
--gutterSize: 11px; --gutterSize: 11px;
--cornerRadius: 12px; --cornerRadius: 12px;
@ -38,7 +38,8 @@ limitations under the License.
padding-top: 0; padding-top: 0;
} }
&:hover { &:hover,
&.mx_EventTile_selected {
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@ -80,7 +81,7 @@ limitations under the License.
.mx_MessageActionBar { .mx_MessageActionBar {
right: 0; right: 0;
transform: translate3d(50%, 50%, 0); transform: translate3d(90%, 50%, 0);
} }
--backgroundColor: $eventbubble-others-bg; --backgroundColor: $eventbubble-others-bg;
@ -91,12 +92,17 @@ limitations under the License.
float: right; float: right;
> a { > a {
left: auto; left: auto;
right: -48px; right: -68px;
} }
} }
.mx_SenderProfile { .mx_SenderProfile {
display: none; display: none;
} }
.mx_ReplyTile .mx_SenderProfile {
display: block;
}
.mx_ReactionsRow { .mx_ReactionsRow {
float: right; float: right;
clear: right; clear: right;
@ -126,7 +132,9 @@ limitations under the License.
margin: 0 -12px 0 -9px; margin: 0 -12px 0 -9px;
> a { > a {
position: absolute; position: absolute;
left: -48px; padding: 10px 20px;
top: 0;
left: -68px;
} }
} }
@ -148,12 +156,24 @@ limitations under the License.
position: absolute; position: absolute;
top: 0; top: 0;
line-height: 1; line-height: 1;
z-index: 9;
img { img {
box-shadow: 0 0 0 3px $eventbubble-avatar-outline; box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
border-radius: 50%; border-radius: 50%;
} }
} }
&.mx_EventTile_noSender {
.mx_EventTile_avatar {
top: -19px;
}
}
.mx_BaseAvatar,
.mx_EventTile_avatar {
line-height: 1;
}
&[data-has-reply=true] { &[data-has-reply=true] {
> .mx_EventTile_line { > .mx_EventTile_line {
flex-direction: column; flex-direction: column;
@ -204,89 +224,6 @@ limitations under the License.
border-left-color: $eventbubble-reply-color; 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 */ /* Special layout scenario for "Unable To Decrypt (UTD)" events */
&.mx_EventTile_bad > .mx_EventTile_line { &.mx_EventTile_bad > .mx_EventTile_line {
display: grid; display: grid;
@ -321,3 +258,78 @@ limitations under the License.
max-width: 100%; max-width: 100%;
} }
} }
.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble],
.mx_EventTile.mx_EventTile_info[data-layout=bubble],
.mx_EventListSummary[data-layout=bubble][data-expanded=false] {
--backgroundColor: transparent;
--gutterSize: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px 0;
.mx_EventTile_avatar {
position: static;
order: -1;
margin-right: 5px;
}
}
.mx_EventListSummary[data-layout=bubble] {
--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;
margin-right: 55px;
}
.mx_EventListSummary_avatars {
padding-top: 0;
}
&::after {
content: "";
clear: both;
}
.mx_EventTile {
margin: 0 6px;
padding: 2px 0;
}
.mx_EventTile_line {
margin: 0 5px;
> a {
left: auto;
right: 0;
transform: translateX(calc(100% + 5px));
}
}
.mx_MessageActionBar {
transform: translate3d(90%, 0, 0);
}
}
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
padding: 0 34px;
}
/* events that do not require bubble layout */
.mx_EventListSummary[data-layout=bubble],
.mx_EventTile.mx_EventTile_bad[data-layout=bubble] {
.mx_EventTile_line {
background: transparent;
}
&:hover {
&::before {
background: transparent;
}
}
}

View file

@ -132,10 +132,15 @@ $hover-select-border: 4px;
} }
} }
&.mx_EventTile_info .mx_EventTile_line { &.mx_EventTile_info .mx_EventTile_line,
& ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line {
padding-left: calc($left-gutter + 18px); padding-left: calc($left-gutter + 18px);
} }
& ~ .mx_EventListSummary .mx_EventTile_line {
padding-left: calc($left-gutter);
}
&.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);
} }
@ -208,43 +213,11 @@ $hover-select-border: 4px;
text-decoration: none; text-decoration: none;
} }
/* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
horizontal scrollbars occasionally appearing, like in
https://github.com/vector-im/vector-web/issues/1154
*/
.mx_EventTile_content {
display: block;
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
}
/* De-zalgoing */ /* De-zalgoing */
.mx_EventTile_body { .mx_EventTile_body {
overflow-y: hidden; overflow-y: hidden;
} }
/* Spoiler stuff */
.mx_EventTile_spoiler {
cursor: pointer;
}
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
&:hover.mx_EventTile_verified .mx_EventTile_line, &:hover.mx_EventTile_verified .mx_EventTile_line,
&:hover.mx_EventTile_unverified .mx_EventTile_line, &:hover.mx_EventTile_unverified .mx_EventTile_line,
&:hover.mx_EventTile_unknown .mx_EventTile_line { &:hover.mx_EventTile_unknown .mx_EventTile_line {
@ -307,6 +280,36 @@ $hover-select-border: 4px;
} }
} }
/* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
horizontal scrollbars occasionally appearing, like in
https://github.com/vector-im/vector-web/issues/1154 */
.mx_EventTile_content {
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
}
/* Spoiler stuff */
.mx_EventTile_spoiler {
cursor: pointer;
}
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
.mx_RoomView_timeline_rr_enabled { .mx_RoomView_timeline_rr_enabled {
.mx_EventTile:not([data-layout=bubble]) { .mx_EventTile:not([data-layout=bubble]) {
@ -333,6 +336,13 @@ $hover-select-border: 4px;
.mx_EventTile_msgOption { .mx_EventTile_msgOption {
grid-column: 2; grid-column: 2;
} }
&:hover {
.mx_EventTile_line {
// To avoid bubble events being highlighted
background-color: inherit !important;
}
}
} }
.mx_EventTile_readAvatars { .mx_EventTile_readAvatars {
@ -462,6 +472,10 @@ $hover-select-border: 4px;
background-color: $header-panel-bg-color; background-color: $header-panel-bg-color;
} }
pre code > * {
display: inline-block;
}
pre { pre {
// have to use overlay rather than auto otherwise Linux and Windows // have to use overlay rather than auto otherwise Linux and Windows
// Chrome gets very confused about vertical spacing: // Chrome gets very confused about vertical spacing:
@ -559,6 +573,12 @@ $hover-select-border: 4px;
color: $accent-color-alt; color: $accent-color-alt;
} }
.mx_EventTile_content .markdown-body blockquote {
border-left: 2px solid $blockquote-bar-color;
border-radius: 2px;
padding: 0 10px;
}
.mx_EventTile_content .markdown-body .hljs { .mx_EventTile_content .markdown-body .hljs {
display: inline !important; display: inline !important;
} }

View file

@ -26,6 +26,7 @@ $left-gutter: 64px;
> .mx_EventTile_avatar { > .mx_EventTile_avatar {
position: absolute; position: absolute;
z-index: 9;
} }
.mx_MessageTimestamp { .mx_MessageTimestamp {

View file

@ -116,6 +116,11 @@ $irc-line-height: $font-18px;
.mx_EditMessageComposer_buttons { .mx_EditMessageComposer_buttons {
position: relative; position: relative;
} }
.mx_ReactionsRow {
padding-left: 0;
padding-right: 0;
}
} }
.mx_EventTile_emote { .mx_EventTile_emote {

View file

@ -19,7 +19,8 @@ limitations under the License.
margin-right: 15px; margin-right: 15px;
margin-bottom: 15px; margin-bottom: 15px;
display: flex; display: flex;
border-left: 4px solid $preview-widget-bar-color; border-left: 2px solid $preview-widget-bar-color;
border-radius: 2px;
color: $preview-widget-fg-color; color: $preview-widget-fg-color;
} }
@ -33,7 +34,7 @@ limitations under the License.
.mx_LinkPreviewWidget_caption { .mx_LinkPreviewWidget_caption {
margin-left: 15px; margin-left: 15px;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: hidden; // cause it to wrap rather than clip overflow: hidden; // cause it to wrap rather than clip
} }
.mx_LinkPreviewWidget_title { .mx_LinkPreviewWidget_title {

View file

@ -60,8 +60,6 @@ limitations under the License.
$reply-lines: 2; $reply-lines: 2;
$line-height: $font-22px; $line-height: $font-22px;
pointer-events: none;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View file

@ -29,8 +29,10 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// min-height at this level so the mx_BasicMessageComposer_input // min-height at this level so the mx_BasicMessageComposer_input
// still stays vertically centered when less than 50px // still stays vertically centered when less than 55px.
min-height: 50px; // We also set this to ensure the voice message recording widget
// doesn't cause a jump.
min-height: 55px;
.mx_BasicMessageComposer_input { .mx_BasicMessageComposer_input {
padding: 3px 0; padding: 3px 0;

View file

@ -47,14 +47,14 @@ limitations under the License.
color: $settings-subsection-fg-color; color: $settings-subsection-fg-color;
font-size: $font-14px; font-size: $font-14px;
display: block; display: block;
margin: 10px 100px 10px 0; // Align with the rest of the view margin: 10px 80px 10px 0; // Align with the rest of the view
} }
.mx_SettingsTab_section { .mx_SettingsTab_section {
margin-bottom: 24px; margin-bottom: 24px;
.mx_SettingsFlag { .mx_SettingsFlag {
margin-right: 100px; margin-right: 80px;
margin-bottom: 10px; margin-bottom: 10px;
} }

View file

@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SecurityRoomSettingsTab {
.mx_SettingsTab_showAdvanced {
padding: 0;
margin-bottom: 16px;
}
.mx_SecurityRoomSettingsTab_spacesWithAccess {
> h4 {
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
text-transform: uppercase;
}
> span {
font-weight: 500;
font-size: $font-14px;
line-height: 32px; // matches height of avatar for v-align
color: $secondary-fg-color;
display: inline-block;
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_BaseAvatar {
margin-right: 8px;
}
& + span {
margin-left: 16px;
}
}
}
}
.mx_SecurityRoomSettingsTab_warning { .mx_SecurityRoomSettingsTab_warning {
display: block; display: block;
@ -26,5 +64,51 @@ limitations under the License.
} }
.mx_SecurityRoomSettingsTab_encryptionSection { .mx_SecurityRoomSettingsTab_encryptionSection {
margin-bottom: 25px; padding-bottom: 24px;
border-bottom: 1px solid $menu-border-color;
margin-bottom: 32px;
}
.mx_SecurityRoomSettingsTab_upgradeRequired {
margin-left: 16px;
padding: 4px 16px;
border: 1px solid $accent-color;
border-radius: 8px;
color: $accent-color;
font-size: $font-12px;
line-height: $font-15px;
}
.mx_SecurityRoomSettingsTab_joinRule {
.mx_RadioButton {
padding-top: 16px;
margin-bottom: 8px;
.mx_RadioButton_content {
margin-left: 14px;
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
display: block;
}
}
> span {
display: inline-block;
margin-left: 34px;
margin-bottom: 16px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
& + .mx_RadioButton {
border-top: 1px solid $menu-border-color;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
} }

View file

@ -15,8 +15,7 @@ limitations under the License.
*/ */
.mx_AppearanceUserSettingsTab_fontSlider, .mx_AppearanceUserSettingsTab_fontSlider,
.mx_AppearanceUserSettingsTab_fontSlider_preview, .mx_AppearanceUserSettingsTab_fontSlider_preview {
.mx_AppearanceUserSettingsTab_Layout {
@mixin mx_Settings_fullWidthField; @mixin mx_Settings_fullWidthField;
} }
@ -45,6 +44,11 @@ limitations under the License.
border-radius: 10px; border-radius: 10px;
padding: 0 16px 9px 16px; padding: 0 16px 9px 16px;
pointer-events: none; pointer-events: none;
display: flow-root;
.mx_EventTile[data-layout=bubble] {
margin-top: 30px;
}
.mx_EventTile_msgOption { .mx_EventTile_msgOption {
display: none; display: none;
@ -154,13 +158,10 @@ limitations under the License.
.mx_AppearanceUserSettingsTab_Layout_RadioButtons { .mx_AppearanceUserSettingsTab_Layout_RadioButtons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 24px;
color: $primary-fg-color; color: $primary-fg-color;
.mx_AppearanceUserSettingsTab_spacer {
width: 24px;
}
> .mx_AppearanceUserSettingsTab_Layout_RadioButton { > .mx_AppearanceUserSettingsTab_Layout_RadioButton {
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
@ -210,6 +211,21 @@ limitations under the License.
.mx_RadioButton_checked { .mx_RadioButton_checked {
background-color: rgba($accent-color, 0.08); background-color: rgba($accent-color, 0.08);
} }
.mx_EventTile {
margin: 0;
&[data-layout=bubble] {
margin-right: 40px;
}
&[data-layout=irc] {
> a {
display: none;
}
}
.mx_EventTile_line {
max-width: 90%;
}
}
} }
.mx_AppearanceUserSettingsTab_Advanced { .mx_AppearanceUserSettingsTab_Advanced {

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.9806 4.5101C6.9306 3.9401 7.3506 3.4401 7.9206 3.4001C8.4806 3.3601 8.9806 3.7801 9.0406 4.3501V4.5101L8.7206 8.5101C8.6906 8.8801 8.3806 9.1601 8.0106 9.1601H7.9506C7.6006 9.1301 7.3306 8.8601 7.3006 8.5101L6.9806 4.5101ZM8.88012 11.1202C8.88012 11.6062 8.48613 12.0002 8.00012 12.0002C7.51411 12.0002 7.12012 11.6062 7.12012 11.1202C7.12012 10.6342 7.51411 10.2402 8.00012 10.2402C8.48613 10.2402 8.88012 10.6342 8.88012 11.1202Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View file

@ -1,7 +0,0 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
<g style="stroke:#454545;stroke-width:.8;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)">
<circle cx="5" cy="5" r="5"/>
<path d="m0 5h10"/>
<path d="m5 0c1.25064019 1.36917645 1.96137638 3.14601693 2 5-.03862362 1.85398307-.74935981 3.63082355-2 5-1.25064019-1.36917645-1.96137638-3.14601693-2-5 .03862362-1.85398307.74935981-3.63082355 2-5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 524 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.81815C0 3.76379 0.89543 2.90906 2 2.90906H9.33333C10.4379 2.90906 11.3333 3.76379 11.3333 4.81815V11.1818C11.3333 12.2361 10.4379 13.0909 9.33333 13.0909H2C0.895429 13.0909 0 12.2361 0 11.1818V4.81815ZM12.6667 6.09089L14.9169 4.37255C15.3534 4.03921 16 4.33587 16 4.86947V11.1305C16 11.6641 15.3534 11.9607 14.9169 11.6274L12.6667 9.90907V6.09089ZM3.68584 8.54792C3.68584 8.82819 3.45653 9.05751 3.17625 9.05751C2.89598 9.05751 2.66667 8.82819 2.66667 8.54792V6.50957C2.66667 6.22929 2.89598 5.99998 3.17625 5.99998H5.2146C5.49488 5.99998 5.72419 6.22929 5.72419 6.50957C5.72419 6.78984 5.49488 7.01916 5.2146 7.01916H4.39926L6.2083 8.82819L8.73076 6.30573C8.9295 6.10699 9.25054 6.10699 9.44928 6.30573C9.64802 6.50447 9.64802 6.82551 9.44928 7.02425L6.56501 9.90852C6.36627 10.1073 6.04523 10.1073 5.84649 9.90852L3.68584 7.74787V8.54792Z" fill="#8D97A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1,016 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.00016 6C4.36683 6 4.66683 5.7 4.66683 5.33333V4.28667L7.4935 7.11333C7.7535 7.37333 8.1735 7.37333 8.4335 7.11333L12.2068 3.34C12.4668 3.08 12.4668 2.66 12.2068 2.4C11.9468 2.14 11.5268 2.14 11.2668 2.4L7.96683 5.7L5.60016 3.33333H6.66683C7.0335 3.33333 7.3335 3.03333 7.3335 2.66667C7.3335 2.3 7.0335 2 6.66683 2H4.00016C3.6335 2 3.3335 2.3 3.3335 2.66667V5.33333C3.3335 5.7 3.6335 6 4.00016 6Z" fill="#8D97A5"/>
<path d="M8.00557 8.67107C6.88076 8.62784 4.56757 8.91974 4.0052 9.06763C3.97195 9.07638 3.93363 9.08616 3.89078 9.0971C3.02734 9.31746 0.321813 10.008 0.0294949 12.1958C-0.196977 13.8909 0.937169 14.4039 1.50412 14.3258C1.89653 14.2766 3.02006 14.0989 4.05816 13.9127C5.07753 13.7298 5.07701 13.0573 5.07666 12.6026C5.07665 12.5943 5.07664 12.586 5.07664 12.5778L5.07665 11.6636C5.07665 11.4308 5.29543 11.2962 5.5972 11.2598C6.66548 11.1147 7.5573 11.1143 8.00369 11.1143L8.00745 11.1143C8.45377 11.1143 9.33453 11.1147 10.4028 11.2598C10.7046 11.2962 10.9234 11.4308 10.9234 11.6636L10.9234 12.5778C10.9234 12.586 10.9233 12.5943 10.9233 12.6026C10.923 13.0573 10.9225 13.7298 11.9418 13.9127C12.9799 14.099 14.1035 14.2766 14.4959 14.3258C15.0628 14.4039 16.197 13.8909 15.9705 12.1958C15.6782 10.008 12.9727 9.31747 12.1092 9.0971C12.0664 9.08617 12.0281 9.07639 11.9948 9.06764C11.4324 8.91975 9.13037 8.62783 8.00557 8.67107Z" fill="#8D97A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049; // "Dark Tile" $message-body-panel-bg-color: #394049; // "Dark Tile"
$message-body-panel-icon-fg-color: #21262C; // "Separator" $message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $tertiary-fg-color; $message-body-panel-icon-bg-color: #21262C; // "System Dark"
$voice-record-stop-border-color: $quaternary-fg-color; $voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD;
.hljs-tag { .hljs-tag {
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
} }
.hljs-addition {
background: #1a4b59;
}
.hljs-deletion {
background: #53232a;
}

View file

@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049; $message-body-panel-bg-color: #394049;
$message-body-panel-icon-fg-color: $primary-bg-color; $message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $secondary-fg-color; $message-body-panel-icon-bg-color: #21262C;
// See non-legacy dark for variable information // See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882; $voice-record-stop-border-color: #6F7882;

View file

@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0; $message-body-panel-bg-color: #E3E8F0;
$message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color; $message-body-panel-icon-bg-color: #F4F6FA;
// See non-legacy _light for variable information // See non-legacy _light for variable information
$voice-record-stop-symbol-color: #ff4b55; $voice-record-stop-symbol-color: #ff4b55;

View file

@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0; // "Separator" $message-body-panel-bg-color: #E3E8F0; // "Separator"
$message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color; $message-body-panel-icon-bg-color: #F4F6FA;
// These two don't change between themes. They are the $warning-color, but we don't // These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident. // want custom themes to affect them by accident.

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { JSXElementConstructor } from "react"; import React, { JSXElementConstructor } from "react";
// Based on https://stackoverflow.com/a/53229857/3532235 // Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never}; export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
@ -22,3 +22,4 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
export type Writeable<T> = { -readonly [P in keyof T]: T[P] }; export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
export type ReactAnyComponent = React.Component | React.ExoticComponent;

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>;
@ -90,6 +92,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxOnRecaptchaLoaded?: () => void;
} }
interface Document { interface Document {
@ -114,7 +117,7 @@ declare global {
} }
interface StorageEstimate { interface StorageEstimate {
usageDetails?: {[key: string]: number}; usageDetails?: { [key: string]: number };
} }
interface HTMLAudioElement { interface HTMLAudioElement {
@ -185,4 +188,21 @@ declare global {
parameterDescriptors?: AudioParamDescriptor[]; parameterDescriptors?: AudioParamDescriptor[];
} }
); );
// eslint-disable-next-line no-var
var grecaptcha:
| undefined
| {
reset: (id: string) => void;
render: (
divId: string,
options: {
sitekey: string;
callback: (response: string) => void;
},
) => string;
isReady: () => boolean;
};
} }
/* eslint-enable @typescript-eslint/naming-convention */

20
src/@types/svg.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
declare module "*.svg" {
const path: string;
export default path;
}

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,
@ -395,17 +395,17 @@ export class Analytics {
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'), title: _t('Analytics'),
description: <div className="mx_AnalyticsModal"> description: <div className="mx_AnalyticsModal">
<div>{_t('The information being sent to us to help make %(brand)s better includes:', { <div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
brand: SdkConfig.get().brand, brand: SdkConfig.get().brand,
})}</div> }) }</div>
<table> <table>
{ rows.map((row) => <tr key={row[0]}> { rows.map((row) => <tr key={row[0]}>
<td>{_t( <td>{ _t(
customVariables[row[0]].expl, customVariables[row[0]].expl,
customVariables[row[0]].getTextVariables ? customVariables[row[0]].getTextVariables ?
customVariables[row[0]].getTextVariables() : customVariables[row[0]].getTextVariables() :
null, null,
)}</td> ) }</td>
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> } { row[1] !== undefined && <td><code>{ row[1] }</code></td> }
</tr>) } </tr>) }
{ otherVariables.map((item, index) => { otherVariables.map((item, index) =>

View file

@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
// (and store the ID of their native room) // (and store the ID of their native room)
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
export enum AudioID { enum AudioID {
Ring = 'ringAudio', Ring = 'ringAudio',
Ringback = 'ringbackAudio', Ringback = 'ringbackAudio',
CallEnd = 'callendAudio', CallEnd = 'callendAudio',
@ -142,6 +142,7 @@ export enum PlaceCallType {
export enum CallHandlerEvent { export enum CallHandlerEvent {
CallsChanged = "calls_changed", CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room", CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
} }
export default class CallHandler extends EventEmitter { export default class CallHandler extends EventEmitter {
@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter {
// do the async lookup when we get new information and then store these mappings here // do the async lookup when we get new information and then store these mappings here
private assertedIdentityNativeUsers = new Map<string, string>(); private assertedIdentityNativeUsers = new Map<string, string>();
private silencedCalls = new Set<string>(); // callIds
static sharedInstance() { static sharedInstance() {
if (!window.mxCallHandler) { if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler(); window.mxCallHandler = new CallHandler();
@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter {
} }
} }
public silenceCall(callId: string) {
this.silencedCalls.add(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
// Don't pause audio if we have calls which are still ringing
if (this.areAnyCallsUnsilenced()) return;
this.pause(AudioID.Ring);
}
public unSilenceCall(callId: string) {
this.silencedCalls.delete(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
this.play(AudioID.Ring);
}
public isCallSilenced(callId: string): boolean {
return this.silencedCalls.has(callId);
}
/**
* Returns true if there is at least one unsilenced call
* @returns {boolean}
*/
private areAnyCallsUnsilenced(): boolean {
return this.calls.size > this.silencedCalls.size;
}
private async checkProtocols(maxTries) { private async checkProtocols(maxTries) {
try { try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter {
}, true); }, true);
}; };
public getCallById(callId: string): MatrixCall {
for (const call of this.calls.values()) {
if (call.callId === callId) return call;
}
return null;
}
getCallForRoom(roomId: string): MatrixCall { getCallForRoom(roomId: string): MatrixCall {
return this.calls.get(roomId) || null; return this.calls.get(roomId) || null;
} }
@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter {
break; break;
} }
if (newState !== CallState.Ringing) {
this.silencedCalls.delete(call.callId);
}
switch (newState) { switch (newState) {
case CallState.Ringing: case CallState.Ringing:
this.play(AudioID.Ring); this.play(AudioID.Ring);
@ -450,28 +491,18 @@ export default class CallHandler extends EventEmitter {
break; break;
case CallState.Ended: case CallState.Ended:
{ {
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); const hangupReason = call.hangupReason;
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
this.removeCallForRoom(mappedRoomId); this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && ( if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
)) {
this.play(AudioID.Busy); this.play(AudioID.Busy);
let title; let title;
let description; let description;
if (call.hangupReason === CallErrorCode.UserHangup) { // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
title = _t("Call Declined"); if (call.hangupReason === CallErrorCode.UserBusy) {
description = _t("The other party declined the call.");
} else if (call.hangupReason === CallErrorCode.UserBusy) {
title = _t("User Busy"); title = _t("User Busy");
description = _t("The user you called is busy."); description = _t("The user you called is busy.");
} else if (call.hangupReason === CallErrorCode.InviteTimeout) { } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
title = _t("Call Failed");
// XXX: full stop appended as some relic here, but these
// strings need proper input from design anyway, so let's
// not change this string until we have a proper one.
description = _t('The remote side failed to pick up') + '.';
} else {
title = _t("Call Failed"); title = _t("Call Failed");
description = _t("The call could not be established"); description = _t("The call could not be established");
} }
@ -480,7 +511,7 @@ export default class CallHandler extends EventEmitter {
title, description, title, description,
}); });
} else if ( } else if (
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
) { ) {
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title: _t("Answered Elsewhere"), title: _t("Answered Elsewhere"),
@ -615,23 +646,23 @@ export default class CallHandler extends EventEmitter {
private showICEFallbackPrompt() { private showICEFallbackPrompt() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const code = sub => <code>{sub}</code>; const code = sub => <code>{ sub }</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
title: _t("Call failed due to misconfigured server"), title: _t("Call failed due to misconfigured server"),
description: <div> description: <div>
<p>{_t( <p>{ _t(
"Please ask the administrator of your homeserver " + "Please ask the administrator of your homeserver " +
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " + "(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
"order for calls to work reliably.", "order for calls to work reliably.",
{ homeserverDomain: cli.getDomain() }, { code }, { homeserverDomain: cli.getDomain() }, { code },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Alternatively, you can try to use the public server at " + "Alternatively, you can try to use the public server at " +
"<code>turn.matrix.org</code>, but this will not be as reliable, and " + "<code>turn.matrix.org</code>, but this will not be as reliable, and " +
"it will share your IP address with that server. You can also manage " + "it will share your IP address with that server. You can also manage " +
"this in Settings.", "this in Settings.",
null, { code }, null, { code },
)}</p> ) }</p>
</div>, </div>,
button: _t('Try using turn.matrix.org'), button: _t('Try using turn.matrix.org'),
cancelButton: _t('OK'), cancelButton: _t('OK'),
@ -649,19 +680,19 @@ export default class CallHandler extends EventEmitter {
if (call.type === CallType.Voice) { if (call.type === CallType.Voice) {
title = _t("Unable to access microphone"); title = _t("Unable to access microphone");
description = <div> description = <div>
{_t( { _t(
"Call failed because microphone could not be accessed. " + "Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.", "Check that a microphone is plugged in and set up correctly.",
)} ) }
</div>; </div>;
} else if (call.type === CallType.Video) { } else if (call.type === CallType.Video) {
title = _t("Unable to access webcam / microphone"); title = _t("Unable to access webcam / microphone");
description = <div> description = <div>
{_t("Call failed because webcam or microphone could not be accessed. Check that:")} { _t("Call failed because webcam or microphone could not be accessed. Check that:") }
<ul> <ul>
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li> <li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
<li>{_t("Permission is granted to use the webcam")}</li> <li>{ _t("Permission is granted to use the webcam") }</li>
<li>{_t("No other application is using the webcam")}</li> <li>{ _t("No other application is using the webcam") }</li>
</ul> </ul>
</div>; </div>;
} }

View file

@ -425,10 +425,10 @@ export default class ContentMessages {
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'), title: _t('Replying With Files'),
description: ( description: (
<div>{_t( <div>{ _t(
'At this time it is not possible to reply with a file. ' + 'At this time it is not possible to reply with a file. ' +
'Would you like to upload this file without replying?', 'Would you like to upload this file without replying?',
)}</div> ) }</div>
), ),
hasCancelButton: true, hasCancelButton: true,
button: _t("Continue"), button: _t("Continue"),

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

@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import linkifyMatrix from './linkify-matrix'; import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import { getEmojiFromUnicode } from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread"; import ReplyThread from "./components/views/elements/ReplyThread";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
@ -57,7 +57,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
* @return {String} The shortcode (such as :thumbup:) * @return {String} The shortcode (such as :thumbup:)
*/ */
export function unicodeToShortcode(char: string): string { export function unicodeToShortcode(char: string): string {
const data = getEmojiFromUnicode(char); const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); return shortcodes?.length ? `:${shortcodes[0]}:` : '';
}
/**
* Returns the unicode character for an emoji shortcode
*
* @param {String} shortcode The shortcode (such as :thumbup:)
* @return {String} The emoji character; null if none exists
*/
export function shortcodeToUnicode(shortcode: string): string {
shortcode = shortcode.slice(1, shortcode.length - 1);
const data = SHORTCODE_TO_EMOJI.get(shortcode);
return data ? data.unicode : null;
} }
export function processHtmlForSending(html: string): string { export function processHtmlForSending(html: string): string {

View file

@ -149,17 +149,17 @@ export default class IdentityAuthClient {
title: _t("Identity server has no terms of service"), title: _t("Identity server has no terms of service"),
description: ( description: (
<div> <div>
<p>{_t( <p>{ _t(
"This action requires accessing the default identity server " + "This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " + "<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.", {}, "but the server does not have any terms of service.", {},
{ {
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>, server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
}, },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Only continue if you trust the owner of the server.", "Only continue if you trust the owner of the server.",
)}</p> ) }</p>
</div> </div>
), ),
button: _t("Trust"), button: _t("Trust"),

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

@ -378,7 +378,7 @@ export class ModalManager {
const dialog = ( const dialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{modal.elem} { modal.elem }
</div> </div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} /> <div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div> </div>

View file

@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
description: _t("Use your account or create a new one to continue."), description: _t("Use your account or create a new one to continue."),
button: _t("Create Account"), button: _t("Create Account"),
extraButtons: [ extraButtons: [
<button key="start_login" onClick={() => { <button
modal.close(); key="start_login"
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after }); onClick={() => {
}}>{ _t('Sign In') }</button>, modal.close();
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
}}
>
{ _t('Sign In') }
</button>,
], ],
onFinished: (proceed) => { onFinished: (proceed) => {
if (proceed) { if (proceed) {

View file

@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils'; import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
import { inviteUsersToRoom } from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType"; import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi"; import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects"; import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler"; import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms"; import { guessAndSetDMRoom } from "./Rooms";
import { upgradeRoom } from './utils/RoomUpgrade';
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
import ErrorDialog from './components/views/dialogs/ErrorDialog'; import ErrorDialog from './components/views/dialogs/ErrorDialog';
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
@ -277,50 +277,8 @@ export const Commands = [
/*isPriority=*/false, /*isStatic=*/true); /*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(async ([resp]) => { return success(finished.then(async ([resp]) => {
if (!resp.continue) return; if (!resp?.continue) return;
await upgradeRoom(room, args, resp.invite);
let checkForUpgradeFn;
try {
const upgradePromise = cli.upgradeRoom(roomId, args);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (resp.invite) {
checkForUpgradeFn = async (newRoom) => {
// The upgradePromise should be done by the time we await it here.
const { replacement_room: newRoomId } = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
await upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
'Double check that your server supports the room version chosen and try again.'),
});
}
})); }));
} }
return reject(this.getUsage()); return reject(this.getUsage());
@ -480,14 +438,14 @@ export const Commands = [
'Identity server', 'Identity server',
QuestionDialog, { QuestionDialog, {
title: _t("Use an identity server"), title: _t("Use an identity server"),
description: <p>{_t( description: <p>{ _t(
"Use an identity server to invite by email. " + "Use an identity server to invite by email. " +
"Click continue to use the default identity server " + "Click continue to use the default identity server " +
"(%(defaultIdentityServerName)s) or manage in Settings.", "(%(defaultIdentityServerName)s) or manage in Settings.",
{ {
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
}, },
)}</p>, ) }</p>,
button: _t("Continue"), button: _t("Continue"),
}, },
); );
@ -522,7 +480,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 +1027,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

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import * as Roles from './Roles'; import * as Roles from './Roles';
import { isValid3pidInvite } from "./RoomInvite"; import { isValid3pidInvite } from "./RoomInvite";
@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
}); });
} }
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
};
}
function textForCallHangupEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
let getReason = () => "";
if (!MatrixClientPeg.get().supportsVoip()) {
getReason = () => _t('(not supported by this browser)');
} else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all
getReason = () => _t('(could not connect media)');
} else if (eventContent.reason === "ice_timeout") {
// We established a connection but it died
getReason = () => _t('(connection failed)');
} else if (eventContent.reason === "user_media_failed") {
// The other side couldn't open capture devices
getReason = () => _t("(their device couldn't start the camera / microphone)");
} else if (eventContent.reason === "unknown_error") {
// An error code the other side doesn't have a way to express
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
getReason = () => _t("(an error occurred)");
} else if (eventContent.reason === "invite_timeout") {
getReason = () => _t('(no answer)');
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
getReason = () => '';
} else {
getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
}
}
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
}
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone');
return _t('%(senderName)s declined the call.', { senderName });
};
}
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
isVoice = false;
}
const isSupported = MatrixClientPeg.get().supportsVoip();
// This ladder could be reduced down to a couple string variables, however other languages
// can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) {
return () => _t("%(senderName)s placed a voice call.", {
senderName: getSenderName(),
});
} else if (isVoice && !isSupported) {
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
senderName: getSenderName(),
});
} else if (!isVoice && isSupported) {
return () => _t("%(senderName)s placed a video call.", {
senderName: getSenderName(),
});
} else if (!isVoice && !isSupported) {
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
senderName: getSenderName(),
});
}
}
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null { function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
@ -652,10 +567,6 @@ interface IHandlers {
const handlers: IHandlers = { const handlers: IHandlers = {
'm.room.message': textForMessageEvent, 'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent,
'm.call.reject': textForCallRejectEvent,
}; };
const stateHandlers: IHandlers = { const stateHandlers: IHandlers = {

View file

@ -14,35 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import PropTypes from "prop-types";
const emailRegex = /^\S+@\S+\.\S+$/; const emailRegex = /^\S+@\S+\.\S+$/;
const mxUserIdRegex = /^@\S+:\S+$/; const mxUserIdRegex = /^@\S+:\S+$/;
const mxRoomIdRegex = /^!\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/;
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
export enum AddressType { export enum AddressType {
Email = "email", Email = "email",
MatrixUserId = "mx-user-id", MatrixUserId = "mx-user-id",
MatrixRoomId = "mx-room-id", MatrixRoomId = "mx-room-id",
} }
export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId];
// PropType definition for an object describing // PropType definition for an object describing
// an address that can be invited to a room (which // an address that can be invited to a room (which
// could be a third party identifier or a matrix ID) // could be a third party identifier or a matrix ID)
// along with some additional information about the // along with some additional information about the
// address / target. // address / target.
export const UserAddressType = PropTypes.shape({ export interface IUserAddress {
addressType: PropTypes.oneOf(addressTypes).isRequired, addressType: AddressType;
address: PropTypes.string.isRequired, address: string;
displayName: PropTypes.string, displayName?: string;
avatarMxc: PropTypes.string, avatarMxc?: string;
// true if the address is known to be a valid address (eg. is a real // true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the // user we've seen) or false otherwise (eg. is just an address the
// user has entered) // user has entered)
isKnown: PropTypes.bool, isKnown?: boolean;
}); }
export function getAddressType(inputText: string): AddressType | null { export function getAddressType(inputText: string): AddressType | null {
if (emailRegex.test(inputText)) { if (emailRegex.test(inputText)) {

View file

@ -370,8 +370,8 @@ export const toggleDialog = () => {
const sections = categoryOrder.map(category => { const sections = categoryOrder.map(category => {
const list = shortcuts[category]; const list = shortcuts[category];
return <div className="mx_KeyboardShortcutsDialog_category" key={category}> return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
<h3>{_t(category)}</h3> <h3>{ _t(category) }</h3>
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div> <div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
</div>; </div>;
}); });

View file

@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
}; };
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}> return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar"> { ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{ children } { children }
</div>} </div> }
</RovingTabIndexProvider>; </RovingTabIndexProvider>;
}; };

View file

@ -15,8 +15,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import * as sdk from '../../../../index';
import PropTypes from 'prop-types'; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { SettingLevel } from "../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../settings/SettingLevel";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
disabling: boolean;
}
/* /*
* Allows the user to disable the Event Index. * Allows the user to disable the Event Index.
*/ */
export default class DisableEventIndexDialog extends React.Component { export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
super(props); super(props);
this.state = { this.state = {
disabling: false, disabling: false,
}; };
} }
_onDisable = async () => { private onDisable = async (): Promise<void> => {
this.setState({ this.setState({
disabling: true, disabling: true,
}); });
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex(); await EventIndexPeg.deleteEventIndex();
this.props.onFinished(); this.props.onFinished(true);
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
} };
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
public render(): React.ReactNode {
return ( return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}> <BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
{_t("If disabled, messages from encrypted rooms won't appear in search results.")} { _t("If disabled, messages from encrypted rooms won't appear in search results.") }
{this.state.disabling ? <Spinner /> : <div />} { this.state.disabling ? <Spinner /> : <div /> }
<DialogButtons <DialogButtons
primaryButton={_t('Disable')} primaryButton={_t('Disable')}
onPrimaryButtonClick={this._onDisable} onPrimaryButtonClick={this.onDisable}
primaryButtonClass="danger" primaryButtonClass="danger"
cancelButtonClass="warning" cancelButtonClass="warning"
onCancel={this.props.onFinished} onCancel={this.props.onFinished}

View file

@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
} }
private onDisable = async () => { private onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search", const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
import("./DisableEventIndexDialog"), Modal.createTrackedDialog("Disable message search", "Disable message search",
DisableEventIndexDialog,
null, null, /* priority = */ false, /* static = */ true, null, null, /* priority = */ false, /* static = */ true,
); );
}; };
@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
const eventIndexingSettings = ( const eventIndexingSettings = (
<div> <div>
{_t( { _t(
"%(brand)s is securely caching encrypted messages locally for them " + "%(brand)s is securely caching encrypted messages locally for them " +
"to appear in search results:", "to appear in search results:",
{ brand }, { brand },
)} ) }
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{crawlerState}<br /> { crawlerState }<br />
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br /> { _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br /> { _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { { _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
doneRooms: formatCountLong(doneRooms), doneRooms: formatCountLong(doneRooms),
totalRooms: formatCountLong(this.state.roomCount), totalRooms: formatCountLong(this.state.roomCount),
})} <br /> }) } <br />
<Field <Field
label={_t('Message downloading sleep time(ms)')} label={_t('Message downloading sleep time(ms)')}
type='number' type='number'
@ -188,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Message search")} title={_t("Message search")}
> >
{eventIndexingSettings} { eventIndexingSettings }
<DialogButtons <DialogButtons
primaryButton={_t("Done")} primaryButton={_t("Done")}
onPrimaryButtonClick={this.props.onFinished} onPrimaryButtonClick={this.props.onFinished}

View file

@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {}, "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> }, { b: sub => <b>{ sub }</b> },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"We'll store an encrypted copy of your keys on our server. " + "We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a Security Phrase.", "Secure your backup with a Security Phrase.",
)}</p> ) }</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p> <p>{ _t("For maximum security, this should be different from your account password.") }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
/> />
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} > <AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
{_t("Set up with a Security Key")} { _t("Set up with a Security Key") }
</AccessibleButton> </AccessibleButton>
</details> </details>
</form>; </form>;
@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let passPhraseMatch = null; let passPhraseMatch = null;
if (matchText) { if (matchText) {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch"> passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{matchText}</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText} { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
)}</p> ) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div> <div>
@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
autoFocus={true} autoFocus={true}
/> />
</div> </div>
{passPhraseMatch} { passPhraseMatch }
</div> </div>
</div> </div>
<DialogButtons <DialogButtons
@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseShowKey() { _renderPhaseShowKey() {
return <div> return <div>
<p>{_t( <p>{ _t(
"Your Security Key is a safety net - you can use it to restore " + "Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.", "access to your encrypted messages if you forget your Security Phrase.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.", "Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p> ) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader"> <div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Security Key")} { _t("Your Security Key") }
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer"> <div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey"> <div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code> <code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons"> <div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}> <button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy")} { _t("Copy") }
</button> </button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}> <button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")} { _t("Download") }
</button> </button>
</div> </div>
</div> </div>
@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.copied) { if (this.state.copied) {
introText = _t( introText = _t(
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:", "Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, { b: s => <b>{s}</b> }, {}, { b: s => <b>{ s }</b> },
); );
} else if (this.state.downloaded) { } else if (this.state.downloaded) {
introText = _t( introText = _t(
"Your Security Key is in your <b>Downloads</b> folder.", "Your Security Key is in your <b>Downloads</b> folder.",
{}, { b: s => <b>{s}</b> }, {}, { b: s => <b>{ s }</b> },
); );
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{introText} { introText }
<ul> <ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul> </ul>
<DialogButtons primaryButton={_t("Continue")} <DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this._createBackup}
hasCancel={false}> hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button> <button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseDone() { _renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t( <p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).", "Your keys are being backed up (the first backup could take a few minutes).",
)}</p> ) }</p>
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this._onDone}
hasCancel={false} hasCancel={false}
@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseOptOutConfirm() { _renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{_t( { _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " + "Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.", "encrypted message history if you log out or use another session.",
)} ) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')} <DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick} onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false} hasCancel={false}
@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.error) { if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div> content = <div>
<p>{_t("Unable to create key backup")}</p> <p>{ _t("Unable to create key backup") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this._createBackup}
@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
> >
<div> <div>
{content} { content }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -474,10 +474,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{_t("Generate a Security Key")} { _t("Generate a Security Key") }
</div> </div>
<div>{_t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div> <div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
</StyledRadioButton> </StyledRadioButton>
); );
} }
@ -493,10 +493,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
{_t("Enter a Security Phrase")} { _t("Enter a Security Phrase") }
</div> </div>
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div> <div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
</StyledRadioButton> </StyledRadioButton>
); );
} }
@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null; const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}> return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t( <p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " + "Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.", "backing up encryption keys on your server.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup"> <div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey} { optionKey }
{optionPassphrase} { optionPassphrase }
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Continue")} primaryButton={_t("Continue")}
@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let nextCaption = _t("Next"); let nextCaption = _t("Next");
if (this.state.canUploadKeysWithPasswordOnly) { if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = <div> authPrompt = <div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div> <div>{ _t("Enter your account password to confirm the upgrade:") }</div>
<div><Field <div><Field
type="password" type="password"
label={_t("Password")} label={_t("Password")}
@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} else if (!this.state.backupSigStatus.usable) { } else if (!this.state.backupSigStatus.usable) {
authPrompt = <div> authPrompt = <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div> <div>{ _t("Restore your key backup to upgrade your encryption") }</div>
</div>; </div>;
nextCaption = _t("Restore"); nextCaption = _t("Restore");
} else { } else {
authPrompt = <p> authPrompt = <p>
{_t("You'll need to authenticate with the server to confirm the upgrade.")} { _t("You'll need to authenticate with the server to confirm the upgrade.") }
</p>; </p>;
} }
return <form onSubmit={this._onMigrateFormSubmit}> return <form onSubmit={this._onMigrateFormSubmit}>
<p>{_t( <p>{ _t(
"Upgrade this session to allow it to verify other sessions, " + "Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " + "granting them access to encrypted messages and marking them " +
"as trusted for other users.", "as trusted for other users.",
)}</p> ) }</p>
<div>{authPrompt}</div> <div>{ authPrompt }</div>
<DialogButtons <DialogButtons
primaryButton={nextCaption} primaryButton={nextCaption}
onPrimaryButtonClick={this._onMigrateFormSubmit} onPrimaryButtonClick={this._onMigrateFormSubmit}
@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
> >
<button type="button" className="danger" onClick={this._onCancelClick}> <button type="button" className="danger" onClick={this._onCancelClick}>
{_t('Skip')} { _t('Skip') }
</button> </button>
</DialogButtons> </DialogButtons>
</form>; </form>;
@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() { _renderPhasePassPhrase() {
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{ _t(
"Enter a security phrase only you know, as its used to safeguard your data. " + "Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.", "To be secure, you shouldnt re-use your account password.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField <PassphraseField
@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this._onCancelClick}
className="danger" className="danger"
>{_t("Cancel")}</button> >{ _t("Cancel") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let passPhraseMatch = null; let passPhraseMatch = null;
if (matchText) { if (matchText) {
passPhraseMatch = <div> passPhraseMatch = <div>
<div>{matchText}</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText} { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <Field
type="password" type="password"
@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
autoComplete="new-password" autoComplete="new-password"
/> />
<div className="mx_CreateSecretStorageDialog_passPhraseMatch"> <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{passPhraseMatch} { passPhraseMatch }
</div> </div>
</div> </div>
<DialogButtons <DialogButtons
@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this._onCancelClick}
className="danger" className="danger"
>{_t("Skip")}</button> >{ _t("Skip") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
@ -691,35 +691,36 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} }
return <div> return <div>
<p>{_t( <p>{ _t(
"Store your Security Key somewhere safe, like a password manager or a safe, " + "Store your Security Key somewhere safe, like a password manager or a safe, " +
"as its used to safeguard your encrypted data.", "as its used to safeguard your encrypted data.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer"> <div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer"> <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey"> <div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code> <code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary" <AccessibleButton kind='primary'
className="mx_Dialog_primary"
onClick={this._onDownloadClick} onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === PHASE_STORING}
> >
{_t("Download")} { _t("Download") }
</AccessibleButton> </AccessibleButton>
<span>{_t("or")}</span> <span>{ _t("or") }</span>
<AccessibleButton <AccessibleButton
kind='primary' kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn" className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick} onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === PHASE_STORING}
> >
{this.state.copied ? _t("Copied!") : _t("Copy")} { this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>
</div> </div>
{continueButton} { continueButton }
</div>; </div>;
} }
@ -732,7 +733,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseLoadError() { _renderPhaseLoadError() {
return <div> return <div>
<p>{_t("Unable to query secret storage status")}</p> <p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick} onPrimaryButtonClick={this._onLoadRetryClick}
@ -745,17 +746,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseSkipConfirm() { _renderPhaseSkipConfirm() {
return <div> return <div>
<p>{_t( <p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"You can also set up Secure Backup & manage your keys in Settings.", "You can also set up Secure Backup & manage your keys in Settings.",
)}</p> ) }</p>
<DialogButtons primaryButton={_t('Go back')} <DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick} onPrimaryButtonClick={this._onGoBackClick}
hasCancel={false} hasCancel={false}
> >
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button> <button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
@ -787,7 +788,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content; let content;
if (this.state.error) { if (this.state.error) {
content = <div> content = <div>
<p>{_t("Unable to set up secret storage")}</p> <p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage} onPrimaryButtonClick={this._bootstrapSecretStorage}
@ -857,7 +858,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
fixedWidth={false} fixedWidth={false}
> >
<div> <div>
{content} { content }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase1} id='passphrase1' <input
autoFocus={true} size='64' type='password' ref={this._passphrase1}
id='passphrase1'
autoFocus={true}
size='64'
type='password'
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>
@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase2} id='passphrase2' <input ref={this._passphrase2}
size='64' type='password' id='passphrase2'
size='64'
type='password'
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>

View file

@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value={_t('Import')} <input
className='mx_Dialog_primary'
type='submit'
value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>

View file

@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("New Recovery Method")} { _t("New Recovery Method") }
</span>; </span>;
const newMethodDetected = <p>{_t( const newMethodDetected = <p>{ _t(
"A new Security Phrase and key for Secure Messages have been detected.", "A new Security Phrase and key for Secure Messages have been detected.",
)}</p>; ) }</p>;
const hackWarning = <p className="warning">{_t( const hackWarning = <p className="warning">{ _t(
"If you didn't set the new recovery method, an " + "If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " + "attacker may be trying to access your account. " +
"Change your account password and set a new recovery " + "Change your account password and set a new recovery " +
"method immediately in Settings.", "method immediately in Settings.",
)}</p>; ) }</p>;
let content; let content;
if (MatrixClientPeg.get().getKeyBackupEnabled()) { if (MatrixClientPeg.get().getKeyBackupEnabled()) {
content = <div> content = <div>
{newMethodDetected} { newMethodDetected }
<p>{_t( <p>{ _t(
"This session is encrypting history using the new recovery method.", "This session is encrypting history using the new recovery method.",
)}</p> ) }</p>
{hackWarning} { hackWarning }
<DialogButtons <DialogButtons
primaryButton={_t("OK")} primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOkClick} onPrimaryButtonClick={this.onOkClick}
@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</div>; </div>;
} else { } else {
content = <div> content = <div>
{newMethodDetected} { newMethodDetected }
{hackWarning} { hackWarning }
<DialogButtons <DialogButtons
primaryButton={_t("Set up Secure Messages")} primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick} onPrimaryButtonClick={this.onSetupClick}
@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={title} title={title}
> >
{content} { content }
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("Recovery Method Removed")} { _t("Recovery Method Removed") }
</span>; </span>;
return ( return (
@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
title={title} title={title}
> >
<div> <div>
<p>{_t( <p>{ _t(
"This session has detected that your Security Phrase and key " + "This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.", "for Secure Messages have been removed.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"If you did this accidentally, you can setup Secure Messages on " + "If you did this accidentally, you can setup Secure Messages on " +
"this session which will re-encrypt this session's message " + "this session which will re-encrypt this session's message " +
"history with a new recovery method.", "history with a new recovery method.",
)}</p> ) }</p>
<p className="warning">{_t( <p className="warning">{ _t(
"If you didn't remove the recovery method, an " + "If you didn't remove the recovery method, an " +
"attacker may be trying to access your account. " + "attacker may be trying to access your account. " +
"Change your account password and set a new recovery " + "Change your account password and set a new recovery " +
"method immediately in Settings.", "method immediately in Settings.",
)}</p> ) }</p>
<DialogButtons <DialogButtons
primaryButton={_t("Set up Secure Messages")} primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick} onPrimaryButtonClick={this.onSetupClick}

View file

@ -0,0 +1,37 @@
/*
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 { DEFAULT_WAVEFORM, Playback } from "./Playback";
import { PlaybackManager } from "./PlaybackManager";
/**
* A managed playback is a Playback instance that is guided by a PlaybackManager.
*/
export class ManagedPlayback extends Playback {
public constructor(private manager: PlaybackManager, buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) {
super(buf, seedWaveform);
}
public async play(): Promise<void> {
this.manager.playOnly(this);
return super.play();
}
public destroy() {
this.manager.destroyPlaybackInstance(this);
super.destroy();
}
}

View file

@ -32,7 +32,7 @@ export enum PlaybackState {
export const PLAYBACK_WAVEFORM_SAMPLES = 39; export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120] const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] { function makePlaybackWaveform(input: number[]): number[] {
// First, convert negative amplitudes to positive so we don't detect zero as "noisy". // First, convert negative amplitudes to positive so we don't detect zero as "noisy".
@ -59,9 +59,10 @@ export class Playback extends EventEmitter implements IDestroyable {
public readonly thumbnailWaveform: number[]; public readonly thumbnailWaveform: number[];
private readonly context: AudioContext; private readonly context: AudioContext;
private source: AudioBufferSourceNode; private source: AudioBufferSourceNode | MediaElementAudioSourceNode;
private state = PlaybackState.Decoding; private state = PlaybackState.Decoding;
private audioBuf: AudioBuffer; private audioBuf: AudioBuffer;
private element: HTMLAudioElement;
private resampledWaveform: number[]; private resampledWaveform: number[];
private waveformObservable = new SimpleObservable<number[]>(); private waveformObservable = new SimpleObservable<number[]>();
private readonly clock: PlaybackClock; private readonly clock: PlaybackClock;
@ -129,36 +130,64 @@ export class Playback extends EventEmitter implements IDestroyable {
this.removeAllListeners(); this.removeAllListeners();
this.clock.destroy(); this.clock.destroy();
this.waveformObservable.close(); this.waveformObservable.close();
if (this.element) {
URL.revokeObjectURL(this.element.src);
this.element.remove();
}
} }
public async prepare() { public async prepare() {
// Safari compat: promise API not supported on this function // The point where we use an audio element is fairly arbitrary, though we don't want
this.audioBuf = await new Promise((resolve, reject) => { // it to be too low. As of writing, voice messages want to show a waveform but audio
this.context.decodeAudioData(this.buf, b => resolve(b), async e => { // messages do not. Using an audio element means we can't show a waveform preview, so
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg // we try to target the difference between a voice message file and large audio file.
// very well. // Overall, the point of this is to avoid memory-related issues due to storing a massive
console.error("Error decoding recording: ", e); // audio buffer in memory, as that can balloon to far greater than the input buffer's
console.warn("Trying to re-encode to WAV instead..."); // byte length.
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
console.log("Audio file too large: processing through <audio /> element");
this.element = document.createElement("AUDIO") as HTMLAudioElement;
const prom = new Promise((resolve, reject) => {
this.element.onloadeddata = () => resolve(null);
this.element.onerror = (e) => reject(e);
});
this.element.src = URL.createObjectURL(new Blob([this.buf]));
await prom; // make sure the audio element is ready for us
} else {
// Safari compat: promise API not supported on this function
this.audioBuf = await new Promise((resolve, reject) => {
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
try {
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
// very well.
console.error("Error decoding recording: ", e);
console.warn("Trying to re-encode to WAV instead...");
const wav = await decodeOgg(this.buf); const wav = await decodeOgg(this.buf);
// noinspection ES6MissingAwait - not needed when using callbacks // noinspection ES6MissingAwait - not needed when using callbacks
this.context.decodeAudioData(wav, b => resolve(b), e => { this.context.decodeAudioData(wav, b => resolve(b), e => {
console.error("Still failed to decode recording: ", e); console.error("Still failed to decode recording: ", e);
reject(e); reject(e);
});
} catch (e) {
console.error("Caught decoding error:", e);
reject(e);
}
}); });
}); });
});
// Update the waveform to the real waveform once we have channel data to use. We don't // Update the waveform to the real waveform once we have channel data to use. We don't
// exactly trust the user-provided waveform to be accurate... // exactly trust the user-provided waveform to be accurate...
const waveform = Array.from(this.audioBuf.getChannelData(0)); const waveform = Array.from(this.audioBuf.getChannelData(0));
this.resampledWaveform = makePlaybackWaveform(waveform); this.resampledWaveform = makePlaybackWaveform(waveform);
}
this.waveformObservable.update(this.resampledWaveform); this.waveformObservable.update(this.resampledWaveform);
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
this.clock.durationSeconds = this.audioBuf.duration; this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration;
} }
private onPlaybackEnd = async () => { private onPlaybackEnd = async () => {
@ -171,7 +200,11 @@ export class Playback extends EventEmitter implements IDestroyable {
if (this.state === PlaybackState.Stopped) { if (this.state === PlaybackState.Stopped) {
this.disconnectSource(); this.disconnectSource();
this.makeNewSourceBuffer(); this.makeNewSourceBuffer();
this.source.start(); if (this.element) {
await this.element.play();
} else {
(this.source as AudioBufferSourceNode).start();
}
} }
// We use the context suspend/resume functions because it allows us to pause a source // We use the context suspend/resume functions because it allows us to pause a source
@ -182,13 +215,21 @@ export class Playback extends EventEmitter implements IDestroyable {
} }
private disconnectSource() { private disconnectSource() {
if (this.element) return; // leave connected, we can (and must) re-use it
this.source?.disconnect(); this.source?.disconnect();
this.source?.removeEventListener("ended", this.onPlaybackEnd); this.source?.removeEventListener("ended", this.onPlaybackEnd);
} }
private makeNewSourceBuffer() { private makeNewSourceBuffer() {
this.source = this.context.createBufferSource(); if (this.element && this.source) return; // leave connected, we can (and must) re-use it
this.source.buffer = this.audioBuf;
if (this.element) {
this.source = this.context.createMediaElementSource(this.element);
} else {
this.source = this.context.createBufferSource();
this.source.buffer = this.audioBuf;
}
this.source.addEventListener("ended", this.onPlaybackEnd); this.source.addEventListener("ended", this.onPlaybackEnd);
this.source.connect(this.context.destination); this.source.connect(this.context.destination);
} }
@ -241,7 +282,11 @@ export class Playback extends EventEmitter implements IDestroyable {
// when it comes time to the user hitting play. After a couple jumps, the user // when it comes time to the user hitting play. After a couple jumps, the user
// will have desynced the clock enough to be about 10-15 seconds off, while this // will have desynced the clock enough to be about 10-15 seconds off, while this
// keeps it as close to perfect as humans can perceive. // keeps it as close to perfect as humans can perceive.
this.source.start(now, timeSeconds); if (this.element) {
this.element.currentTime = timeSeconds;
} else {
(this.source as AudioBufferSourceNode).start(now, timeSeconds);
}
// Dev note: it's critical that the code gap between `this.source.start()` and // Dev note: it's critical that the code gap between `this.source.start()` and
// `this.pause()` is as small as possible: we do not want to delay *anything* // `this.pause()` is as small as possible: we do not want to delay *anything*

View file

@ -103,8 +103,8 @@ export class PlaybackClock implements IDestroyable {
* @param {MatrixEvent} event The event to use for placeholders. * @param {MatrixEvent} event The event to use for placeholders.
*/ */
public populatePlaceholdersFrom(event: MatrixEvent) { public populatePlaceholdersFrom(event: MatrixEvent) {
const durationSeconds = Number(event.getContent()['info']?.['duration']); const durationMs = Number(event.getContent()['info']?.['duration']);
if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds; if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
} }
/** /**
@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable {
public flagStop() { public flagStop() {
this.stopped = true; this.stopped = true;
// Reset the clock time now so that the update going out will trigger components
// to check their seek/position information (alongside the clock).
this.clipStart = this.context.currentTime;
} }
public syncTo(contextTime: number, clipTime: number) { public syncTo(contextTime: number, clipTime: number) {

View file

@ -0,0 +1,54 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DEFAULT_WAVEFORM, Playback } from "./Playback";
import { ManagedPlayback } from "./ManagedPlayback";
/**
* Handles management of playback instances to ensure certain functionality, like
* one playback operating at any one time.
*/
export class PlaybackManager {
private static internalInstance: PlaybackManager;
private instances: ManagedPlayback[] = [];
public static get instance(): PlaybackManager {
if (!PlaybackManager.internalInstance) {
PlaybackManager.internalInstance = new PlaybackManager();
}
return PlaybackManager.internalInstance;
}
/**
* Stops all other playback instances. If no playback is provided, all instances
* are stopped.
* @param playback Optional. The playback to leave untouched.
*/
public playOnly(playback?: Playback) {
this.instances.filter(p => p !== playback).forEach(p => p.stop());
}
public destroyPlaybackInstance(playback: ManagedPlayback) {
this.instances = this.instances.filter(p => p !== playback);
}
public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {
const instance = new ManagedPlayback(this, buf, waveform);
this.instances.push(instance);
return instance;
}
}

View file

@ -333,12 +333,17 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
if (this.lastUpload) return this.lastUpload; if (this.lastUpload) return this.lastUpload;
this.emit(RecordingState.Uploading); try {
const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], { this.emit(RecordingState.Uploading);
type: this.contentType, const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
})); type: this.contentType,
this.lastUpload = { mxc, encrypted }; }));
this.emit(RecordingState.Uploaded); this.lastUpload = { mxc, encrypted };
this.emit(RecordingState.Uploaded);
} catch (e) {
this.emit(RecordingState.Ended);
throw e;
}
return this.lastUpload; return this.lastUpload;
} }
} }

View file

@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
import { ICompletion, ISelectionRange } from './Autocompleter'; import { ICompletion, ISelectionRange } from './Autocompleter';
import { uniq, sortBy } from 'lodash'; import { uniq, sortBy } from 'lodash';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import { EMOJI, IEmoji } from '../emoji'; import { EMOJI, IEmoji } from '../emoji';
import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EMOTICON_REGEX from 'emojibase-regex/emoticon';
@ -36,20 +35,18 @@ const LIMIT = 20;
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g'); const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
interface IEmojiShort { interface ISortedEmoji {
emoji: IEmoji; emoji: IEmoji;
shortname: string;
_orderBy: number; _orderBy: number;
} }
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => { const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
if (a.group === b.group) { if (a.group === b.group) {
return a.order - b.order; return a.order - b.order;
} }
return a.group - b.group; return a.group - b.group;
}).map((emoji, index) => ({ }).map((emoji, index) => ({
emoji, emoji,
shortname: `:${emoji.shortcodes[0]}:`,
// Include the index so that we can preserve the original order // Include the index so that we can preserve the original order
_orderBy: index, _orderBy: index,
})); }));
@ -64,20 +61,18 @@ function score(query, space) {
} }
export default class EmojiProvider extends AutocompleteProvider { export default class EmojiProvider extends AutocompleteProvider {
matcher: QueryMatcher<IEmojiShort>; matcher: QueryMatcher<ISortedEmoji>;
nameMatcher: QueryMatcher<IEmojiShort>; nameMatcher: QueryMatcher<ISortedEmoji>;
constructor() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, { this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
keys: ['emoji.emoticon', 'shortname'], keys: ['emoji.emoticon'],
funcs: [ funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
],
// For matching against ascii equivalents // For matching against ascii equivalents
shouldMatchWordsOnly: false, shouldMatchWordsOnly: false,
}); });
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
keys: ['emoji.annotation'], keys: ['emoji.annotation'],
// For removing punctuation // For removing punctuation
shouldMatchWordsOnly: true, shouldMatchWordsOnly: true,
@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
const sorters = []; const sorters = [];
// make sure that emoticons come first // make sure that emoticons come first
sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
// then sort by score (Infinity if matchedString not in shortname) // then sort by score (Infinity if matchedString not in shortcode)
sorters.push((c) => score(matchedString, c.shortname)); sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:` // then sort by max score of all shortcodes, trim off the `:`
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)))); sorters.push(c => Math.min(
// If the matchedString is not empty, sort by length of shortname. Example: ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
));
// If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark" // matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...] // completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) { if (matchedString.length > 1) {
sorters.push((c) => c.shortname.length); sorters.push(c => c.emoji.shortcodes[0].length);
} }
// Finally, sort by original ordering // Finally, sort by original ordering
sorters.push((c) => c._orderBy); sorters.push(c => c._orderBy);
completions = sortBy(uniq(completions), sorters); completions = sortBy(uniq(completions), sorters);
completions = completions.map(({ shortname }) => { completions = completions.map(c => ({
const unicode = shortcodeToUnicode(shortname); completion: c.emoji.unicode,
return { component: (
completion: unicode, <PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
component: ( <span>{ c.emoji.unicode }</span>
<PillCompletion title={shortname} aria-label={unicode}> </PillCompletion>
<span>{ unicode }</span> ),
</PillCompletion> range,
), })).slice(0, LIMIT);
range,
};
}).slice(0, LIMIT);
} }
return completions; return completions;
} }

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

@ -0,0 +1,153 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { EventEmitter } from 'events';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
export enum CallEventGrouperEvent {
StateChanged = "state_changed",
SilencedChanged = "silenced_changed",
}
const SUPPORTED_STATES = [
CallState.Connected,
CallState.Connecting,
CallState.Ringing,
];
export enum CustomCallState {
Missed = "missed",
}
export default class CallEventGrouper extends EventEmitter {
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
private call: MatrixCall;
public state: CallState | CustomCallState;
constructor() {
super();
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
}
private get invite(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallInvite);
}
private get hangup(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallHangup);
}
private get reject(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallReject);
}
public get isVoice(): boolean {
const invite = this.invite;
if (!invite) return;
// FIXME: Find a better way to determine this from the event?
if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false;
return true;
}
public get hangupReason(): string | null {
return this.hangup?.getContent()?.reason;
}
public get rejectParty(): string {
return this.reject?.getSender();
}
public get gotRejected(): boolean {
return Boolean(this.reject);
}
/**
* Returns true if there are only events from the other side - we missed the call
*/
private get callWasMissed(): boolean {
return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
}
private get callId(): string {
return [...this.events][0].getContent().call_id;
}
private onSilencedCallsChanged = () => {
const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
};
public answerCall = () => {
this.call?.answer();
};
public rejectCall = () => {
this.call?.reject();
};
public callBack = () => {
defaultDispatcher.dispatch({
action: 'place_call',
type: this.isVoice ? CallType.Voice : CallType.Video,
room_id: [...this.events][0]?.getRoomId(),
});
};
public toggleSilenced = () => {
const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
silenced ?
CallHandler.sharedInstance().unSilenceCall(this.callId) :
CallHandler.sharedInstance().silenceCall(this.callId);
};
private setCallListeners() {
if (!this.call) return;
this.call.addListener(CallEvent.State, this.setState);
}
private setState = () => {
if (SUPPORTED_STATES.includes(this.call?.state)) {
this.state = this.call.state;
} else {
if (this.callWasMissed) this.state = CustomCallState.Missed;
else if (this.reject) this.state = CallState.Ended;
else if (this.hangup) this.state = CallState.Ended;
else if (this.invite && this.call) this.state = CallState.Connecting;
}
this.emit(CallEventGrouperEvent.StateChanged, this.state);
};
private setCall = () => {
if (this.call) return;
this.call = CallHandler.sharedInstance().getCallById(this.callId);
this.setCallListeners();
this.setState();
};
public add(event: MatrixEvent) {
this.events.add(event);
this.setCall();
}
}

View file

@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component {
return (<div className={classes}> return (<div className={classes}>
<div className="mx_CustomRoomTagPanel_divider" /> <div className="mx_CustomRoomTagPanel_divider" />
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller"> <AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
{tags} { tags }
</AutoHideScrollbar> </AutoHideScrollbar>
</div>); </div>);
} }
@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
"mx_TagTile_badge": true, "mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions, "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
}); });
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>); badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
} }
return ( return (

View file

@ -120,16 +120,15 @@ export default class EmbeddedPage extends React.PureComponent {
const content = <div className={`${className}_body`} const content = <div className={`${className}_body`}
dangerouslySetInnerHTML={{ __html: this.state.page }} dangerouslySetInnerHTML={{ __html: this.state.page }}
> />;
</div>;
if (this.props.scrollbar) { if (this.props.scrollbar) {
return <AutoHideScrollbar className={classes}> return <AutoHideScrollbar className={classes}>
{content} { content }
</AutoHideScrollbar>; </AutoHideScrollbar>;
} else { } else {
return <div className={classes}> return <div className={classes}>
{content} { content }
</div>; </div>;
} }
} }

View file

@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile'; import { TileShape } from '../views/rooms/EventTile';
import { Layout } from "../../settings/Layout";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -241,8 +242,8 @@ class FilePanel extends React.Component<IProps, IState> {
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2> <h2>{ _t('No files visible in this room') }</h2>
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p> <p>{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }</p>
</div>); </div>);
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
@ -262,11 +263,12 @@ class FilePanel extends React.Component<IProps, IState> {
manageReadReceipts={false} manageReadReceipts={false}
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = {false} showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest} onPaginationRequest={this.onPaginationRequest}
tileShape={TileShape.FileGrid} tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
empty={emptyState} empty={emptyState}
layout={Layout.Group}
/> />
</BaseCard> </BaseCard>
); );

View file

@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
render() { render() {
return <div className='mx_GenericErrorPage'> return <div className='mx_GenericErrorPage'>
<div className='mx_GenericErrorPage_box'> <div className='mx_GenericErrorPage_box'>
<h1>{this.props.title}</h1> <h1>{ this.props.title }</h1>
<p>{this.props.message}</p> <p>{ this.props.message }</p>
</div> </div>
</div>; </div>;
} }

View file

@ -222,7 +222,7 @@ class FeaturedRoom extends React.Component {
let roomNameNode = null; let roomNameNode = null;
if (permalink) { if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>; roomNameNode = <a href={permalink} onClick={this.onClick}>{ roomName }</a>;
} else { } else {
roomNameNode = <span>{ roomName }</span>; roomNameNode = <span>{ roomName }</span>;
} }
@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
let hostingSignup = null; let hostingSignup = null;
if (hostingSignupLink && this.state.isUserPrivileged) { if (hostingSignupLink && this.state.isUserPrivileged) {
hostingSignup = <div className="mx_GroupView_hostingSignup"> hostingSignup = <div className="mx_GroupView_hostingSignup">
{_t( { _t(
"Want more than a community? <a>Get your own server</a>", {}, "Want more than a community? <a>Get your own server</a>", {},
{ {
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>, a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
}, },
)} ) }
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener"> <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' /> <img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a> </a>
@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
avatarImage = <Spinner />; avatarImage = <Spinner />;
} else { } else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId} avatarImage = <GroupAvatar
groupId={this.props.groupId}
groupName={this.state.profileForm.name} groupName={this.state.profileForm.name}
groupAvatarUrl={this.state.profileForm.avatar_url} groupAvatarUrl={this.state.profileForm.avatar_url}
width={28} height={28} resizeMethod='crop' width={28}
height={28}
resizeMethod='crop'
/>; />;
} }
@ -1199,9 +1202,12 @@ export default class GroupView extends React.Component {
</label> </label>
<div className="mx_GroupView_avatarPicker_edit"> <div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label"> <label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
<img src={require("../../../res/img/camera.svg")} <img
alt={_t("Upload avatar")} title={_t("Upload avatar")} src={require("../../../res/img/camera.svg")}
width="17" height="15" /> alt={_t("Upload avatar")}
title={_t("Upload avatar")}
width="17"
height="15" />
</label> </label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} /> <input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
</div> </div>
@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
groupAvatarUrl={groupAvatarUrl} groupAvatarUrl={groupAvatarUrl}
groupName={groupName} groupName={groupName}
onClick={onGroupHeaderItemClick} onClick={onGroupHeaderItemClick}
width={28} height={28} width={28}
height={28}
/>; />;
if (summary.profile && summary.profile.name) { if (summary.profile && summary.profile.name) {
nameNode = <div onClick={onGroupHeaderItemClick}> nameNode = <div onClick={onGroupHeaderItemClick}>
@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
key="_cancelButton" key="_cancelButton"
onClick={this._onCancelClick} onClick={this._onCancelClick}
> >
<img src={require("../../../res/img/cancel.svg")} className="mx_filterFlipColor" <img
width="18" height="18" alt={_t("Cancel")} /> src={require("../../../res/img/cancel.svg")}
className="mx_filterFlipColor"
width="18"
height="18"
alt={_t("Cancel")} />
</AccessibleButton>, </AccessibleButton>,
); );
} else { } else {
if (summary.user && summary.user.membership === 'join') { if (summary.user && summary.user.membership === 'join') {
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_editButton" <AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_editButton"
key="_editButton" key="_editButton"
onClick={this._onEditClick} onClick={this._onEditClick}
title={_t("Community Settings")} title={_t("Community Settings")}
> />,
</AccessibleButton>,
); );
} }
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_shareButton" <AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_shareButton"
key="_shareButton" key="_shareButton"
onClick={this._onShareClick} onClick={this._onShareClick}
title={_t('Share Community')} title={_t('Share Community')}
> />,
</AccessibleButton>,
); );
} }

View file

@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onSelectRoom={this.selectRoom} onSelectRoom={this.selectRoom}
/> />
{dialPadButton} { dialPadButton }
<AccessibleTooltipButton <AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", { className={classNames("mx_LeftPanel_exploreButton", {
@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
leftLeftPanel = ( leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer"> <div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel /> <GroupFilterPanel />
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null} { SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
</div> </div>
); );
} }
@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return ( return (
<div className={containerClasses} ref={this.ref}> <div className={containerClasses} ref={this.ref}>
{leftLeftPanel} { leftLeftPanel }
<aside className="mx_LeftPanel_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} { this.renderHeader() }
{this.renderSearchDialExplore()} { this.renderSearchDialExplore() }
{this.renderBreadcrumbs()} { this.renderBreadcrumbs() }
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} /> <RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
<div className="mx_LeftPanel_roomListWrapper"> <div className="mx_LeftPanel_roomListWrapper">
<div <div
@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// overflow:scroll;, so force it out of tab order. // overflow:scroll;, so force it out of tab order.
tabIndex={-1} tabIndex={-1}
> >
{roomList} { roomList }
</div> </div>
</div> </div>
{ !this.props.isMinimized && <LeftPanelWidget /> } { !this.props.isMinimized && <LeftPanelWidget /> }

View file

@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
<span>{ WidgetUtils.getWidgetName(app) }</span> <span>{ WidgetUtils.getWidgetName(app) }</span>
</AccessibleButton> </AccessibleButton>
{/* Code for the maximise button for once we have full screen widgets */} { /* Code for the maximise button for once we have full screen widgets */ }
{/*<AccessibleTooltipButton { /*<AccessibleTooltipButton
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={() => { onClick={() => {
}} }}
className="mx_LeftPanelWidget_maximizeButton" className="mx_LeftPanelWidget_maximizeButton"
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
title={_t("Maximize")} title={_t("Maximize")}
/>*/} />*/ }
</div> </div>
</div> </div>

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}
> >
@ -644,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
<CallContainer /> <CallContainer />
<NonUrgentToastContainer /> <NonUrgentToastContainer />
<HostSignupContainer /> <HostSignupContainer />
{audioFeedArraysForCalls} { audioFeedArraysForCalls }
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
} }

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();
@ -1112,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (memberCount === 1) { if (memberCount === 1) {
warnings.push(( warnings.push((
<span className="warning" key="only_member_warning"> <span className="warning" key="only_member_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ } { ' '/* Whitespace, otherwise the sentences get smashed together */ }
{ _t("You are the only person here. " + { _t("You are the only person here. " +
"If you leave, no one will be able to join in the future, including you.") } "If you leave, no one will be able to join in the future, including you.") }
</span> </span>
@ -1127,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (rule !== "public") { if (rule !== "public") {
warnings.push(( warnings.push((
<span className="warning" key="non_public_warning"> <span className="warning" key="non_public_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ } { ' '/* Whitespace, otherwise the sentences get smashed together */ }
{ isSpace { isSpace
? _t("This space is not public. You will not be able to rejoin without an invite.") ? _t("This space is not public. You will not be able to rejoin without an invite.")
: _t("This room is not public. You will not be able to rejoin without an invite.") } : _t("This room is not public. You will not be able to rejoin without an invite.") }
@ -1155,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
: _t( : _t(
"Are you sure you want to leave the room '%(roomName)s'?", "Are you sure you want to leave the room '%(roomName)s'?",
{ roomName: roomToLeave.name }, { roomName: roomToLeave.name },
)} ) }
{ warnings } { warnings }
</span> </span>
), ),
@ -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}
/> />
@ -2053,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let errorBox; let errorBox;
if (this.state.syncError && !isStoreError) { if (this.state.syncError && !isStoreError) {
errorBox = <div className="mx_MatrixChat_syncError"> errorBox = <div className="mx_MatrixChat_syncError">
{messageForSyncError(this.state.syncError)} { messageForSyncError(this.state.syncError) }
</div>; </div>;
} }
view = ( view = (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
{errorBox} { errorBox }
<Spinner /> <Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}> <a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
{_t('Logout')} { _t('Logout') }
</a> </a>
</div> </div>
); );
@ -2124,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
return <ErrorBoundary> return <ErrorBoundary>
{view} { view }
</ErrorBoundary>; </ErrorBoundary>;
} }
} }

View file

@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher'; import defaultDispatcher from '../../dispatcher/dispatcher';
import CallEventGrouper from "./CallEventGrouper";
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
import ScrollPanel, { IScrollState } from "./ScrollPanel"; import ScrollPanel, { IScrollState } from "./ScrollPanel";
import EventListSummary from '../views/elements/EventListSummary'; import EventListSummary from '../views/elements/EventListSummary';
@ -232,6 +233,11 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private readonly showTypingNotificationsWatcherRef: string; private readonly showTypingNotificationsWatcherRef: string;
private eventNodes: Record<string, HTMLElement>; private eventNodes: Record<string, HTMLElement>;
// A map of <callId, CallEventGrouper>
private callEventGroupers = new Map<string, CallEventGrouper>();
private membersCount = 0;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -252,11 +258,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
componentDidMount() { componentDidMount() {
this.calculateRoomMembersCount();
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
this.isMounted = true; this.isMounted = true;
} }
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.room?.off("RoomState.members", this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
} }
@ -270,6 +279,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
} }
private calculateRoomMembersCount = (): void => {
this.membersCount = this.props.room?.getMembers().length || 0;
};
private onShowTypingNotificationsChange = (): void => { private onShowTypingNotificationsChange = (): void => {
this.setState({ this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@ -576,6 +589,20 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const last = (mxEv === lastShownEvent); const last = (mxEv === lastShownEvent);
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
if (
mxEv.getType().indexOf("m.call.") === 0 ||
mxEv.getType().indexOf("org.matrix.call.") === 0
) {
const callId = mxEv.getContent().call_id;
if (this.callEventGroupers.has(callId)) {
this.callEventGroupers.get(callId).add(mxEv);
} else {
const callEventGrouper = new CallEventGrouper();
callEventGrouper.add(mxEv);
this.callEventGroupers.set(callId, callEventGrouper);
}
}
if (grouper) { if (grouper) {
if (grouper.shouldGroup(mxEv)) { if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv, this.showHiddenEvents); grouper.add(mxEv, this.showHiddenEvents);
@ -591,7 +618,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
for (const Grouper of groupers) { for (const Grouper of groupers) {
if (Grouper.canStartGroup(this, mxEv)) { if (Grouper.canStartGroup(this, mxEv)) {
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); grouper = new Grouper(
this,
mxEv,
prevEvent,
lastShownEvent,
this.props.layout,
nextEvent,
nextTile,
);
} }
} }
if (!grouper) { if (!grouper) {
@ -692,6 +727,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// it's successful: we received it. // it's successful: we received it.
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
// use txnId as key if available so that we don't remount during sending // use txnId as key if available so that we don't remount during sending
ret.push( ret.push(
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}> <TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
@ -722,7 +758,8 @@ 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} callEventGrouper={callEventGrouper}
hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
/> />
</TileErrorBoundary>, </TileErrorBoundary>,
); );
@ -952,6 +989,7 @@ abstract class BaseGrouper {
public readonly event: MatrixEvent, public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent, public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout,
public readonly nextEvent?: MatrixEvent, public readonly nextEvent?: MatrixEvent,
public readonly nextEventTile?: MatrixEvent, public readonly nextEventTile?: MatrixEvent,
) { ) {
@ -1078,6 +1116,7 @@ class CreationGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]} summaryMembers={[ev.sender]}
summaryText={summaryText} summaryText={summaryText}
layout={this.layout}
> >
{ eventTiles } { eventTiles }
</EventListSummary>, </EventListSummary>,
@ -1105,10 +1144,11 @@ class RedactionGrouper extends BaseGrouper {
ev: MatrixEvent, ev: MatrixEvent,
prevEvent: MatrixEvent, prevEvent: MatrixEvent,
lastShownEvent: MatrixEvent, lastShownEvent: MatrixEvent,
layout: Layout,
nextEvent: MatrixEvent, nextEvent: MatrixEvent,
nextEventTile: MatrixEvent, nextEventTile: MatrixEvent,
) { ) {
super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile); super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
this.events = [ev]; this.events = [ev];
} }
@ -1173,6 +1213,7 @@ class RedactionGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)} summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })} summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
layout={this.layout}
> >
{ eventTiles } { eventTiles }
</EventListSummary>, </EventListSummary>,
@ -1201,8 +1242,9 @@ class MemberGrouper extends BaseGrouper {
public readonly event: MatrixEvent, public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent, public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout,
) { ) {
super(panel, event, prevEvent, lastShownEvent); super(panel, event, prevEvent, lastShownEvent, layout);
this.events = [event]; this.events = [event];
} }
@ -1277,6 +1319,7 @@ class MemberGrouper extends BaseGrouper {
events={this.events} events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state onToggle={panel.onHeightChanged} // Update scroll state
startExpanded={highlightInMels} startExpanded={highlightInMels}
layout={this.layout}
> >
{ eventTiles } { eventTiles }
</MemberEventListSummary>, </MemberEventListSummary>,

View file

@ -109,8 +109,7 @@ export default class MyGroups extends React.Component {
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} /> <SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} />
<div className='mx_MyGroups_header'> <div className='mx_MyGroups_header'>
<div className="mx_MyGroups_headerCard"> <div className="mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}> <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick} />
</AccessibleButton>
<div className="mx_MyGroups_headerCard_content"> <div className="mx_MyGroups_headerCard_content">
<div className="mx_MyGroups_headerCard_header"> <div className="mx_MyGroups_headerCard_header">
{ _t('Create a new community') } { _t('Create a new community') }
@ -121,7 +120,7 @@ export default class MyGroups extends React.Component {
) } ) }
</div> </div>
</div> </div>
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard"> { /*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}> <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" /> <img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
</AccessibleButton> </AccessibleButton>
@ -137,7 +136,7 @@ export default class MyGroups extends React.Component {
{ 'i': (sub) => <i>{ sub }</i> }) { 'i': (sub) => <i>{ sub }</i> })
} }
</div> </div>
</div>*/} </div>*/ }
</div> </div>
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} /> <BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
<div className="mx_MyGroups_content"> <div className="mx_MyGroups_content">

View file

@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
const toasts = this.state.toasts.map((t, i) => { const toasts = this.state.toasts.map((t, i) => {
return ( return (
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}> <div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
{React.createElement(t, {})} { React.createElement(t, {}) }
</div> </div>
); );
}); });
return ( return (
<div className="mx_NonUrgentToastContainer" role="alert"> <div className="mx_NonUrgentToastContainer" role="alert">
{toasts} { toasts }
</div> </div>
); );
} }

View file

@ -23,6 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { TileShape } from "../views/rooms/EventTile"; import { TileShape } from "../views/rooms/EventTile";
import { Layout } from "../../settings/Layout";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -35,8 +36,8 @@ interface IProps {
export default class NotificationPanel extends React.PureComponent<IProps> { export default class NotificationPanel extends React.PureComponent<IProps> {
render() { render() {
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2> <h2>{ _t('Youre all caught up') }</h2>
<p>{_t('You have no visible notifications.')}</p> <p>{ _t('You have no visible notifications.') }</p>
</div>); </div>);
let content; let content;
@ -52,6 +53,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
tileShape={TileShape.Notif} tileShape={TileShape.Notif}
empty={emptyState} empty={emptyState}
alwaysShowTimestamps={true} alwaysShowTimestamps={true}
layout={Layout.Group}
/> />
); );
} else { } else {

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

@ -589,7 +589,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// We use onMouseDown instead of onClick, so that we can avoid text getting selected // We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [ return [
<div <div
key={ `${room.room_id}_avatar` } key={`${room.room_id}_avatar`}
onMouseDown={(ev) => this.onRoomClicked(room, ev)} onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomAvatar" className="mx_RoomDirectory_roomAvatar"
> >
@ -603,7 +603,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
/> />
</div>, </div>,
<div <div
key={ `${room.room_id}_description` } key={`${room.room_id}_description`}
onMouseDown={(ev) => this.onRoomClicked(room, ev)} onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomDescription" className="mx_RoomDirectory_roomDescription"
> >
@ -626,14 +626,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
</div> </div>
</div>, </div>,
<div <div
key={ `${room.room_id}_memberCount` } key={`${room.room_id}_memberCount`}
onMouseDown={(ev) => this.onRoomClicked(room, ev)} onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomMemberCount" className="mx_RoomDirectory_roomMemberCount"
> >
{ room.num_joined_members } { room.num_joined_members }
</div>, </div>,
<div <div
key={ `${room.room_id}_preview` } key={`${room.room_id}_preview`}
onMouseDown={(ev) => this.onRoomClicked(room, ev)} onMouseDown={(ev) => this.onRoomClicked(room, ev)}
// cancel onMouseDown otherwise shift-clicking highlights text // cancel onMouseDown otherwise shift-clicking highlights text
className="mx_RoomDirectory_preview" className="mx_RoomDirectory_preview"
@ -641,7 +641,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
{ previewButton } { previewButton }
</div>, </div>,
<div <div
key={ `${room.room_id}_join` } key={`${room.room_id}_join`}
onMouseDown={(ev) => this.onRoomClicked(room, ev)} onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_join" className="mx_RoomDirectory_join"
> >
@ -796,7 +796,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
showJoinButton={showJoinButton} showJoinButton={showJoinButton}
initialText={this.props.initialText} initialText={this.props.initialText}
/> />
{dropdown} { dropdown }
</div>; </div>;
} }
const explanation = const explanation =
@ -814,16 +814,16 @@ 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}
> >
<div className="mx_RoomDirectory"> <div className="mx_RoomDirectory">
{explanation} { explanation }
<div className="mx_RoomDirectory_list"> <div className="mx_RoomDirectory_list">
{listHeader} { listHeader }
{content} { content }
</div> </div>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -209,9 +209,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
return ( return (
<div className={classes}> <div className={classes}>
{icon} { icon }
{input} { input }
{clearButton} { clearButton }
</div> </div>
); );
} }

View file

@ -222,17 +222,17 @@ export default class RoomStatusBar extends React.PureComponent {
let buttonRow = <> let buttonRow = <>
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn"> <AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
{_t("Delete all")} { _t("Delete all") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn"> <AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
{_t("Retry all")} { _t("Retry all") }
</AccessibleButton> </AccessibleButton>
</>; </>;
if (this.state.isResending) { if (this.state.isResending) {
buttonRow = <> buttonRow = <>
<InlineSpinner w={20} h={20} /> <InlineSpinner w={20} h={20} />
{/* span for css */} { /* span for css */ }
<span>{_t("Sending")}</span> <span>{ _t("Sending") }</span>
</>; </>;
} }
@ -253,7 +253,7 @@ export default class RoomStatusBar extends React.PureComponent {
</div> </div>
</div> </div>
<div className="mx_RoomStatusBar_unsentButtonBar"> <div className="mx_RoomStatusBar_unsentButtonBar">
{buttonRow} { buttonRow }
</div> </div>
</div> </div>
</div> </div>
@ -266,14 +266,18 @@ export default class RoomStatusBar extends React.PureComponent {
<div className="mx_RoomStatusBar"> <div className="mx_RoomStatusBar">
<div role="alert"> <div role="alert">
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" <img
height="24" title="/!\ " alt="/!\ " /> src={require("../../../res/img/feather-customised/warning-triangle.svg")}
width="24"
height="24"
title="/!\ "
alt="/!\ " />
<div> <div>
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{_t('Connectivity to the server has been lost.')} { _t('Connectivity to the server has been lost.') }
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_desc">
{_t('Sent messages will be stored until your connection has returned.')} { _t('Sent messages will be stored until your connection has returned.') }
</div> </div>
</div> </div>
</div> </div>

View file

@ -166,6 +166,10 @@ export interface IState {
canReply: boolean; canReply: boolean;
layout: Layout; layout: Layout;
lowBandwidth: boolean; lowBandwidth: boolean;
alwaysShowTimestamps: boolean;
showTwelveHourTimestamps: boolean;
readMarkerInViewThresholdMs: number;
readMarkerOutOfViewThresholdMs: number;
showHiddenEventsInTimeline: boolean; showHiddenEventsInTimeline: boolean;
showReadReceipts: boolean; showReadReceipts: boolean;
showRedactions: boolean; showRedactions: boolean;
@ -231,6 +235,10 @@ export default class RoomView extends React.Component<IProps, IState> {
canReply: false, canReply: false,
layout: SettingsStore.getValue("layout"), layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"), lowBandwidth: SettingsStore.getValue("lowBandwidth"),
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"), showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true, showReadReceipts: true,
showRedactions: true, showRedactions: true,
@ -263,14 +271,26 @@ export default class RoomView extends React.Component<IProps, IState> {
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.settingWatchers = [ this.settingWatchers = [
SettingsStore.watchSetting("layout", null, () => SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
this.setState({ layout: SettingsStore.getValue("layout") }), this.setState({ layout: value as Layout }),
), ),
SettingsStore.watchSetting("lowBandwidth", null, () => SettingsStore.watchSetting("lowBandwidth", null, (...[,,, value]) =>
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }), this.setState({ lowBandwidth: value as boolean }),
), ),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () => SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[,,, value]) =>
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }), this.setState({ alwaysShowTimestamps: value as boolean }),
),
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[,,, value]) =>
this.setState({ showTwelveHourTimestamps: value as boolean }),
),
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[,,, value]) =>
this.setState({ readMarkerInViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[,,, value]) =>
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
this.setState({ showHiddenEventsInTimeline: value as boolean }),
), ),
]; ];
} }
@ -337,30 +357,20 @@ export default class RoomView extends React.Component<IProps, IState> {
// Add watchers for each of the settings we just looked up // Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([ this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", null, () => SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>
this.setState({ this.setState({ showReadReceipts: value as boolean }),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
}),
), ),
SettingsStore.watchSetting("showRedactions", null, () => SettingsStore.watchSetting("showRedactions", roomId, (...[,,, value]) =>
this.setState({ this.setState({ showRedactions: value as boolean }),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
}),
), ),
SettingsStore.watchSetting("showJoinLeaves", null, () => SettingsStore.watchSetting("showJoinLeaves", roomId, (...[,,, value]) =>
this.setState({ this.setState({ showJoinLeaves: value as boolean }),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
}),
), ),
SettingsStore.watchSetting("showAvatarChanges", null, () => SettingsStore.watchSetting("showAvatarChanges", roomId, (...[,,, value]) =>
this.setState({ this.setState({ showAvatarChanges: value as boolean }),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
}),
), ),
SettingsStore.watchSetting("showDisplaynameChanges", null, () => SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[,,, value]) =>
this.setState({ this.setState({ showDisplaynameChanges: value as boolean }),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
}),
), ),
]); ]);
@ -1730,7 +1740,8 @@ export default class RoomView extends React.Component<IProps, IState> {
onJoinClick={this.onJoinButtonClicked} onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError} canPreview={false}
error={this.state.roomLoadError}
roomAlias={roomAlias} roomAlias={roomAlias}
joining={this.state.joining} joining={this.state.joining}
inviterName={inviterName} inviterName={inviterName}
@ -1892,10 +1903,10 @@ export default class RoomView extends React.Component<IProps, IState> {
className="mx_RoomView_auxPanel_hiddenHighlights" className="mx_RoomView_auxPanel_hiddenHighlights"
onClick={this.onHiddenHighlightsClick} onClick={this.onHiddenHighlightsClick}
> >
{_t( { _t(
"You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.",
{ count: hiddenHighlightCount }, { count: hiddenHighlightCount },
)} ) }
</AccessibleButton> </AccessibleButton>
); );
} }
@ -2007,7 +2018,7 @@ export default class RoomView extends React.Component<IProps, IState> {
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onUserScroll={this.onUserScroll} onUserScroll={this.onUserScroll}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar} onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview} showUrlPreview={this.state.showUrlPreview}
className={messagePanelClassNames} className={messagePanelClassNames}
membersLoaded={this.state.membersLoaded} membersLoaded={this.state.membersLoaded}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
@ -2057,7 +2068,7 @@ export default class RoomView extends React.Component<IProps, IState> {
return ( return (
<RoomContext.Provider value={this.state}> <RoomContext.Provider value={this.state}>
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> <main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && { showChatEffects && this.roomView.current &&
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
} }
<ErrorBoundary> <ErrorBoundary>
@ -2076,22 +2087,22 @@ export default class RoomView extends React.Component<IProps, IState> {
/> />
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}> <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className="mx_RoomView_body"> <div className="mx_RoomView_body">
{auxPanel} { auxPanel }
<div className={timelineClasses}> <div className={timelineClasses}>
{fileDropTarget} { fileDropTarget }
{topUnreadMessagesBar} { topUnreadMessagesBar }
{jumpToBottom} { jumpToBottom }
{messagePanel} { messagePanel }
{searchResultsPanel} { searchResultsPanel }
</div> </div>
<div className={statusBarAreaClass}> <div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox"> <div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" /> <div className="mx_RoomView_statusAreaBox_line" />
{statusBar} { statusBar }
</div> </div>
</div> </div>
{previewBar} { previewBar }
{messageComposer} { messageComposer }
</div> </div>
</MainSplit> </MainSplit>
</ErrorBoundary> </ErrorBoundary>

View file

@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component<IProps> {
private readonly itemlist = createRef<HTMLOListElement>(); private readonly itemlist = createRef<HTMLOListElement>();
private unmounted = false; private unmounted = false;
private scrollTimeout: Timer; private scrollTimeout: Timer;
// Are we currently trying to backfill?
private isFilling: boolean; private isFilling: boolean;
// Is the current fill request caused by a props update?
private isFillingDueToPropsUpdate = false;
// Did another request to check the fill state arrive while we were trying to backfill?
private fillRequestWhileRunning: boolean; private fillRequestWhileRunning: boolean;
// Is that next fill request scheduled because of a props update?
private pendingFillDueToPropsUpdate: boolean;
private scrollState: IScrollState; private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState; private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: number; private unfillDebouncer: number;
@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component<IProps> {
// adding events to the top). // adding events to the top).
// //
// This will also re-check the fill state, in case the paginate was inadequate // This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll(); this.checkScroll(true);
this.updatePreventShrinking(); this.updatePreventShrinking();
} }
@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component<IProps> {
// after an update to the contents of the panel, check that the scroll is // after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary. // where it ought to be, and set off pagination requests if necessary.
public checkScroll = () => { public checkScroll = (isFromPropsUpdate = false) => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
this.restoreSavedScrollState(); this.restoreSavedScrollState();
this.checkFillState(); this.checkFillState(0, isFromPropsUpdate);
}; };
// return true if the content is fully scrolled down right now; else false. // return true if the content is fully scrolled down right now; else false.
@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component<IProps> {
} }
// check the scroll state and send out backfill requests if necessary. // check the scroll state and send out backfill requests if necessary.
public checkFillState = async (depth = 0): Promise<void> => { public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise<void> => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component<IProps> {
// don't allow more than 1 chain of calls concurrently // don't allow more than 1 chain of calls concurrently
// do make a note when a new request comes in while already running one, // do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done. // so we can trigger a new chain of calls once done.
// However, we make an exception for when we're already filling due to a
// props (or children) update, because very often the children include
// spinners to say whether we're paginating or not, so this would cause
// infinite paginating.
if (isFirstCall) { if (isFirstCall) {
if (this.isFilling) { if (this.isFilling && !this.isFillingDueToPropsUpdate) {
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request"); debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
this.fillRequestWhileRunning = true; this.fillRequestWhileRunning = true;
this.pendingFillDueToPropsUpdate = isFromPropsUpdate;
return; return;
} }
debuglog("isFilling: setting"); debuglog("isFilling: setting");
this.isFilling = true; this.isFilling = true;
this.isFillingDueToPropsUpdate = isFromPropsUpdate;
} }
const itemlist = this.itemlist.current; const itemlist = this.itemlist.current;
@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component<IProps> {
if (isFirstCall) { if (isFirstCall) {
debuglog("isFilling: clearing"); debuglog("isFilling: clearing");
this.isFilling = false; this.isFilling = false;
this.isFillingDueToPropsUpdate = false;
} }
if (this.fillRequestWhileRunning) { if (this.fillRequestWhileRunning) {
const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
this.fillRequestWhileRunning = false; this.fillRequestWhileRunning = false;
this.checkFillState(); this.pendingFillDueToPropsUpdate = false;
this.checkFillState(0, refillDueToPropsUpdate);
} }
}; };

View file

@ -136,8 +136,8 @@ export default class SearchBox extends React.Component {
key="button" key="button"
tabIndex={-1} tabIndex={-1}
className="mx_SearchBox_closeButton" className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }> onClick={() => {this._clearSearch("button"); }}
</AccessibleButton>) : undefined; />) : undefined;
// show a shorter placeholder when blurred, if requested // show a shorter placeholder when blurred, if requested
// this is used for the room filter field that has // this is used for the room filter field that has
@ -153,12 +153,12 @@ export default class SearchBox extends React.Component {
type="text" type="text"
ref={this._search} ref={this._search}
className={"mx_textinput_icon mx_textinput_search " + className} className={"mx_textinput_icon mx_textinput_search " + className}
value={ this.state.searchTerm } value={this.state.searchTerm}
onFocus={ this._onFocus } onFocus={this._onFocus}
onChange={ this.onChange } onChange={this.onChange}
onKeyDown={ this._onKeyDown } onKeyDown={this._onKeyDown}
onBlur={this._onBlur} onBlur={this._onBlur}
placeholder={ placeholder } placeholder={placeholder}
autoComplete="off" autoComplete="off"
autoFocus={this.props.autoFocus} autoFocus={this.props.autoFocus}
/> />

View file

@ -404,7 +404,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
if (summaryError) { if (summaryError) {
return <p>{_t("Your server does not support showing space hierarchies.")}</p>; return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
} }
let content; let content;
@ -569,7 +569,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
return <> return <>
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and descriptions") } placeholder={_t("Search names and descriptions")}
onSearch={setQuery} onSearch={setQuery}
autoFocus={true} autoFocus={true}
initialValue={initialText} initialValue={initialText}
@ -608,7 +608,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }
{ _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", { _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
null, null,
{ a: sub => { { a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>; return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{ sub }</AccessibleButton>;
} }, } },
) } ) }

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { RefObject, useContext, useRef, useState } from "react"; import React, { RefObject, useContext, useRef, useState } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { Preset } from "matrix-js-sdk/src/@types/partials"; import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { EventSubscription } from "fbemitter"; import { EventSubscription } from "fbemitter";
@ -66,7 +66,6 @@ import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
interface IProps { interface IProps {
space: Room; space: Room;
@ -101,12 +100,14 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
<hr /> <hr />
<div> <div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span> <span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton kind="link" onClick={() => { <AccessibleButton
if (onClick) onClick(); kind="link"
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces", featureId: "feature_spaces",
}); });
}}> }}>
{ _t("Feedback") } { _t("Feedback") }
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -146,7 +147,7 @@ const SpaceInfo = ({ space }) => {
return <div className="mx_SpaceRoomView_info"> return <div className="mx_SpaceRoomView_info">
{ visibilitySection } { visibilitySection }
{ joinRule === "public" && <RoomMemberCount room={space}> { joinRule === "public" && <RoomMemberCount room={space}>
{(count) => count > 0 ? ( { (count) => count > 0 ? (
<AccessibleButton <AccessibleButton
kind="link" kind="link"
onClick={() => { onClick={() => {
@ -159,7 +160,7 @@ const SpaceInfo = ({ space }) => {
> >
{ _t("%(count)s members", { count }) } { _t("%(count)s members", { count }) }
</AccessibleButton> </AccessibleButton>
) : null} ) : null }
</RoomMemberCount> } </RoomMemberCount> }
</div>; </div>;
}; };
@ -292,7 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</h1> </h1>
<SpaceInfo space={space} /> <SpaceInfo space={space} />
<RoomTopic room={space}> <RoomTopic room={space}>
{(topic, ref) => { (topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}> <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
{ topic } { topic }
</div> </div>
@ -307,7 +308,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
}; };
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const cli = useContext(MatrixClientContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu; let contextMenu;
@ -330,7 +330,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
e.stopPropagation(); e.stopPropagation();
closeMenu(); closeMenu();
if (await showCreateNewRoom(cli, space)) { if (await showCreateNewRoom(space)) {
onNewRoomAdded(); onNewRoomAdded();
} }
}} }}
@ -343,7 +343,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
e.stopPropagation(); e.stopPropagation();
closeMenu(); closeMenu();
const [added] = await showAddExistingRooms(cli, space); const [added] = await showAddExistingRooms(space);
if (added) { if (added) {
onNewRoomAdded(); onNewRoomAdded();
} }
@ -397,11 +397,11 @@ const SpaceLanding = ({ space }) => {
} }
let settingsButton; let settingsButton;
if (shouldShowSpaceSettings(cli, space)) { if (shouldShowSpaceSettings(space)) {
settingsButton = <AccessibleTooltipButton settingsButton = <AccessibleTooltipButton
className="mx_SpaceRoomView_landing_settingsButton" className="mx_SpaceRoomView_landing_settingsButton"
onClick={() => { onClick={() => {
showSpaceSettings(cli, space); showSpaceSettings(space);
}} }}
title={_t("Settings")} title={_t("Settings")}
/>; />;
@ -419,12 +419,12 @@ const SpaceLanding = ({ space }) => {
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<div className="mx_SpaceRoomView_landing_name"> <div className="mx_SpaceRoomView_landing_name">
<RoomName room={space}> <RoomName room={space}>
{(name) => { { (name) => {
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow"> const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
<h1>{ name }</h1> <h1>{ name }</h1>
</div> }; </div> };
return _t("Welcome to <name/>", {}, tags) as JSX.Element; return _t("Welcome to <name/>", {}, tags) as JSX.Element;
}} } }
</RoomName> </RoomName>
</div> </div>
<div className="mx_SpaceRoomView_landing_info"> <div className="mx_SpaceRoomView_landing_info">
@ -434,11 +434,11 @@ const SpaceLanding = ({ space }) => {
{ settingsButton } { settingsButton }
</div> </div>
<RoomTopic room={space}> <RoomTopic room={space}>
{(topic, ref) => ( { (topic, ref) => (
<div className="mx_SpaceRoomView_landing_topic" ref={ref}> <div className="mx_SpaceRoomView_landing_topic" ref={ref}>
{ topic } { topic }
</div> </div>
)} ) }
</RoomTopic> </RoomTopic>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
<hr /> <hr />
@ -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}
@ -553,9 +553,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
onFinished={onFinished} onFinished={onFinished}
/> />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons" />
</div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -625,7 +623,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,23 +94,23 @@ 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 (
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}> <AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
{tabIcon} { tabIcon }
<span className="mx_TabbedView_tabLabel_text"> <span className="mx_TabbedView_tabLabel_text">
{ label } { label }
</span> </span>
@ -118,19 +118,19 @@ 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'>
{tab.body} { tab.body }
</AutoHideScrollbar> </AutoHideScrollbar>
</div> </div>
); );
} }
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,
@ -141,9 +141,9 @@ export default class TabbedView extends React.Component<IProps, IState> {
return ( return (
<div className={tabbedViewClasses}> <div className={tabbedViewClasses}>
<div className="mx_TabbedView_tabLabels"> <div className="mx_TabbedView_tabLabels">
{labels} { labels }
</div> </div>
{panel} { panel }
</div> </div>
); );
} }

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");
@ -665,8 +665,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
private readMarkerTimeout(readMarkerPosition: number): number { private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ? return readMarkerPosition === 0 ?
this.state.readMarkerInViewThresholdMs : this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs :
this.state.readMarkerOutOfViewThresholdMs; this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs;
} }
private async updateReadMarkerOnUserActivity(): Promise<void> { private async updateReadMarkerOnUserActivity(): Promise<void> {
@ -1448,7 +1448,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) { if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return ( return (
<div className={this.props.className + " mx_RoomView_messageListWrapper"}> <div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<div className="mx_RoomView_empty">{this.props.empty}</div> <div className="mx_RoomView_empty">{ this.props.empty }</div>
</div> </div>
); );
} }
@ -1493,8 +1493,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
onUserScroll={this.props.onUserScroll} onUserScroll={this.props.onUserScroll}
onFillRequest={this.onMessageListFillRequest} onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest} onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour} isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps} alwaysShowTimestamps={
this.props.alwaysShowTimestamps ??
this.context?.alwaysShowTimestamps ??
this.state.alwaysShowTimestamps
}
className={this.props.className} className={this.props.className}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}

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