Merge branch 'develop' into t3chguy/fix/18071
This commit is contained in:
commit
39d9ec3ced
464 changed files with 10033 additions and 6253 deletions
|
@ -46,6 +46,7 @@
|
|||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 src test",
|
||||
"lint:js-fix": "eslint --fix src test",
|
||||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||
"test": "jest",
|
||||
|
@ -64,8 +65,8 @@
|
|||
"counterpart": "^0.18.6",
|
||||
"diff-dom": "^4.2.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"emojibase-data": "^5.1.1",
|
||||
"emojibase-regex": "^4.1.1",
|
||||
"emojibase-data": "^6.2.0",
|
||||
"emojibase-regex": "^5.1.3",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "6.1.0",
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||
@import "./views/dialogs/_LeaveSpaceDialog.scss";
|
||||
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||
|
@ -160,6 +161,7 @@
|
|||
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||
@import "./views/groups/_GroupRoomList.scss";
|
||||
@import "./views/groups/_GroupUserSettings.scss";
|
||||
@import "./views/messages/_CallEvent.scss";
|
||||
@import "./views/messages/_CreateEvent.scss";
|
||||
@import "./views/messages/_DateSeparator.scss";
|
||||
@import "./views/messages/_EventTileBubble.scss";
|
||||
|
@ -172,7 +174,6 @@
|
|||
@import "./views/messages/_MStickerBody.scss";
|
||||
@import "./views/messages/_MTextBody.scss";
|
||||
@import "./views/messages/_MVideoBody.scss";
|
||||
@import "./views/messages/_MVoiceMessageBody.scss";
|
||||
@import "./views/messages/_MediaBody.scss";
|
||||
@import "./views/messages/_MessageActionBar.scss";
|
||||
@import "./views/messages/_MessageTimestamp.scss";
|
||||
|
|
|
@ -45,9 +45,14 @@ limitations under the License.
|
|||
|
||||
/* Overrides for the attachment body tiles */
|
||||
|
||||
.mx_FilePanel .mx_EventTile {
|
||||
.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
|
||||
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 {
|
||||
|
@ -118,10 +123,6 @@ limitations under the License.
|
|||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
|
||||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
||||
.mx_FilePanel_empty::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ limitations under the License.
|
|||
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
|
||||
position: relative;
|
||||
|
||||
|
@ -105,7 +105,7 @@ limitations under the License.
|
|||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_line {
|
||||
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
|
||||
margin-right: 0px;
|
||||
padding-left: 36px; // align with the room name
|
||||
padding-top: 0px;
|
||||
|
|
|
@ -190,7 +190,6 @@ limitations under the License.
|
|||
position: relative;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
min-height: 56px;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: grid;
|
||||
|
|
|
@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
|
||||
.mx_SpaceRoomView_landing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .mx_BaseAvatar_image,
|
||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||
border-radius: 12px;
|
||||
|
@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
|
||||
.mx_SearchBox {
|
||||
margin: 0 0 20px;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.mx_SpaceFeedbackPrompt {
|
||||
|
@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomDirectory_list {
|
||||
// we don't want this container to get forced into the flexbox layout
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope {
|
||||
|
|
|
@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_AudioPlayer_container {
|
||||
.mx_MediaBody.mx_AudioPlayer_container {
|
||||
padding: 16px 12px 12px 12px;
|
||||
max-width: 267px; // use max to make the control fit in the files/pinned panels
|
||||
|
||||
.mx_AudioPlayer_primaryContainer {
|
||||
display: flex;
|
||||
|
|
|
@ -18,10 +18,10 @@ limitations under the License.
|
|||
// are shared amongst multiple voice message components.
|
||||
|
||||
// Container for live recording and playback controls
|
||||
.mx_VoiceMessagePrimaryContainer {
|
||||
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||
// has a 1px padding on it that we want to account for.
|
||||
padding: 7px 12px 7px 11px;
|
||||
.mx_MediaBody.mx_VoiceMessagePrimaryContainer {
|
||||
// The waveform (right) has a 1px padding on it that we want to account for, otherwise
|
||||
// inherit from mx_MediaBody
|
||||
padding-right: 11px;
|
||||
|
||||
// Cheat at alignment a bit
|
||||
display: flex;
|
||||
|
|
|
@ -27,7 +27,6 @@ limitations under the License.
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mx_BaseAvatar_initial {
|
||||
|
|
|
@ -65,7 +65,7 @@ limitations under the License.
|
|||
.mx_CreateRoomDialog_aliasContainer {
|
||||
display: flex;
|
||||
// put margin on container so it can collapse with siblings
|
||||
margin: 10px 0;
|
||||
margin: 24px 0 10px;
|
||||
|
||||
.mx_RoomAliasField {
|
||||
margin: 0;
|
||||
|
@ -101,10 +101,6 @@ limitations under the License.
|
|||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.mx_CreateRoomDialog_topic {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content > .mx_SettingsFlag {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
@ -113,5 +109,56 @@ limitations under the License.
|
|||
margin: 0 85px 0 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@ limitations under the License.
|
|||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.mx_EventTile[data-layout=bubble] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
div {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal file
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $strong-input-border-color;
|
||||
font-size: $font-12px;
|
||||
user-select: none;
|
||||
|
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
|
|||
z-index: 2;
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $input-focused-border-color;
|
||||
background-color: $primary-bg-color;
|
||||
max-height: 200px;
|
||||
|
|
|
@ -30,5 +30,12 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_InfoTooltip_icon_info::before {
|
||||
mask-image: url('$(res)/img/element-icons/info.svg');
|
||||
}
|
||||
|
||||
.mx_InfoTooltip_icon_warning::before {
|
||||
mask-image: url('$(res)/img/element-icons/warning.svg');
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@ limitations under the License.
|
|||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid $button-bg-color;
|
||||
padding: 0 10px;
|
||||
border-left: 2px solid $button-bg-color;
|
||||
border-radius: 2px;
|
||||
|
||||
.mx_ReplyThread_show {
|
||||
cursor: pointer;
|
||||
|
|
162
res/css/views/messages/_CallEvent.scss
Normal file
162
res/css/views/messages/_CallEvent.scss
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,12 +60,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.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 {
|
||||
background-color: $message-body-panel-icon-bg-color;
|
||||
border-radius: 20px;
|
||||
|
|
|
@ -16,23 +16,15 @@ limitations under the License.
|
|||
|
||||
$timelineImageBorderRadius: 4px;
|
||||
|
||||
.mx_MImageBody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_MImageBody_thumbnail {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
object-fit: contain;
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> canvas {
|
||||
> div > canvas {
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,11 @@ limitations under the License.
|
|||
.mx_MediaBody {
|
||||
background-color: $message-body-panel-bg-color;
|
||||
border-radius: 12px;
|
||||
max-width: 243px; // use max-width instead of width so it fits within right panels
|
||||
|
||||
color: $message-body-panel-fg-color;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
|
|
@ -107,3 +107,12 @@ limitations under the License.
|
|||
.mx_MessageActionBar_cancelButton::after {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_EventTile[data-layout=bubble],
|
||||
.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
|
||||
.mx_EventListSummary[data-layout=bubble] {
|
||||
--avatarSize: 32px;
|
||||
--gutterSize: 11px;
|
||||
--cornerRadius: 12px;
|
||||
|
@ -38,7 +38,8 @@ limitations under the License.
|
|||
padding-top: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.mx_EventTile_selected {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
@ -80,7 +81,7 @@ limitations under the License.
|
|||
|
||||
.mx_MessageActionBar {
|
||||
right: 0;
|
||||
transform: translate3d(50%, 50%, 0);
|
||||
transform: translate3d(90%, 50%, 0);
|
||||
}
|
||||
|
||||
--backgroundColor: $eventbubble-others-bg;
|
||||
|
@ -91,12 +92,17 @@ limitations under the License.
|
|||
float: right;
|
||||
> a {
|
||||
left: auto;
|
||||
right: -48px;
|
||||
right: -68px;
|
||||
}
|
||||
}
|
||||
.mx_SenderProfile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_ReplyTile .mx_SenderProfile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_ReactionsRow {
|
||||
float: right;
|
||||
clear: right;
|
||||
|
@ -126,7 +132,9 @@ limitations under the License.
|
|||
margin: 0 -12px 0 -9px;
|
||||
> a {
|
||||
position: absolute;
|
||||
left: -48px;
|
||||
padding: 10px 20px;
|
||||
top: 0;
|
||||
left: -68px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,12 +156,24 @@ limitations under the License.
|
|||
position: absolute;
|
||||
top: 0;
|
||||
line-height: 1;
|
||||
z-index: 9;
|
||||
img {
|
||||
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_noSender {
|
||||
.mx_EventTile_avatar {
|
||||
top: -19px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BaseAvatar,
|
||||
.mx_EventTile_avatar {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&[data-has-reply=true] {
|
||||
> .mx_EventTile_line {
|
||||
flex-direction: column;
|
||||
|
@ -204,89 +224,6 @@ limitations under the License.
|
|||
border-left-color: $eventbubble-reply-color;
|
||||
}
|
||||
|
||||
&.mx_EventTile_bubbleContainer,
|
||||
&.mx_EventTile_info,
|
||||
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||
--backgroundColor: transparent;
|
||||
--gutterSize: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
position: static;
|
||||
order: -1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .mx_EventListSummary {
|
||||
--maxWidth: 80%;
|
||||
margin-left: calc(var(--avatarSize) + var(--gutterSize));
|
||||
margin-right: calc(var(--gutterSize) + var(--avatarSize));
|
||||
.mx_EventListSummary_toggle {
|
||||
float: none;
|
||||
margin: 0;
|
||||
order: 9;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.mx_EventListSummary_avatars {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mx_EventTile {
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
margin: 0 5px;
|
||||
> a {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(calc(100% + 5px));
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageActionBar {
|
||||
transform: translate3d(50%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||
padding: 0 34px;
|
||||
}
|
||||
|
||||
/* events that do not require bubble layout */
|
||||
& ~ .mx_EventListSummary,
|
||||
&.mx_EventTile_bad {
|
||||
.mx_EventTile_line {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& + .mx_EventListSummary {
|
||||
.mx_EventTile {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventListSummary_toggle {
|
||||
margin-right: 55px;
|
||||
}
|
||||
|
||||
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
|
||||
&.mx_EventTile_bad > .mx_EventTile_line {
|
||||
display: grid;
|
||||
|
@ -321,3 +258,78 @@ limitations under the License.
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
& ~ .mx_EventListSummary .mx_EventTile_line {
|
||||
padding-left: calc($left-gutter);
|
||||
}
|
||||
|
||||
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||
}
|
||||
|
@ -208,43 +213,11 @@ $hover-select-border: 4px;
|
|||
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 */
|
||||
.mx_EventTile_body {
|
||||
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_unverified .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_EventTile:not([data-layout=bubble]) {
|
||||
|
@ -333,6 +336,13 @@ $hover-select-border: 4px;
|
|||
.mx_EventTile_msgOption {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mx_EventTile_line {
|
||||
// To avoid bubble events being highlighted
|
||||
background-color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
|
@ -462,6 +472,10 @@ $hover-select-border: 4px;
|
|||
background-color: $header-panel-bg-color;
|
||||
}
|
||||
|
||||
pre code > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
pre {
|
||||
// have to use overlay rather than auto otherwise Linux and Windows
|
||||
// Chrome gets very confused about vertical spacing:
|
||||
|
@ -559,6 +573,12 @@ $hover-select-border: 4px;
|
|||
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 {
|
||||
display: inline !important;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ $left-gutter: 64px;
|
|||
|
||||
> .mx_EventTile_avatar {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
|
|
|
@ -116,6 +116,11 @@ $irc-line-height: $font-18px;
|
|||
.mx_EditMessageComposer_buttons {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_ReactionsRow {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_emote {
|
||||
|
|
|
@ -19,7 +19,8 @@ limitations under the License.
|
|||
margin-right: 15px;
|
||||
margin-bottom: 15px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -33,7 +34,7 @@ limitations under the License.
|
|||
.mx_LinkPreviewWidget_caption {
|
||||
margin-left: 15px;
|
||||
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 {
|
||||
|
|
|
@ -60,8 +60,6 @@ limitations under the License.
|
|||
$reply-lines: 2;
|
||||
$line-height: $font-22px;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
|
@ -29,8 +29,10 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
// min-height at this level so the mx_BasicMessageComposer_input
|
||||
// still stays vertically centered when less than 50px
|
||||
min-height: 50px;
|
||||
// still stays vertically centered when less than 55px.
|
||||
// We also set this to ensure the voice message recording widget
|
||||
// doesn't cause a jump.
|
||||
min-height: 55px;
|
||||
|
||||
.mx_BasicMessageComposer_input {
|
||||
padding: 3px 0;
|
||||
|
|
|
@ -47,14 +47,14 @@ limitations under the License.
|
|||
color: $settings-subsection-fg-color;
|
||||
font-size: $font-14px;
|
||||
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 {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_SettingsFlag {
|
||||
margin-right: 100px;
|
||||
margin-right: 80px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
|
|||
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 {
|
||||
display: block;
|
||||
|
||||
|
@ -26,5 +64,51 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider,
|
||||
.mx_AppearanceUserSettingsTab_fontSlider_preview,
|
||||
.mx_AppearanceUserSettingsTab_Layout {
|
||||
.mx_AppearanceUserSettingsTab_fontSlider_preview {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
||||
|
||||
|
@ -45,6 +44,11 @@ limitations under the License.
|
|||
border-radius: 10px;
|
||||
padding: 0 16px 9px 16px;
|
||||
pointer-events: none;
|
||||
display: flow-root;
|
||||
|
||||
.mx_EventTile[data-layout=bubble] {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mx_EventTile_msgOption {
|
||||
display: none;
|
||||
|
@ -154,13 +158,10 @@ limitations under the License.
|
|||
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
|
||||
color: $primary-fg-color;
|
||||
|
||||
.mx_AppearanceUserSettingsTab_spacer {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
@ -210,6 +211,21 @@ limitations under the License.
|
|||
.mx_RadioButton_checked {
|
||||
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 {
|
||||
|
|
Binary file not shown.
Binary file not shown.
3
res/img/element-icons/warning.svg
Normal file
3
res/img/element-icons/warning.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="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 |
|
@ -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 |
3
res/img/voip/missed-video.svg
Normal file
3
res/img/voip/missed-video.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="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 |
4
res/img/voip/missed-voice.svg
Normal file
4
res/img/voip/missed-voice.svg
Normal 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 |
|
@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
|||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||
$message-body-panel-icon-fg-color: #21262C; // "Separator"
|
||||
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: #21262C; // "System Dark"
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
|
@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD;
|
|||
.hljs-tag {
|
||||
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #1a4b59;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #53232a;
|
||||
}
|
||||
|
|
|
@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
|||
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049;
|
||||
$message-body-panel-icon-fg-color: $primary-bg-color;
|
||||
$message-body-panel-icon-bg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: #21262C;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
|
|
|
@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
|||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0;
|
||||
$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
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
|
|
|
@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
|||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||
$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
|
||||
// want custom themes to affect them by accident.
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { JSXElementConstructor } from "react";
|
||||
import React, { JSXElementConstructor } from "react";
|
||||
|
||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||
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 ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
||||
export type ReactAnyComponent = React.Component | React.ExoticComponent;
|
||||
|
|
22
src/@types/global.d.ts
vendored
22
src/@types/global.d.ts
vendored
|
@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore";
|
|||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
matrixChat: ReturnType<Renderer>;
|
||||
|
@ -90,6 +92,7 @@ declare global {
|
|||
mxUIStore: UIStore;
|
||||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||
mxOnRecaptchaLoaded?: () => void;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
@ -114,7 +117,7 @@ declare global {
|
|||
}
|
||||
|
||||
interface StorageEstimate {
|
||||
usageDetails?: {[key: string]: number};
|
||||
usageDetails?: { [key: string]: number };
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
|
@ -185,4 +188,21 @@ declare global {
|
|||
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
20
src/@types/svg.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -270,7 +270,7 @@ export class Analytics {
|
|||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||
}
|
||||
|
||||
private async _track(data: IData) {
|
||||
private async track(data: IData) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const now = new Date();
|
||||
|
@ -304,7 +304,7 @@ export class Analytics {
|
|||
}
|
||||
|
||||
public ping() {
|
||||
this._track({
|
||||
this.track({
|
||||
ping: "1",
|
||||
});
|
||||
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
|
||||
}
|
||||
|
||||
this._track({
|
||||
this.track({
|
||||
gt_ms: String(generationTimeMs),
|
||||
});
|
||||
}
|
||||
|
||||
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
||||
if (this.disabled) return;
|
||||
this._track({
|
||||
this.track({
|
||||
e_c: category,
|
||||
e_a: action,
|
||||
e_n: name,
|
||||
|
@ -395,17 +395,17 @@ export class Analytics {
|
|||
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||
title: _t('Analytics'),
|
||||
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,
|
||||
})}</div>
|
||||
}) }</div>
|
||||
<table>
|
||||
{ rows.map((row) => <tr key={row[0]}>
|
||||
<td>{_t(
|
||||
<td>{ _t(
|
||||
customVariables[row[0]].expl,
|
||||
customVariables[row[0]].getTextVariables ?
|
||||
customVariables[row[0]].getTextVariables() :
|
||||
null,
|
||||
)}</td>
|
||||
) }</td>
|
||||
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
|
||||
</tr>) }
|
||||
{ otherVariables.map((item, index) =>
|
||||
|
|
|
@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
|||
// (and store the ID of their native room)
|
||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||
|
||||
export enum AudioID {
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
Ringback = 'ringbackAudio',
|
||||
CallEnd = 'callendAudio',
|
||||
|
@ -142,6 +142,7 @@ export enum PlaceCallType {
|
|||
export enum CallHandlerEvent {
|
||||
CallsChanged = "calls_changed",
|
||||
CallChangeRoom = "call_change_room",
|
||||
SilencedCallsChanged = "silenced_calls_changed",
|
||||
}
|
||||
|
||||
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
|
||||
private assertedIdentityNativeUsers = new Map<string, string>();
|
||||
|
||||
private silencedCalls = new Set<string>(); // callIds
|
||||
|
||||
static sharedInstance() {
|
||||
if (!window.mxCallHandler) {
|
||||
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) {
|
||||
try {
|
||||
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
||||
|
@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter {
|
|||
}, 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 {
|
||||
return this.calls.get(roomId) || null;
|
||||
}
|
||||
|
@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter {
|
|||
break;
|
||||
}
|
||||
|
||||
if (newState !== CallState.Ringing) {
|
||||
this.silencedCalls.delete(call.callId);
|
||||
}
|
||||
|
||||
switch (newState) {
|
||||
case CallState.Ringing:
|
||||
this.play(AudioID.Ring);
|
||||
|
@ -450,28 +491,18 @@ export default class CallHandler extends EventEmitter {
|
|||
break;
|
||||
case CallState.Ended:
|
||||
{
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
|
||||
const hangupReason = call.hangupReason;
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && (
|
||||
call.hangupParty === CallParty.Remote ||
|
||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||
)) {
|
||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||
this.play(AudioID.Busy);
|
||||
let title;
|
||||
let description;
|
||||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||
title = _t("Call Declined");
|
||||
description = _t("The other party declined the call.");
|
||||
} else if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
title = _t("User Busy");
|
||||
description = _t("The user you called is busy.");
|
||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||
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 {
|
||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
||||
title = _t("Call Failed");
|
||||
description = _t("The call could not be established");
|
||||
}
|
||||
|
@ -480,7 +511,7 @@ export default class CallHandler extends EventEmitter {
|
|||
title, description,
|
||||
});
|
||||
} else if (
|
||||
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
|
||||
hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
|
||||
) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title: _t("Answered Elsewhere"),
|
||||
|
@ -615,23 +646,23 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
private showICEFallbackPrompt() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const code = sub => <code>{sub}</code>;
|
||||
const code = sub => <code>{ sub }</code>;
|
||||
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
||||
title: _t("Call failed due to misconfigured server"),
|
||||
description: <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Please ask the administrator of your homeserver " +
|
||||
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
||||
"order for calls to work reliably.",
|
||||
{ homeserverDomain: cli.getDomain() }, { code },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Alternatively, you can try to use the public server at " +
|
||||
"<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 " +
|
||||
"this in Settings.",
|
||||
null, { code },
|
||||
)}</p>
|
||||
) }</p>
|
||||
</div>,
|
||||
button: _t('Try using turn.matrix.org'),
|
||||
cancelButton: _t('OK'),
|
||||
|
@ -649,19 +680,19 @@ export default class CallHandler extends EventEmitter {
|
|||
if (call.type === CallType.Voice) {
|
||||
title = _t("Unable to access microphone");
|
||||
description = <div>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Call failed because microphone could not be accessed. " +
|
||||
"Check that a microphone is plugged in and set up correctly.",
|
||||
)}
|
||||
) }
|
||||
</div>;
|
||||
} else if (call.type === CallType.Video) {
|
||||
title = _t("Unable to access webcam / microphone");
|
||||
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>
|
||||
<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("No other application is using the webcam")}</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("No other application is using the webcam") }</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -425,10 +425,10 @@ export default class ContentMessages {
|
|||
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
<div>{ _t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
) }</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
|
|
|
@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
|
|||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { ActionPayload } from "./dispatcher/payloads";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
|
@ -58,28 +59,28 @@ export default class DeviceListener {
|
|||
}
|
||||
|
||||
start() {
|
||||
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().on('sync', this._onSync);
|
||||
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
this._recheck();
|
||||
MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||
MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().on('accountData', this.onAccountData);
|
||||
MatrixClientPeg.get().on('sync', this.onSync);
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener('sync', this.onSync);
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -103,15 +104,15 @@ export default class DeviceListener {
|
|||
this.dismissed.add(d);
|
||||
}
|
||||
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
dismissEncryptionSetup() {
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
_ensureDeviceIdsAtStartPopulated() {
|
||||
private ensureDeviceIdsAtStartPopulated() {
|
||||
if (this.ourDeviceIdsAtStart === null) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
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),
|
||||
// then they are all pre-existing devices, so ignore this and set the
|
||||
// devicesAtStart list to the devices that we see after the fetch.
|
||||
if (initialFetch) return;
|
||||
|
||||
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
|
||||
// before we download any new ones.
|
||||
};
|
||||
|
||||
_onDevicesUpdated = (users: string[]) => {
|
||||
private onDevicesUpdated = (users: string[]) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
_onDeviceVerificationChanged = (userId: string) => {
|
||||
private onDeviceVerificationChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
_onUserTrustStatusChanged = (userId: string) => {
|
||||
private onUserTrustStatusChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
_onCrossSingingKeysChanged = () => {
|
||||
this._recheck();
|
||||
private onCrossSingingKeysChanged = () => {
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
_onAccountData = (ev) => {
|
||||
private onAccountData = (ev: MatrixEvent) => {
|
||||
// User may have:
|
||||
// * migrated SSSS to symmetric
|
||||
// * uploaded keys to secret storage
|
||||
|
@ -163,32 +164,32 @@ export default class DeviceListener {
|
|||
ev.getType().startsWith('m.cross_signing.') ||
|
||||
ev.getType() === 'm.megolm_backup.v1'
|
||||
) {
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
_onSync = (state, prevState) => {
|
||||
if (state === 'PREPARED' && prevState === null) this._recheck();
|
||||
private onSync = (state, prevState) => {
|
||||
if (state === 'PREPARED' && prevState === null) this.recheck();
|
||||
};
|
||||
|
||||
_onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
if (ev.getType() !== "m.room.encryption") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a room changes to encrypted, re-check as it may be our first
|
||||
// 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;
|
||||
this._recheck();
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
// The server doesn't tell us when key backup is set up, so we poll
|
||||
// & cache the result
|
||||
async _getKeyBackupInfo() {
|
||||
private async getKeyBackupInfo() {
|
||||
const now = (new Date()).getTime();
|
||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
|
@ -206,7 +207,7 @@ export default class DeviceListener {
|
|||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
private async recheck() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
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)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
} else {
|
||||
const backupInfo = await this._getKeyBackupInfo();
|
||||
const backupInfo = await this.getKeyBackupInfo();
|
||||
if (backupInfo) {
|
||||
// No cross-signing on account but key backup available (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
|
||||
// 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
|
||||
// (technically could just be a boolean: we don't actually
|
||||
|
|
|
@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
|||
import linkifyMatrix from './linkify-matrix';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
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 { 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}$/;
|
||||
|
||||
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)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||
|
||||
|
@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
|
|||
* @return {String} The shortcode (such as :thumbup:)
|
||||
*/
|
||||
export function unicodeToShortcode(char: string): string {
|
||||
const data = getEmojiFromUnicode(char);
|
||||
return (data && data.shortcodes ? `:${data.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;
|
||||
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
|
||||
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
|
||||
}
|
||||
|
||||
export function processHtmlForSending(html: string): string {
|
||||
|
|
|
@ -149,17 +149,17 @@ export default class IdentityAuthClient {
|
|||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"This action requires accessing the default identity server " +
|
||||
"<server /> to validate an email address or phone number, " +
|
||||
"but the server does not have any terms of service.", {},
|
||||
{
|
||||
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
|
||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||
},
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Only continue if you trust the owner of the server.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
|
|
|
@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
|
|||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class _MatrixClientPeg implements IMatrixClientPeg {
|
||||
class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
|
@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
}
|
||||
|
||||
if (!window.mxMatrixClientPeg) {
|
||||
window.mxMatrixClientPeg = new _MatrixClientPeg();
|
||||
window.mxMatrixClientPeg = new MatrixClientPegClass();
|
||||
}
|
||||
|
||||
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
||||
|
|
|
@ -378,7 +378,7 @@ export class ModalManager {
|
|||
const dialog = (
|
||||
<div className={classes}>
|
||||
<div className="mx_Dialog">
|
||||
{modal.elem}
|
||||
{ modal.elem }
|
||||
</div>
|
||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
||||
</div>
|
||||
|
|
|
@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
|
|||
description: _t("Use your account or create a new one to continue."),
|
||||
button: _t("Create Account"),
|
||||
extraButtons: [
|
||||
<button key="start_login" onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
|
||||
}}>{ _t('Sign In') }</button>,
|
||||
<button
|
||||
key="start_login"
|
||||
onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
|
||||
}}
|
||||
>
|
||||
{ _t('Sign In') }
|
||||
</button>,
|
||||
],
|
||||
onFinished: (proceed) => {
|
||||
if (proceed) {
|
||||
|
|
|
@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
|
|||
import { abbreviateUrl } from './utils/UrlUtils';
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
||||
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
||||
import { inviteUsersToRoom } from "./RoomInvite";
|
||||
import { WidgetType } from "./widgets/WidgetType";
|
||||
import { Jitsi } from "./widgets/Jitsi";
|
||||
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||
|
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
|
|||
import { CHAT_EFFECTS } from "./effects";
|
||||
import CallHandler from "./CallHandler";
|
||||
import { guessAndSetDMRoom } from "./Rooms";
|
||||
import { upgradeRoom } from './utils/RoomUpgrade';
|
||||
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
||||
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
||||
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
||||
|
@ -277,50 +277,8 @@ export const Commands = [
|
|||
/*isPriority=*/false, /*isStatic=*/true);
|
||||
|
||||
return success(finished.then(async ([resp]) => {
|
||||
if (!resp.continue) return;
|
||||
|
||||
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.'),
|
||||
});
|
||||
}
|
||||
if (!resp?.continue) return;
|
||||
await upgradeRoom(room, args, resp.invite);
|
||||
}));
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
|
@ -480,14 +438,14 @@ export const Commands = [
|
|||
'Identity server',
|
||||
QuestionDialog, {
|
||||
title: _t("Use an identity server"),
|
||||
description: <p>{_t(
|
||||
description: <p>{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Click continue to use the default identity server " +
|
||||
"(%(defaultIdentityServerName)s) or manage in Settings.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
)}</p>,
|
||||
) }</p>,
|
||||
button: _t("Continue"),
|
||||
},
|
||||
);
|
||||
|
@ -522,7 +480,7 @@ export const Commands = [
|
|||
aliases: ['j', 'goto'],
|
||||
args: '<room-address>',
|
||||
description: _td('Joins room with given address'),
|
||||
runFn: function(_, args) {
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
// Note: we support 2 versions of this command. The first is
|
||||
// the public-facing one for most users and the other is a
|
||||
|
@ -1069,7 +1027,7 @@ export const Commands = [
|
|||
command: "msg",
|
||||
description: _td("Sends a message to the given user"),
|
||||
args: "<user-id> <message>",
|
||||
runFn: function(_, args) {
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
// matches the first whitespace delimited group and then the rest of the string
|
||||
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { _t } from './languageHandler';
|
||||
import * as Roles from './Roles';
|
||||
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 {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
|
||||
|
@ -652,10 +567,6 @@ interface IHandlers {
|
|||
|
||||
const handlers: IHandlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.call.reject': textForCallRejectEvent,
|
||||
};
|
||||
|
||||
const stateHandlers: IHandlers = {
|
||||
|
|
|
@ -14,35 +14,33 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||
|
||||
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
||||
|
||||
export enum AddressType {
|
||||
Email = "email",
|
||||
MatrixUserId = "mx-user-id",
|
||||
MatrixRoomId = "mx-room-id",
|
||||
}
|
||||
|
||||
export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId];
|
||||
|
||||
// PropType definition for an object describing
|
||||
// an address that can be invited to a room (which
|
||||
// could be a third party identifier or a matrix ID)
|
||||
// along with some additional information about the
|
||||
// address / target.
|
||||
export const UserAddressType = PropTypes.shape({
|
||||
addressType: PropTypes.oneOf(addressTypes).isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string,
|
||||
avatarMxc: PropTypes.string,
|
||||
export interface IUserAddress {
|
||||
addressType: AddressType;
|
||||
address: string;
|
||||
displayName?: string;
|
||||
avatarMxc?: string;
|
||||
// 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 has entered)
|
||||
isKnown: PropTypes.bool,
|
||||
});
|
||||
isKnown?: boolean;
|
||||
}
|
||||
|
||||
export function getAddressType(inputText: string): AddressType | null {
|
||||
if (emailRegex.test(inputText)) {
|
||||
|
|
|
@ -370,8 +370,8 @@ export const toggleDialog = () => {
|
|||
const sections = categoryOrder.map(category => {
|
||||
const list = shortcuts[category];
|
||||
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
|
||||
<h3>{_t(category)}</h3>
|
||||
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>
|
||||
<h3>{ _t(category) }</h3>
|
||||
<div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
|
|
@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
|
|||
};
|
||||
|
||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
|
||||
{ ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
|
||||
{ children }
|
||||
</div>}
|
||||
</div> }
|
||||
</RovingTabIndexProvider>;
|
||||
};
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { _t } from '../../../../languageHandler';
|
||||
|
||||
|
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
|
|||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
disabling: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allows the user to disable the Event Index.
|
||||
*/
|
||||
export default class DisableEventIndexDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
disabling: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onDisable = async () => {
|
||||
private onDisable = async (): Promise<void> => {
|
||||
this.setState({
|
||||
disabling: true,
|
||||
});
|
||||
|
||||
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
||||
await EventIndexPeg.deleteEventIndex();
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(true);
|
||||
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 (
|
||||
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
||||
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
|
||||
{this.state.disabling ? <Spinner /> : <div />}
|
||||
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
|
||||
{ this.state.disabling ? <Spinner /> : <div /> }
|
||||
<DialogButtons
|
||||
primaryButton={_t('Disable')}
|
||||
onPrimaryButtonClick={this._onDisable}
|
||||
onPrimaryButtonClick={this.onDisable}
|
||||
primaryButtonClass="danger"
|
||||
cancelButtonClass="warning"
|
||||
onCancel={this.props.onFinished}
|
|
@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
|||
}
|
||||
|
||||
private onDisable = async () => {
|
||||
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
||||
import("./DisableEventIndexDialog"),
|
||||
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
|
||||
Modal.createTrackedDialog("Disable message search", "Disable message search",
|
||||
DisableEventIndexDialog,
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
};
|
||||
|
@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
|||
|
||||
const eventIndexingSettings = (
|
||||
<div>
|
||||
{_t(
|
||||
{ _t(
|
||||
"%(brand)s is securely caching encrypted messages locally for them " +
|
||||
"to appear in search results:",
|
||||
{ brand },
|
||||
)}
|
||||
) }
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{crawlerState}<br />
|
||||
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
||||
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
||||
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
|
||||
{ crawlerState }<br />
|
||||
{ _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
|
||||
{ _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
|
||||
{ _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
|
||||
doneRooms: formatCountLong(doneRooms),
|
||||
totalRooms: formatCountLong(this.state.roomCount),
|
||||
})} <br />
|
||||
}) } <br />
|
||||
<Field
|
||||
label={_t('Message downloading sleep time(ms)')}
|
||||
type='number'
|
||||
|
@ -188,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
|||
onFinished={this.props.onFinished}
|
||||
title={_t("Message search")}
|
||||
>
|
||||
{eventIndexingSettings}
|
||||
{ eventIndexingSettings }
|
||||
<DialogButtons
|
||||
primaryButton={_t("Done")}
|
||||
onPrimaryButtonClick={this.props.onFinished}
|
||||
|
|
|
@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
{ b: sub => <b>{ sub }</b> },
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"We'll store an encrypted copy of your keys on our server. " +
|
||||
"Secure your backup with a Security Phrase.",
|
||||
)}</p>
|
||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
||||
) }</p>
|
||||
<p>{ _t("For maximum security, this should be different from your account password.") }</p>
|
||||
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
|
@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
/>
|
||||
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||
{_t("Set up with a Security Key")}
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
|
||||
{ _t("Set up with a Security Key") }
|
||||
</AccessibleButton>
|
||||
</details>
|
||||
</form>;
|
||||
|
@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
||||
<div>{matchText}</div>
|
||||
<div>{ matchText }</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{changeText}
|
||||
{ changeText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
<div>
|
||||
|
@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
{passPhraseMatch}
|
||||
{ passPhraseMatch }
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
_renderPhaseShowKey() {
|
||||
return <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Your Security Key is a safety net - you can use it to restore " +
|
||||
"access to your encrypted messages if you forget your Security Phrase.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"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_recoveryKeyHeader">
|
||||
{_t("Your Security Key")}
|
||||
{ _t("Your Security Key") }
|
||||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
||||
<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 className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
|
||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||
{_t("Copy")}
|
||||
{ _t("Copy") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
{_t("Download")}
|
||||
{ _t("Download") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
if (this.state.copied) {
|
||||
introText = _t(
|
||||
"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) {
|
||||
introText = _t(
|
||||
"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');
|
||||
return <div>
|
||||
{introText}
|
||||
{ introText }
|
||||
<ul>
|
||||
<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>Copy it</b> to your personal cloud storage", {}, { 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>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
|
||||
</ul>
|
||||
<DialogButtons primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._createBackup}
|
||||
hasCancel={false}>
|
||||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
||||
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
_renderPhaseDone() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Your keys are being backed up (the first backup could take a few minutes).",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
hasCancel={false}
|
||||
|
@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
_renderPhaseOptOutConfirm() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
{_t(
|
||||
{ _t(
|
||||
"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.",
|
||||
)}
|
||||
) }
|
||||
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
|
||||
onPrimaryButtonClick={this._onSetUpClick}
|
||||
hasCancel={false}
|
||||
|
@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
if (this.state.error) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
content = <div>
|
||||
<p>{_t("Unable to create key backup")}</p>
|
||||
<p>{ _t("Unable to create key backup") }</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._createBackup}
|
||||
|
@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -474,10 +474,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
||||
{_t("Generate a Security Key")}
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
|
||||
{ _t("Generate a Security Key") }
|
||||
</div>
|
||||
<div>{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
|
||||
<div>{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
|
||||
</StyledRadioButton>
|
||||
);
|
||||
}
|
||||
|
@ -493,10 +493,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
||||
{_t("Enter a Security Phrase")}
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
|
||||
{ _t("Enter a Security Phrase") }
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
|
||||
|
||||
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 " +
|
||||
"backing up encryption keys on your server.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
||||
{optionKey}
|
||||
{optionPassphrase}
|
||||
{ optionKey }
|
||||
{ optionPassphrase }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Continue")}
|
||||
|
@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
let nextCaption = _t("Next");
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
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
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
|
@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</div>;
|
||||
} else if (!this.state.backupSigStatus.usable) {
|
||||
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>;
|
||||
nextCaption = _t("Restore");
|
||||
} else {
|
||||
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>;
|
||||
}
|
||||
|
||||
return <form onSubmit={this._onMigrateFormSubmit}>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Upgrade this session to allow it to verify other sessions, " +
|
||||
"granting them access to encrypted messages and marking them " +
|
||||
"as trusted for other users.",
|
||||
)}</p>
|
||||
<div>{authPrompt}</div>
|
||||
) }</p>
|
||||
<div>{ authPrompt }</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||
|
@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this._onCancelClick}>
|
||||
{_t('Skip')}
|
||||
{ _t('Skip') }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
|
@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
_renderPhasePassPhrase() {
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Enter a security phrase only you know, as it’s used to safeguard your data. " +
|
||||
"To be secure, you shouldn’t re-use your account password.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
|
@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
className="danger"
|
||||
>{_t("Cancel")}</button>
|
||||
>{ _t("Cancel") }</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div>
|
||||
<div>{matchText}</div>
|
||||
<div>{ matchText }</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{changeText}
|
||||
{ changeText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field
|
||||
type="password"
|
||||
|
@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
autoComplete="new-password"
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||
{passPhraseMatch}
|
||||
{ passPhraseMatch }
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
className="danger"
|
||||
>{_t("Skip")}</button>
|
||||
>{ _t("Skip") }</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
@ -691,35 +691,36 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
return <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Store your Security Key somewhere safe, like a password manager or a safe, " +
|
||||
"as it’s used to safeguard your encrypted data.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
|
||||
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||
<AccessibleButton kind='primary' className="mx_Dialog_primary"
|
||||
<AccessibleButton kind='primary'
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onDownloadClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
>
|
||||
{_t("Download")}
|
||||
{ _t("Download") }
|
||||
</AccessibleButton>
|
||||
<span>{_t("or")}</span>
|
||||
<span>{ _t("or") }</span>
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
||||
onClick={this._onCopyClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
>
|
||||
{this.state.copied ? _t("Copied!") : _t("Copy")}
|
||||
{ this.state.copied ? _t("Copied!") : _t("Copy") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{continueButton}
|
||||
{ continueButton }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -732,7 +733,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
_renderPhaseLoadError() {
|
||||
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">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._onLoadRetryClick}
|
||||
|
@ -745,17 +746,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
_renderPhaseSkipConfirm() {
|
||||
return <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"You can also set up Secure Backup & manage your keys in Settings.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<DialogButtons primaryButton={_t('Go back')}
|
||||
onPrimaryButtonClick={this._onGoBackClick}
|
||||
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>
|
||||
</div>;
|
||||
}
|
||||
|
@ -787,7 +788,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
let content;
|
||||
if (this.state.error) {
|
||||
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">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
|
@ -857,7 +858,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
fixedWidth={false}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this._passphrase1} id='passphrase1'
|
||||
autoFocus={true} size='64' type='password'
|
||||
<input
|
||||
ref={this._passphrase1}
|
||||
id='passphrase1'
|
||||
autoFocus={true}
|
||||
size='64'
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
|
@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this._passphrase2} id='passphrase2'
|
||||
size='64' type='password'
|
||||
<input ref={this._passphrase2}
|
||||
id='passphrase2'
|
||||
size='64'
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
|
|
|
@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
|||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{_t("New Recovery Method")}
|
||||
{ _t("New Recovery Method") }
|
||||
</span>;
|
||||
|
||||
const newMethodDetected = <p>{_t(
|
||||
const newMethodDetected = <p>{ _t(
|
||||
"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 " +
|
||||
"attacker may be trying to access your account. " +
|
||||
"Change your account password and set a new recovery " +
|
||||
"method immediately in Settings.",
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
|
||||
let content;
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
content = <div>
|
||||
{newMethodDetected}
|
||||
<p>{_t(
|
||||
{ newMethodDetected }
|
||||
<p>{ _t(
|
||||
"This session is encrypting history using the new recovery method.",
|
||||
)}</p>
|
||||
{hackWarning}
|
||||
) }</p>
|
||||
{ hackWarning }
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
onPrimaryButtonClick={this.onOkClick}
|
||||
|
@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
|||
</div>;
|
||||
} else {
|
||||
content = <div>
|
||||
{newMethodDetected}
|
||||
{hackWarning}
|
||||
{ newMethodDetected }
|
||||
{ hackWarning }
|
||||
<DialogButtons
|
||||
primaryButton={_t("Set up Secure Messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
|
@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
|||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
|||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{_t("Recovery Method Removed")}
|
||||
{ _t("Recovery Method Removed") }
|
||||
</span>;
|
||||
|
||||
return (
|
||||
|
@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
|||
title={title}
|
||||
>
|
||||
<div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"This session has detected that your Security Phrase and key " +
|
||||
"for Secure Messages have been removed.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"If you did this accidentally, you can setup Secure Messages on " +
|
||||
"this session which will re-encrypt this session's message " +
|
||||
"history with a new recovery method.",
|
||||
)}</p>
|
||||
<p className="warning">{_t(
|
||||
) }</p>
|
||||
<p className="warning">{ _t(
|
||||
"If you didn't remove the recovery method, an " +
|
||||
"attacker may be trying to access your account. " +
|
||||
"Change your account password and set a new recovery " +
|
||||
"method immediately in Settings.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Set up Secure Messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
|
|
37
src/audio/ManagedPlayback.ts
Normal file
37
src/audio/ManagedPlayback.ts
Normal 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();
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ export enum PlaybackState {
|
|||
|
||||
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
||||
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[] {
|
||||
// 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[];
|
||||
|
||||
private readonly context: AudioContext;
|
||||
private source: AudioBufferSourceNode;
|
||||
private source: AudioBufferSourceNode | MediaElementAudioSourceNode;
|
||||
private state = PlaybackState.Decoding;
|
||||
private audioBuf: AudioBuffer;
|
||||
private element: HTMLAudioElement;
|
||||
private resampledWaveform: number[];
|
||||
private waveformObservable = new SimpleObservable<number[]>();
|
||||
private readonly clock: PlaybackClock;
|
||||
|
@ -129,36 +130,64 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
this.removeAllListeners();
|
||||
this.clock.destroy();
|
||||
this.waveformObservable.close();
|
||||
if (this.element) {
|
||||
URL.revokeObjectURL(this.element.src);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public async prepare() {
|
||||
// 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 => {
|
||||
// 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...");
|
||||
// The point where we use an audio element is fairly arbitrary, though we don't want
|
||||
// it to be too low. As of writing, voice messages want to show a waveform but audio
|
||||
// messages do not. Using an audio element means we can't show a waveform preview, so
|
||||
// we try to target the difference between a voice message file and large audio file.
|
||||
// Overall, the point of this is to avoid memory-related issues due to storing a massive
|
||||
// audio buffer in memory, as that can balloon to far greater than the input buffer's
|
||||
// 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
|
||||
this.context.decodeAudioData(wav, b => resolve(b), e => {
|
||||
console.error("Still failed to decode recording: ", e);
|
||||
reject(e);
|
||||
// noinspection ES6MissingAwait - not needed when using callbacks
|
||||
this.context.decodeAudioData(wav, b => resolve(b), e => {
|
||||
console.error("Still failed to decode recording: ", 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
|
||||
// exactly trust the user-provided waveform to be accurate...
|
||||
const waveform = Array.from(this.audioBuf.getChannelData(0));
|
||||
this.resampledWaveform = makePlaybackWaveform(waveform);
|
||||
// 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...
|
||||
const waveform = Array.from(this.audioBuf.getChannelData(0));
|
||||
this.resampledWaveform = makePlaybackWaveform(waveform);
|
||||
}
|
||||
|
||||
this.waveformObservable.update(this.resampledWaveform);
|
||||
|
||||
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.durationSeconds = this.audioBuf.duration;
|
||||
this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration;
|
||||
}
|
||||
|
||||
private onPlaybackEnd = async () => {
|
||||
|
@ -171,7 +200,11 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
if (this.state === PlaybackState.Stopped) {
|
||||
this.disconnectSource();
|
||||
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
|
||||
|
@ -182,13 +215,21 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
}
|
||||
|
||||
private disconnectSource() {
|
||||
if (this.element) return; // leave connected, we can (and must) re-use it
|
||||
this.source?.disconnect();
|
||||
this.source?.removeEventListener("ended", this.onPlaybackEnd);
|
||||
}
|
||||
|
||||
private makeNewSourceBuffer() {
|
||||
this.source = this.context.createBufferSource();
|
||||
this.source.buffer = this.audioBuf;
|
||||
if (this.element && this.source) return; // leave connected, we can (and must) re-use it
|
||||
|
||||
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.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
|
||||
// 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.
|
||||
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
|
||||
// `this.pause()` is as small as possible: we do not want to delay *anything*
|
|
@ -103,8 +103,8 @@ export class PlaybackClock implements IDestroyable {
|
|||
* @param {MatrixEvent} event The event to use for placeholders.
|
||||
*/
|
||||
public populatePlaceholdersFrom(event: MatrixEvent) {
|
||||
const durationSeconds = Number(event.getContent()['info']?.['duration']);
|
||||
if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds;
|
||||
const durationMs = Number(event.getContent()['info']?.['duration']);
|
||||
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable {
|
|||
|
||||
public flagStop() {
|
||||
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) {
|
54
src/audio/PlaybackManager.ts
Normal file
54
src/audio/PlaybackManager.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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;
|
||||
}
|
||||
}
|
|
@ -333,12 +333,17 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
|
||||
if (this.lastUpload) return this.lastUpload;
|
||||
|
||||
this.emit(RecordingState.Uploading);
|
||||
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);
|
||||
try {
|
||||
this.emit(RecordingState.Uploading);
|
||||
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);
|
||||
} catch (e) {
|
||||
this.emit(RecordingState.Ended);
|
||||
throw e;
|
||||
}
|
||||
return this.lastUpload;
|
||||
}
|
||||
}
|
|
@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
|
|||
import { ICompletion, ISelectionRange } from './Autocompleter';
|
||||
import { uniq, sortBy } from 'lodash';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
import { EMOJI, IEmoji } from '../emoji';
|
||||
|
||||
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
|
||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
|
||||
|
||||
interface IEmojiShort {
|
||||
interface ISortedEmoji {
|
||||
emoji: IEmoji;
|
||||
shortname: string;
|
||||
_orderBy: number;
|
||||
}
|
||||
|
||||
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
|
||||
const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
|
||||
if (a.group === b.group) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
return a.group - b.group;
|
||||
}).map((emoji, index) => ({
|
||||
emoji,
|
||||
shortname: `:${emoji.shortcodes[0]}:`,
|
||||
// Include the index so that we can preserve the original order
|
||||
_orderBy: index,
|
||||
}));
|
||||
|
@ -64,20 +61,18 @@ function score(query, space) {
|
|||
}
|
||||
|
||||
export default class EmojiProvider extends AutocompleteProvider {
|
||||
matcher: QueryMatcher<IEmojiShort>;
|
||||
nameMatcher: QueryMatcher<IEmojiShort>;
|
||||
matcher: QueryMatcher<ISortedEmoji>;
|
||||
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||
|
||||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
|
||||
keys: ['emoji.emoticon', 'shortname'],
|
||||
funcs: [
|
||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
||||
],
|
||||
this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
|
||||
keys: ['emoji.emoticon'],
|
||||
funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
|
||||
// For matching against ascii equivalents
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||
this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
|
||||
keys: ['emoji.annotation'],
|
||||
// For removing punctuation
|
||||
shouldMatchWordsOnly: true,
|
||||
|
@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
|
||||
const sorters = [];
|
||||
// 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)
|
||||
sorters.push((c) => score(matchedString, c.shortname));
|
||||
// then sort by score (Infinity if matchedString not in shortcode)
|
||||
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
|
||||
// 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))));
|
||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||
sorters.push(c => Math.min(
|
||||
...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
|
||||
));
|
||||
// If the matchedString is not empty, sort by length of shortcode. Example:
|
||||
// matchedString = ":bookmark"
|
||||
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
||||
if (matchedString.length > 1) {
|
||||
sorters.push((c) => c.shortname.length);
|
||||
sorters.push(c => c.emoji.shortcodes[0].length);
|
||||
}
|
||||
// Finally, sort by original ordering
|
||||
sorters.push((c) => c._orderBy);
|
||||
sorters.push(c => c._orderBy);
|
||||
completions = sortBy(uniq(completions), sorters);
|
||||
|
||||
completions = completions.map(({ shortname }) => {
|
||||
const unicode = shortcodeToUnicode(shortname);
|
||||
return {
|
||||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} aria-label={unicode}>
|
||||
<span>{ unicode }</span>
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, LIMIT);
|
||||
completions = completions.map(c => ({
|
||||
completion: c.emoji.unicode,
|
||||
component: (
|
||||
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
||||
<span>{ c.emoji.unicode }</span>
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
})).slice(0, LIMIT);
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
limit = -1,
|
||||
): Promise<ICompletion[]> {
|
||||
// lazy-load user list into matcher
|
||||
if (!this.users) this._makeUsers();
|
||||
if (!this.users) this.makeUsers();
|
||||
|
||||
let completions = [];
|
||||
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
|
||||
|
@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
return _t('Users');
|
||||
}
|
||||
|
||||
_makeUsers() {
|
||||
private makeUsers() {
|
||||
const events = this.room.getLiveTimeline().getEvents();
|
||||
const lastSpoken = {};
|
||||
|
||||
|
|
153
src/components/structures/CallEventGrouper.ts
Normal file
153
src/components/structures/CallEventGrouper.ts
Normal 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();
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component {
|
|||
return (<div className={classes}>
|
||||
<div className="mx_CustomRoomTagPanel_divider" />
|
||||
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
|
||||
{tags}
|
||||
{ tags }
|
||||
</AutoHideScrollbar>
|
||||
</div>);
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
|
|||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
|
||||
});
|
||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
|
||||
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -120,16 +120,15 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
|
||||
const content = <div className={`${className}_body`}
|
||||
dangerouslySetInnerHTML={{ __html: this.state.page }}
|
||||
>
|
||||
</div>;
|
||||
/>;
|
||||
|
||||
if (this.props.scrollbar) {
|
||||
return <AutoHideScrollbar className={classes}>
|
||||
{content}
|
||||
{ content }
|
||||
</AutoHideScrollbar>;
|
||||
} else {
|
||||
return <div className={classes}>
|
||||
{content}
|
||||
{ content }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import { Layout } from "../../settings/Layout";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -241,8 +242,8 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
||||
<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>
|
||||
<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>
|
||||
</div>);
|
||||
|
||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
|
@ -262,11 +263,12 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = {false}
|
||||
showUrlPreview={false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape={TileShape.FileGrid}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
);
|
||||
|
|
|
@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
|
|||
render() {
|
||||
return <div className='mx_GenericErrorPage'>
|
||||
<div className='mx_GenericErrorPage_box'>
|
||||
<h1>{this.props.title}</h1>
|
||||
<p>{this.props.message}</p>
|
||||
<h1>{ this.props.title }</h1>
|
||||
<p>{ this.props.message }</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -222,7 +222,7 @@ class FeaturedRoom extends React.Component {
|
|||
|
||||
let roomNameNode = null;
|
||||
if (permalink) {
|
||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
|
||||
roomNameNode = <a href={permalink} onClick={this.onClick}>{ roomName }</a>;
|
||||
} else {
|
||||
roomNameNode = <span>{ roomName }</span>;
|
||||
}
|
||||
|
@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
|
|||
let hostingSignup = null;
|
||||
if (hostingSignupLink && this.state.isUserPrivileged) {
|
||||
hostingSignup = <div className="mx_GroupView_hostingSignup">
|
||||
{_t(
|
||||
{ _t(
|
||||
"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">
|
||||
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
|
@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
|
|||
avatarImage = <Spinner />;
|
||||
} else {
|
||||
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
||||
avatarImage = <GroupAvatar
|
||||
groupId={this.props.groupId}
|
||||
groupName={this.state.profileForm.name}
|
||||
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>
|
||||
<div className="mx_GroupView_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||
<img src={require("../../../res/img/camera.svg")}
|
||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||
width="17" height="15" />
|
||||
<img
|
||||
src={require("../../../res/img/camera.svg")}
|
||||
alt={_t("Upload avatar")}
|
||||
title={_t("Upload avatar")}
|
||||
width="17"
|
||||
height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
||||
</div>
|
||||
|
@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
|
|||
groupAvatarUrl={groupAvatarUrl}
|
||||
groupName={groupName}
|
||||
onClick={onGroupHeaderItemClick}
|
||||
width={28} height={28}
|
||||
width={28}
|
||||
height={28}
|
||||
/>;
|
||||
if (summary.profile && summary.profile.name) {
|
||||
nameNode = <div onClick={onGroupHeaderItemClick}>
|
||||
|
@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
|
|||
key="_cancelButton"
|
||||
onClick={this._onCancelClick}
|
||||
>
|
||||
<img src={require("../../../res/img/cancel.svg")} className="mx_filterFlipColor"
|
||||
width="18" height="18" alt={_t("Cancel")} />
|
||||
<img
|
||||
src={require("../../../res/img/cancel.svg")}
|
||||
className="mx_filterFlipColor"
|
||||
width="18"
|
||||
height="18"
|
||||
alt={_t("Cancel")} />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
} else {
|
||||
if (summary.user && summary.user.membership === 'join') {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_editButton"
|
||||
<AccessibleButton
|
||||
className="mx_GroupHeader_button mx_GroupHeader_editButton"
|
||||
key="_editButton"
|
||||
onClick={this._onEditClick}
|
||||
title={_t("Community Settings")}
|
||||
>
|
||||
</AccessibleButton>,
|
||||
/>,
|
||||
);
|
||||
}
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_shareButton"
|
||||
<AccessibleButton
|
||||
className="mx_GroupHeader_button mx_GroupHeader_shareButton"
|
||||
key="_shareButton"
|
||||
onClick={this._onShareClick}
|
||||
title={_t('Share Community')}
|
||||
>
|
||||
</AccessibleButton>,
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onSelectRoom={this.selectRoom}
|
||||
/>
|
||||
|
||||
{dialPadButton}
|
||||
{ dialPadButton }
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
|
@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
leftLeftPanel = (
|
||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||
<GroupFilterPanel />
|
||||
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
||||
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={containerClasses} ref={this.ref}>
|
||||
{leftLeftPanel}
|
||||
{ leftLeftPanel }
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchDialExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
{ this.renderHeader() }
|
||||
{ this.renderSearchDialExplore() }
|
||||
{ this.renderBreadcrumbs() }
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
|
@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
>
|
||||
{roomList}
|
||||
{ roomList }
|
||||
</div>
|
||||
</div>
|
||||
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
||||
|
|
|
@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
|
|||
<span>{ WidgetUtils.getWidgetName(app) }</span>
|
||||
</AccessibleButton>
|
||||
|
||||
{/* Code for the maximise button for once we have full screen widgets */}
|
||||
{/*<AccessibleTooltipButton
|
||||
{ /* Code for the maximise button for once we have full screen widgets */ }
|
||||
{ /*<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={() => {
|
||||
}}
|
||||
className="mx_LeftPanelWidget_maximizeButton"
|
||||
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
|
||||
title={_t("Maximize")}
|
||||
/>*/}
|
||||
/>*/ }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { Key } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
|
|||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
|
@ -140,18 +142,6 @@ interface IState {
|
|||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
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 _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||
|
@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
this.updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
|
@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer = this.createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
return this._roomView.current.canResetTimeline();
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
let size;
|
||||
let collapsed;
|
||||
private createResizer() {
|
||||
let panelSize;
|
||||
let panelCollapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||
toggleSize: 206 - 50,
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
onCollapsed: (collapsed) => {
|
||||
panelCollapsed = collapsed;
|
||||
if (collapsed) {
|
||||
dis.dispatch({ action: "hide_left_panel" });
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
} else {
|
||||
dis.dispatch({ action: "show_left_panel" });
|
||||
}
|
||||
},
|
||||
onResized: (_size) => {
|
||||
size = _size;
|
||||
onResized: (size) => {
|
||||
panelSize = size;
|
||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||
},
|
||||
onResizeStart: () => {
|
||||
this.props.resizeNotifier.startResizing();
|
||||
},
|
||||
onResizeStop: () => {
|
||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
isItemCollapsed: domNode => {
|
||||
|
@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
return resizer;
|
||||
}
|
||||
|
||||
_loadResizerPreferences() {
|
||||
private loadResizerPreferences() {
|
||||
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
||||
if (isNaN(lhsSize)) {
|
||||
lhsSize = 350;
|
||||
|
@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.resizer.forHandleAt(0).resize(lhsSize);
|
||||
}
|
||||
|
||||
onAccountData = (event) => {
|
||||
private onAccountData = (event: MatrixEvent) => {
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({ action: "ignore_state_changed" });
|
||||
}
|
||||
|
@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
this.updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||
this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||
}
|
||||
};
|
||||
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
||||
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";
|
||||
if (error) {
|
||||
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];
|
||||
if (!serverNoticeList) return [];
|
||||
|
||||
|
@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
);
|
||||
});
|
||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||
this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||
this.setState({
|
||||
usageLimitEventContent,
|
||||
usageLimitEventTs: pinnedEventTs,
|
||||
|
@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
private onPaste = (ev) => {
|
||||
let canReceiveInput = false;
|
||||
let element = ev.target;
|
||||
// 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.
|
||||
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
|
||||
// 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.
|
||||
// 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.
|
||||
if (ev.target === document.body) {
|
||||
this._onKeyDown(ev);
|
||||
this.onKeyDown(ev);
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
private onKeyDown = (ev) => {
|
||||
let handled = false;
|
||||
|
||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||
|
@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
case RoomAction.JumpToFirstMessage:
|
||||
case RoomAction.JumpToLatestMessage:
|
||||
// pass the event down to the scroll panel
|
||||
this._onScrollKeyPressed(ev);
|
||||
this.onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
break;
|
||||
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
|
||||
* @param {Object} ev The key event
|
||||
*/
|
||||
_onScrollKeyPressed = (ev) => {
|
||||
private onScrollKeyPressed = (ev) => {
|
||||
if (this._roomView.current) {
|
||||
this._roomView.current.handleScrollKey(ev);
|
||||
}
|
||||
|
@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
onPaste={this._onPaste}
|
||||
onKeyDown={this._onReactKeyDown}
|
||||
onPaste={this.onPaste}
|
||||
onKeyDown={this.onReactKeyDown}
|
||||
className='mx_MatrixChat_wrapper'
|
||||
aria-hidden={this.props.hideToSRUsers}
|
||||
>
|
||||
|
@ -644,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
{audioFeedArraysForCalls}
|
||||
{ audioFeedArraysForCalls }
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||
// eslint-disable-next-line camelcase
|
||||
// eslint-disable-next-line
|
||||
UNSAFE_componentWillUpdate(props, state) {
|
||||
if (this.shouldTrackPageChange(this.state, state)) {
|
||||
this.startPageChangeTimer();
|
||||
|
@ -1112,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (memberCount === 1) {
|
||||
warnings.push((
|
||||
<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. " +
|
||||
"If you leave, no one will be able to join in the future, including you.") }
|
||||
</span>
|
||||
|
@ -1127,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (rule !== "public") {
|
||||
warnings.push((
|
||||
<span className="warning" key="non_public_warning">
|
||||
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ ' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ isSpace
|
||||
? _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.") }
|
||||
|
@ -1155,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
: _t(
|
||||
"Are you sure you want to leave the room '%(roomName)s'?",
|
||||
{ roomName: roomToLeave.name },
|
||||
)}
|
||||
) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
|
@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({ action: 'timeline_resize' });
|
||||
}
|
||||
|
||||
onRoomCreated(roomId: string) {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
}
|
||||
|
||||
onRegisterClick = () => {
|
||||
this.showScreen("register");
|
||||
};
|
||||
|
@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
{...this.state}
|
||||
ref={this.loggedInView}
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
/>
|
||||
|
@ -2053,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
let errorBox;
|
||||
if (this.state.syncError && !isStoreError) {
|
||||
errorBox = <div className="mx_MatrixChat_syncError">
|
||||
{messageForSyncError(this.state.syncError)}
|
||||
{ messageForSyncError(this.state.syncError) }
|
||||
</div>;
|
||||
}
|
||||
view = (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
{errorBox}
|
||||
{ errorBox }
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{_t('Logout')}
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -2124,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return <ErrorBoundary>
|
||||
{view}
|
||||
{ view }
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
|
|||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||
import CallEventGrouper from "./CallEventGrouper";
|
||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||
import EventListSummary from '../views/elements/EventListSummary';
|
||||
|
@ -232,6 +233,11 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
private readonly showTypingNotificationsWatcherRef: string;
|
||||
private eventNodes: Record<string, HTMLElement>;
|
||||
|
||||
// A map of <callId, CallEventGrouper>
|
||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||
|
||||
private membersCount = 0;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -252,11 +258,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.calculateRoomMembersCount();
|
||||
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
this.props.room?.off("RoomState.members", this.calculateRoomMembersCount);
|
||||
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 => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
@ -576,6 +589,20 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
const last = (mxEv === lastShownEvent);
|
||||
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.shouldGroup(mxEv)) {
|
||||
grouper.add(mxEv, this.showHiddenEvents);
|
||||
|
@ -591,7 +618,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
for (const Grouper of groupers) {
|
||||
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) {
|
||||
|
@ -692,6 +727,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
// it's successful: we received it.
|
||||
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
|
||||
ret.push(
|
||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||
|
@ -722,7 +758,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
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>,
|
||||
);
|
||||
|
@ -952,6 +989,7 @@ abstract class BaseGrouper {
|
|||
public readonly event: MatrixEvent,
|
||||
public readonly prevEvent: MatrixEvent,
|
||||
public readonly lastShownEvent: MatrixEvent,
|
||||
protected readonly layout: Layout,
|
||||
public readonly nextEvent?: MatrixEvent,
|
||||
public readonly nextEventTile?: MatrixEvent,
|
||||
) {
|
||||
|
@ -1078,6 +1116,7 @@ class CreationGrouper extends BaseGrouper {
|
|||
onToggle={panel.onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={summaryText}
|
||||
layout={this.layout}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
|
@ -1105,10 +1144,11 @@ class RedactionGrouper extends BaseGrouper {
|
|||
ev: MatrixEvent,
|
||||
prevEvent: MatrixEvent,
|
||||
lastShownEvent: MatrixEvent,
|
||||
layout: Layout,
|
||||
nextEvent: MatrixEvent,
|
||||
nextEventTile: MatrixEvent,
|
||||
) {
|
||||
super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
|
||||
super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
|
||||
this.events = [ev];
|
||||
}
|
||||
|
||||
|
@ -1173,6 +1213,7 @@ class RedactionGrouper extends BaseGrouper {
|
|||
onToggle={panel.onHeightChanged} // Update scroll state
|
||||
summaryMembers={Array.from(senders)}
|
||||
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
|
||||
layout={this.layout}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
|
@ -1201,8 +1242,9 @@ class MemberGrouper extends BaseGrouper {
|
|||
public readonly event: MatrixEvent,
|
||||
public readonly prevEvent: MatrixEvent,
|
||||
public readonly lastShownEvent: MatrixEvent,
|
||||
protected readonly layout: Layout,
|
||||
) {
|
||||
super(panel, event, prevEvent, lastShownEvent);
|
||||
super(panel, event, prevEvent, lastShownEvent, layout);
|
||||
this.events = [event];
|
||||
}
|
||||
|
||||
|
@ -1277,6 +1319,7 @@ class MemberGrouper extends BaseGrouper {
|
|||
events={this.events}
|
||||
onToggle={panel.onHeightChanged} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
layout={this.layout}
|
||||
>
|
||||
{ eventTiles }
|
||||
</MemberEventListSummary>,
|
||||
|
|
|
@ -109,8 +109,7 @@ export default class MyGroups extends React.Component {
|
|||
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} />
|
||||
<div className='mx_MyGroups_header'>
|
||||
<div className="mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick} />
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Create a new community') }
|
||||
|
@ -121,7 +120,7 @@ export default class MyGroups extends React.Component {
|
|||
) }
|
||||
</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}>
|
||||
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
|
@ -137,7 +136,7 @@ export default class MyGroups extends React.Component {
|
|||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>*/ }
|
||||
</div>
|
||||
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
|
||||
<div className="mx_MyGroups_content">
|
||||
|
|
|
@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
|
|||
const toasts = this.state.toasts.map((t, i) => {
|
||||
return (
|
||||
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
|
||||
{React.createElement(t, {})}
|
||||
{ React.createElement(t, {}) }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_NonUrgentToastContainer" role="alert">
|
||||
{toasts}
|
||||
{ toasts }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -35,8 +36,8 @@ interface IProps {
|
|||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
<h2>{ _t('You’re all caught up') }</h2>
|
||||
<p>{ _t('You have no visible notifications.') }</p>
|
||||
</div>);
|
||||
|
||||
let content;
|
||||
|
@ -52,6 +53,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
tileShape={TileShape.Notif}
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
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
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this.unregisterGroupStore();
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
return [
|
||||
<div
|
||||
key={ `${room.room_id}_avatar` }
|
||||
key={`${room.room_id}_avatar`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
|
@ -603,7 +603,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>,
|
||||
<div
|
||||
key={ `${room.room_id}_description` }
|
||||
key={`${room.room_id}_description`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
|
@ -626,14 +626,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</div>,
|
||||
<div
|
||||
key={ `${room.room_id}_memberCount` }
|
||||
key={`${room.room_id}_memberCount`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div
|
||||
key={ `${room.room_id}_preview` }
|
||||
key={`${room.room_id}_preview`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
className="mx_RoomDirectory_preview"
|
||||
|
@ -641,7 +641,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
{ previewButton }
|
||||
</div>,
|
||||
<div
|
||||
key={ `${room.room_id}_join` }
|
||||
key={`${room.room_id}_join`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
|
@ -796,7 +796,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
showJoinButton={showJoinButton}
|
||||
initialText={this.props.initialText}
|
||||
/>
|
||||
{dropdown}
|
||||
{ dropdown }
|
||||
</div>;
|
||||
}
|
||||
const explanation =
|
||||
|
@ -814,16 +814,16 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
}) : _t("Explore rooms");
|
||||
return (
|
||||
<BaseDialog
|
||||
className={'mx_RoomDirectory_dialog'}
|
||||
className="mx_RoomDirectory_dialog"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
{explanation}
|
||||
{ explanation }
|
||||
<div className="mx_RoomDirectory_list">
|
||||
{listHeader}
|
||||
{content}
|
||||
{ listHeader }
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -209,9 +209,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{icon}
|
||||
{input}
|
||||
{clearButton}
|
||||
{ icon }
|
||||
{ input }
|
||||
{ clearButton }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -222,17 +222,17 @@ export default class RoomStatusBar extends React.PureComponent {
|
|||
|
||||
let buttonRow = <>
|
||||
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||
{_t("Delete all")}
|
||||
{ _t("Delete all") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||
{_t("Retry all")}
|
||||
{ _t("Retry all") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
if (this.state.isResending) {
|
||||
buttonRow = <>
|
||||
<InlineSpinner w={20} h={20} />
|
||||
{/* span for css */}
|
||||
<span>{_t("Sending")}</span>
|
||||
{ /* span for css */ }
|
||||
<span>{ _t("Sending") }</span>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -253,7 +253,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||
{buttonRow}
|
||||
{ buttonRow }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -266,14 +266,18 @@ export default class RoomStatusBar extends React.PureComponent {
|
|||
<div className="mx_RoomStatusBar">
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
|
||||
height="24" title="/!\ " alt="/!\ " />
|
||||
<img
|
||||
src={require("../../../res/img/feather-customised/warning-triangle.svg")}
|
||||
width="24"
|
||||
height="24"
|
||||
title="/!\ "
|
||||
alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{_t('Connectivity to the server has been lost.')}
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
@ -166,6 +166,10 @@ export interface IState {
|
|||
canReply: boolean;
|
||||
layout: Layout;
|
||||
lowBandwidth: boolean;
|
||||
alwaysShowTimestamps: boolean;
|
||||
showTwelveHourTimestamps: boolean;
|
||||
readMarkerInViewThresholdMs: number;
|
||||
readMarkerOutOfViewThresholdMs: number;
|
||||
showHiddenEventsInTimeline: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRedactions: boolean;
|
||||
|
@ -231,6 +235,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canReply: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
||||
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
||||
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
|
@ -263,14 +271,26 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
this.settingWatchers = [
|
||||
SettingsStore.watchSetting("layout", null, () =>
|
||||
this.setState({ layout: SettingsStore.getValue("layout") }),
|
||||
SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||
this.setState({ layout: value as Layout }),
|
||||
),
|
||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||
SettingsStore.watchSetting("lowBandwidth", null, (...[,,, value]) =>
|
||||
this.setState({ lowBandwidth: value as boolean }),
|
||||
),
|
||||
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
|
||||
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
|
||||
SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[,,, value]) =>
|
||||
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
|
||||
this.settingWatchers = this.settingWatchers.concat([
|
||||
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
}),
|
||||
SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>
|
||||
this.setState({ showReadReceipts: value as boolean }),
|
||||
),
|
||||
SettingsStore.watchSetting("showRedactions", null, () =>
|
||||
this.setState({
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
}),
|
||||
SettingsStore.watchSetting("showRedactions", roomId, (...[,,, value]) =>
|
||||
this.setState({ showRedactions: value as boolean }),
|
||||
),
|
||||
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
||||
this.setState({
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
}),
|
||||
SettingsStore.watchSetting("showJoinLeaves", roomId, (...[,,, value]) =>
|
||||
this.setState({ showJoinLeaves: value as boolean }),
|
||||
),
|
||||
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
||||
this.setState({
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
}),
|
||||
SettingsStore.watchSetting("showAvatarChanges", roomId, (...[,,, value]) =>
|
||||
this.setState({ showAvatarChanges: value as boolean }),
|
||||
),
|
||||
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
||||
this.setState({
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
}),
|
||||
SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[,,, value]) =>
|
||||
this.setState({ showDisplaynameChanges: value as boolean }),
|
||||
),
|
||||
]);
|
||||
|
||||
|
@ -1730,7 +1740,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
|
@ -1892,10 +1903,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
className="mx_RoomView_auxPanel_hiddenHighlights"
|
||||
onClick={this.onHiddenHighlightsClick}
|
||||
>
|
||||
{_t(
|
||||
{ _t(
|
||||
"You have %(count)s unread notifications in a prior version of this room.",
|
||||
{ count: hiddenHighlightCount },
|
||||
)}
|
||||
) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -2007,7 +2018,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
onScroll={this.onMessageListScroll}
|
||||
onUserScroll={this.onUserScroll}
|
||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
showUrlPreview={this.state.showUrlPreview}
|
||||
className={messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||
|
@ -2057,7 +2068,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current &&
|
||||
{ showChatEffects && this.roomView.current &&
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
}
|
||||
<ErrorBoundary>
|
||||
|
@ -2076,22 +2087,22 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className="mx_RoomView_body">
|
||||
{auxPanel}
|
||||
{ auxPanel }
|
||||
<div className={timelineClasses}>
|
||||
{fileDropTarget}
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
{ fileDropTarget }
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line" />
|
||||
{statusBar}
|
||||
{ statusBar }
|
||||
</div>
|
||||
</div>
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</div>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
private readonly itemlist = createRef<HTMLOListElement>();
|
||||
private unmounted = false;
|
||||
private scrollTimeout: Timer;
|
||||
// Are we currently trying to backfill?
|
||||
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;
|
||||
// Is that next fill request scheduled because of a props update?
|
||||
private pendingFillDueToPropsUpdate: boolean;
|
||||
private scrollState: IScrollState;
|
||||
private preventShrinkingState: IPreventShrinkingState;
|
||||
private unfillDebouncer: number;
|
||||
|
@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
// adding events to the top).
|
||||
//
|
||||
// This will also re-check the fill state, in case the paginate was inadequate
|
||||
this.checkScroll();
|
||||
this.checkScroll(true);
|
||||
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
|
||||
// where it ought to be, and set off pagination requests if necessary.
|
||||
public checkScroll = () => {
|
||||
public checkScroll = (isFromPropsUpdate = false) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.restoreSavedScrollState();
|
||||
this.checkFillState();
|
||||
this.checkFillState(0, isFromPropsUpdate);
|
||||
};
|
||||
|
||||
// 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.
|
||||
public checkFillState = async (depth = 0): Promise<void> => {
|
||||
public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise<void> => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
// don't allow more than 1 chain of calls concurrently
|
||||
// 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.
|
||||
// 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 (this.isFilling) {
|
||||
if (this.isFilling && !this.isFillingDueToPropsUpdate) {
|
||||
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this.fillRequestWhileRunning = true;
|
||||
this.pendingFillDueToPropsUpdate = isFromPropsUpdate;
|
||||
return;
|
||||
}
|
||||
debuglog("isFilling: setting");
|
||||
this.isFilling = true;
|
||||
this.isFillingDueToPropsUpdate = isFromPropsUpdate;
|
||||
}
|
||||
|
||||
const itemlist = this.itemlist.current;
|
||||
|
@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
if (isFirstCall) {
|
||||
debuglog("isFilling: clearing");
|
||||
this.isFilling = false;
|
||||
this.isFillingDueToPropsUpdate = false;
|
||||
}
|
||||
|
||||
if (this.fillRequestWhileRunning) {
|
||||
const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
|
||||
this.fillRequestWhileRunning = false;
|
||||
this.checkFillState();
|
||||
this.pendingFillDueToPropsUpdate = false;
|
||||
this.checkFillState(0, refillDueToPropsUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -136,8 +136,8 @@ export default class SearchBox extends React.Component {
|
|||
key="button"
|
||||
tabIndex={-1}
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button"); } }>
|
||||
</AccessibleButton>) : undefined;
|
||||
onClick={() => {this._clearSearch("button"); }}
|
||||
/>) : undefined;
|
||||
|
||||
// show a shorter placeholder when blurred, if requested
|
||||
// this is used for the room filter field that has
|
||||
|
@ -153,12 +153,12 @@ export default class SearchBox extends React.Component {
|
|||
type="text"
|
||||
ref={this._search}
|
||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
value={this.state.searchTerm}
|
||||
onFocus={this._onFocus}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
onBlur={this._onBlur}
|
||||
placeholder={ placeholder }
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
autoFocus={this.props.autoFocus}
|
||||
/>
|
||||
|
|
|
@ -404,7 +404,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
const [saving, setSaving] = useState(false);
|
||||
|
||||
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;
|
||||
|
@ -569,7 +569,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
return <>
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Search names and descriptions") }
|
||||
placeholder={_t("Search names and descriptions")}
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
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>.",
|
||||
null,
|
||||
{ a: sub => {
|
||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{ sub }</AccessibleButton>;
|
||||
} },
|
||||
) }
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||
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 { EventSubscription } from "fbemitter";
|
||||
|
||||
|
@ -66,7 +66,6 @@ import Modal from "../../Modal";
|
|||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -101,12 +100,14 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
|||
<hr />
|
||||
<div>
|
||||
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
|
||||
<AccessibleButton kind="link" onClick={() => {
|
||||
if (onClick) onClick();
|
||||
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
if (onClick) onClick();
|
||||
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
|
||||
featureId: "feature_spaces",
|
||||
});
|
||||
}}>
|
||||
});
|
||||
}}>
|
||||
{ _t("Feedback") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -146,7 +147,7 @@ const SpaceInfo = ({ space }) => {
|
|||
return <div className="mx_SpaceRoomView_info">
|
||||
{ visibilitySection }
|
||||
{ joinRule === "public" && <RoomMemberCount room={space}>
|
||||
{(count) => count > 0 ? (
|
||||
{ (count) => count > 0 ? (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
|
@ -159,7 +160,7 @@ const SpaceInfo = ({ space }) => {
|
|||
>
|
||||
{ _t("%(count)s members", { count }) }
|
||||
</AccessibleButton>
|
||||
) : null}
|
||||
) : null }
|
||||
</RoomMemberCount> }
|
||||
</div>;
|
||||
};
|
||||
|
@ -292,7 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</h1>
|
||||
<SpaceInfo space={space} />
|
||||
<RoomTopic room={space}>
|
||||
{(topic, ref) =>
|
||||
{ (topic, ref) =>
|
||||
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
|
||||
{ topic }
|
||||
</div>
|
||||
|
@ -307,7 +308,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
};
|
||||
|
||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
|
@ -330,7 +330,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
|||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
if (await showCreateNewRoom(cli, space)) {
|
||||
if (await showCreateNewRoom(space)) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
}}
|
||||
|
@ -343,7 +343,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
|||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
const [added] = await showAddExistingRooms(space);
|
||||
if (added) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
|
@ -397,11 +397,11 @@ const SpaceLanding = ({ space }) => {
|
|||
}
|
||||
|
||||
let settingsButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) {
|
||||
if (shouldShowSpaceSettings(space)) {
|
||||
settingsButton = <AccessibleTooltipButton
|
||||
className="mx_SpaceRoomView_landing_settingsButton"
|
||||
onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
showSpaceSettings(space);
|
||||
}}
|
||||
title={_t("Settings")}
|
||||
/>;
|
||||
|
@ -419,12 +419,12 @@ const SpaceLanding = ({ space }) => {
|
|||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<div className="mx_SpaceRoomView_landing_name">
|
||||
<RoomName room={space}>
|
||||
{(name) => {
|
||||
{ (name) => {
|
||||
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
|
||||
<h1>{ name }</h1>
|
||||
</div> };
|
||||
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
|
||||
}}
|
||||
} }
|
||||
</RoomName>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_info">
|
||||
|
@ -434,11 +434,11 @@ const SpaceLanding = ({ space }) => {
|
|||
{ settingsButton }
|
||||
</div>
|
||||
<RoomTopic room={space}>
|
||||
{(topic, ref) => (
|
||||
{ (topic, ref) => (
|
||||
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
|
||||
{ topic }
|
||||
</div>
|
||||
)}
|
||||
) }
|
||||
</RoomTopic>
|
||||
<SpaceFeedbackPrompt />
|
||||
<hr />
|
||||
|
@ -458,7 +458,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
const numFields = 3;
|
||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||
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;
|
||||
return <Field
|
||||
key={name}
|
||||
|
@ -553,9 +553,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
|||
onFinished={onFinished}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_buttons" />
|
||||
<SpaceFeedbackPrompt />
|
||||
</div>;
|
||||
};
|
||||
|
@ -625,7 +623,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
const numFields = 3;
|
||||
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
||||
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;
|
||||
return <Field
|
||||
key={name}
|
||||
|
|
|
@ -74,7 +74,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
|||
tabLocation: TabLocation.LEFT,
|
||||
};
|
||||
|
||||
private _getActiveTabIndex() {
|
||||
private getActiveTabIndex() {
|
||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||
return this.state.activeTabIndex;
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
|||
* @param {Tab} tab the tab to show
|
||||
* @private
|
||||
*/
|
||||
private _setActiveTab(tab: Tab) {
|
||||
private setActiveTab(tab: Tab) {
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx !== -1) {
|
||||
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 ";
|
||||
|
||||
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;
|
||||
if (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);
|
||||
return (
|
||||
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
|
||||
{tabIcon}
|
||||
{ tabIcon }
|
||||
<span className="mx_TabbedView_tabLabel_text">
|
||||
{ label }
|
||||
</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 (
|
||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
||||
{tab.body}
|
||||
{ tab.body }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||
const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
|
||||
const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
|
||||
|
||||
const tabbedViewClasses = classNames({
|
||||
'mx_TabbedView': true,
|
||||
|
@ -141,9 +141,9 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<div className={tabbedViewClasses}>
|
||||
<div className="mx_TabbedView_tabLabels">
|
||||
{labels}
|
||||
{ labels }
|
||||
</div>
|
||||
{panel}
|
||||
{ panel }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -277,7 +277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move into constructor
|
||||
// eslint-disable-next-line camelcase
|
||||
// eslint-disable-next-line
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.props.manageReadReceipts) {
|
||||
this.updateReadReceiptOnUserActivity();
|
||||
|
@ -290,7 +290,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
// eslint-disable-next-line
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.timelineSet !== this.props.timelineSet) {
|
||||
// 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 {
|
||||
return readMarkerPosition === 0 ?
|
||||
this.state.readMarkerInViewThresholdMs :
|
||||
this.state.readMarkerOutOfViewThresholdMs;
|
||||
this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs :
|
||||
this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs;
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -1493,8 +1493,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
onUserScroll={this.props.onUserScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={
|
||||
this.props.alwaysShowTimestamps ??
|
||||
this.context?.alwaysShowTimestamps ??
|
||||
this.state.alwaysShowTimestamps
|
||||
}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue