mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17686
Conflicts: src/components/views/dialogs/CreateRoomDialog.tsx src/components/views/dialogs/RoomUpgradeDialog.tsx src/components/views/dialogs/RoomUpgradeWarningDialog.tsx src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
This commit is contained in:
commit
390b05617c
324 changed files with 4440 additions and 3040 deletions
|
@ -46,6 +46,7 @@
|
||||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||||
"lint:js": "eslint --max-warnings 0 src test",
|
"lint:js": "eslint --max-warnings 0 src test",
|
||||||
|
"lint:js-fix": "eslint --fix src test",
|
||||||
"lint:types": "tsc --noEmit --jsx react",
|
"lint:types": "tsc --noEmit --jsx react",
|
||||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
@ -64,8 +65,8 @@
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
"diff-dom": "^4.2.2",
|
"diff-dom": "^4.2.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"emojibase-data": "^5.1.1",
|
"emojibase-data": "^6.2.0",
|
||||||
"emojibase-regex": "^4.1.1",
|
"emojibase-regex": "^5.1.3",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "6.1.0",
|
"filesize": "6.1.0",
|
||||||
|
|
|
@ -163,6 +163,7 @@
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
@import "./views/messages/_EventTileBubble.scss";
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
|
@import "./views/messages/_CallEvent.scss";
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
@ -202,6 +203,7 @@
|
||||||
@import "./views/rooms/_EditMessageComposer.scss";
|
@import "./views/rooms/_EditMessageComposer.scss";
|
||||||
@import "./views/rooms/_EntityTile.scss";
|
@import "./views/rooms/_EntityTile.scss";
|
||||||
@import "./views/rooms/_EventTile.scss";
|
@import "./views/rooms/_EventTile.scss";
|
||||||
|
@import "./views/rooms/_EventBubbleTile.scss";
|
||||||
@import "./views/rooms/_GroupLayout.scss";
|
@import "./views/rooms/_GroupLayout.scss";
|
||||||
@import "./views/rooms/_IRCLayout.scss";
|
@import "./views/rooms/_IRCLayout.scss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||||
|
|
|
@ -118,10 +118,6 @@ limitations under the License.
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_FilePanel_empty::before {
|
.mx_FilePanel_empty::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ limitations under the License.
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
|
|
|
@ -30,5 +30,12 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
content: '';
|
content: '';
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InfoTooltip_icon_info::before {
|
||||||
mask-image: url('$(res)/img/element-icons/info.svg');
|
mask-image: url('$(res)/img/element-icons/info.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InfoTooltip_icon_warning::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/warning.svg');
|
||||||
|
}
|
||||||
|
|
154
res/css/views/messages/_CallEvent.scss
Normal file
154
res/css/views/messages/_CallEvent.scss
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
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_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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,17 +18,16 @@ $timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
position: absolute;
|
object-fit: contain;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
> canvas {
|
> canvas {
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
|
@ -43,17 +42,6 @@ $timelineImageBorderRadius: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_spinner {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inner img should be centered around 0, 0
|
|
||||||
.mx_MImageBody_thumbnail_spinner > * {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MImageBody_gifLabel {
|
.mx_MImageBody_gifLabel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -107,3 +107,12 @@ limitations under the License.
|
||||||
.mx_MessageActionBar_cancelButton::after {
|
.mx_MessageActionBar_cancelButton::after {
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_downloadButton::after {
|
||||||
|
mask-size: 14px;
|
||||||
|
mask-image: url('$(res)/img/download.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
|
||||||
|
background-color: transparent; // hide the download icon mask
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ limitations under the License.
|
||||||
height: 24px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
323
res/css/views/rooms/_EventBubbleTile.scss
Normal file
323
res/css/views/rooms/_EventBubbleTile.scss
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble],
|
||||||
|
.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
|
||||||
|
--avatarSize: 32px;
|
||||||
|
--gutterSize: 11px;
|
||||||
|
--cornerRadius: 12px;
|
||||||
|
--maxWidth: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
margin-top: var(--gutterSize);
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-right: 100px;
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For replies */
|
||||||
|
.mx_EventTile {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
left: -60px;
|
||||||
|
right: -60px;
|
||||||
|
z-index: -1;
|
||||||
|
background: $eventbubble-bg-hover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
img {
|
||||||
|
box-shadow: 0 0 0 3px $eventbubble-bg-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile,
|
||||||
|
.mx_EventTile_line {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-self=false] {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
border-bottom-right-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
left: -34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
right: 0;
|
||||||
|
transform: translate3d(50%, 50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
--backgroundColor: $eventbubble-others-bg;
|
||||||
|
}
|
||||||
|
&[data-self=true] {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
border-bottom-left-radius: var(--cornerRadius);
|
||||||
|
float: right;
|
||||||
|
> a {
|
||||||
|
left: auto;
|
||||||
|
right: -48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_SenderProfile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
float: right;
|
||||||
|
clear: right;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
/* Moving the "add reaction button" before the reactions */
|
||||||
|
> :last-child {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
top: -19px; // height of the sender block
|
||||||
|
right: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
--backgroundColor: $eventbubble-self-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--gutterSize);
|
||||||
|
border-top-left-radius: var(--cornerRadius);
|
||||||
|
border-top-right-radius: var(--cornerRadius);
|
||||||
|
background: var(--backgroundColor);
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 0 -12px 0 -9px;
|
||||||
|
> a {
|
||||||
|
position: absolute;
|
||||||
|
left: -48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
|
||||||
|
border-bottom-left-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
|
||||||
|
border-bottom-right-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
line-height: 1;
|
||||||
|
img {
|
||||||
|
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-has-reply=true] {
|
||||||
|
> .mx_EventTile_line {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread_show {
|
||||||
|
order: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread {
|
||||||
|
margin: 0 calc(-1 * var(--gutterSize));
|
||||||
|
|
||||||
|
.mx_EventTile_reply {
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 0;
|
||||||
|
> a {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--gutterSize);
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.mx_SenderProfile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EditMessageComposer_buttons {
|
||||||
|
position: static;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
margin-right: -18px;
|
||||||
|
margin-left: -9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread {
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-left-color: $eventbubble-reply-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_bubbleContainer,
|
||||||
|
&.mx_EventTile_info,
|
||||||
|
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||||
|
--backgroundColor: transparent;
|
||||||
|
--gutterSize: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: static;
|
||||||
|
order: -1;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .mx_EventListSummary {
|
||||||
|
--maxWidth: 80%;
|
||||||
|
margin-left: calc(var(--avatarSize) + var(--gutterSize));
|
||||||
|
margin-right: calc(var(--gutterSize) + var(--avatarSize));
|
||||||
|
.mx_EventListSummary_toggle {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
order: 9;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.mx_EventListSummary_avatars {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
margin: 0 5px;
|
||||||
|
> a {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(calc(100% + 5px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
transform: translate3d(50%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||||
|
padding: 0 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* events that do not require bubble layout */
|
||||||
|
& ~ .mx_EventListSummary,
|
||||||
|
&.mx_EventTile_bad {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_EventListSummary {
|
||||||
|
.mx_EventTile {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventListSummary_toggle {
|
||||||
|
margin-right: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
|
||||||
|
&.mx_EventTile_bad > .mx_EventTile_line {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"reply reply" auto
|
||||||
|
"shield body" auto
|
||||||
|
"shield link" auto
|
||||||
|
/ auto 1fr;
|
||||||
|
.mx_EventTile_e2eIcon {
|
||||||
|
grid-area: shield;
|
||||||
|
}
|
||||||
|
.mx_UnknownBody {
|
||||||
|
grid-area: body;
|
||||||
|
}
|
||||||
|
.mx_EventTile_keyRequestInfo {
|
||||||
|
grid-area: link;
|
||||||
|
}
|
||||||
|
.mx_ReplyThread_wrapper {
|
||||||
|
grid-area: reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mx_EventTile_readAvatars {
|
||||||
|
position: absolute;
|
||||||
|
right: -110px;
|
||||||
|
bottom: 0;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MTextBody {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,102 +18,309 @@ limitations under the License.
|
||||||
$left-gutter: 64px;
|
$left-gutter: 64px;
|
||||||
$hover-select-border: 4px;
|
$hover-select-border: 4px;
|
||||||
|
|
||||||
.mx_EventTile {
|
.mx_EventTile:not([data-layout=bubble]) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
clear: both;
|
clear: both;
|
||||||
padding-top: 18px;
|
padding-top: 18px;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info {
|
&.mx_EventTile_info {
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
top: 14px;
|
top: 14px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
|
&.mx_EventTile_info .mx_EventTile_avatar {
|
||||||
top: $font-6px;
|
top: $font-6px;
|
||||||
left: $left-gutter;
|
left: $left-gutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
padding-top: 0px !important;
|
padding-top: 0px !important;
|
||||||
|
|
||||||
|
&.mx_EventTile_isEditing {
|
||||||
|
padding-top: 5px !important;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_EventTile_isEditing {
|
&.mx_EventTile_isEditing {
|
||||||
padding-top: 5px !important;
|
background-color: $header-panel-bg-color;
|
||||||
margin-top: -5px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_isEditing {
|
.mx_SenderProfile {
|
||||||
background-color: $header-panel-bg-color;
|
color: $primary-fg-color;
|
||||||
}
|
font-size: $font-14px;
|
||||||
|
display: inline-block; /* anti-zalgo, with overflow hidden */
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
/* the next three lines, along with overflow hidden, truncate long display names */
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: calc(100% - $left-gutter);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_SenderProfile {
|
.mx_SenderProfile .mx_Flair {
|
||||||
color: $primary-fg-color;
|
opacity: 0.7;
|
||||||
font-size: $font-14px;
|
margin-left: 5px;
|
||||||
display: inline-block; /* anti-zalgo, with overflow hidden */
|
display: inline-block;
|
||||||
overflow: hidden;
|
vertical-align: top;
|
||||||
cursor: pointer;
|
overflow: hidden;
|
||||||
padding-bottom: 0px;
|
user-select: none;
|
||||||
padding-top: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
/* the next three lines, along with overflow hidden, truncate long display names */
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: calc(100% - $left-gutter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile .mx_SenderProfile .mx_Flair {
|
img {
|
||||||
opacity: 0.7;
|
vertical-align: -2px;
|
||||||
margin-left: 5px;
|
margin-right: 2px;
|
||||||
display: inline-block;
|
border-radius: 8px;
|
||||||
vertical-align: top;
|
}
|
||||||
overflow: hidden;
|
}
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
img {
|
&.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
vertical-align: -2px;
|
visibility: hidden;
|
||||||
margin-right: 2px;
|
}
|
||||||
|
|
||||||
|
.mx_MessageTimestamp {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
left: 0px;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation .mx_EventTile_line {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
|
position: relative;
|
||||||
|
padding-left: $left-gutter;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
.mx_EventTile_reply {
|
||||||
visibility: hidden;
|
margin-right: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
left: 0px;
|
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_continuation .mx_EventTile_line {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
|
||||||
position: relative;
|
|
||||||
padding-left: $left-gutter;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled,
|
|
||||||
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
|
||||||
.mx_EventListSummary {
|
|
||||||
.mx_EventTile_line {
|
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
|
||||||
margin-right: 110px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||||
|
left: calc(-$hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* this is used for the tile for the event which is selected via the URL.
|
||||||
|
* TODO: ultimately we probably want some transition on here.
|
||||||
|
*/
|
||||||
|
&.mx_EventTile_selected > .mx_EventTile_line {
|
||||||
|
border-left: $accent-color 4px solid;
|
||||||
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
|
background-color: $event-selected-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_highlight,
|
||||||
|
&.mx_EventTile_highlight .markdown-body {
|
||||||
|
color: $event-highlight-fg-color;
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
background-color: $event-highlight-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_info .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);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile:hover .mx_EventTile_line,
|
||||||
|
&.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
|
||||||
|
&.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
|
||||||
|
background-color: $event-selected-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_searchHighlight {
|
||||||
|
background-color: $accent-color;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_searchHighlight a {
|
||||||
|
background-color: $accent-color;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_receiptSent,
|
||||||
|
.mx_EventTile_receiptSending {
|
||||||
|
// We don't use `position: relative` on the element because then it won't line
|
||||||
|
// up with the other read receipts
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_receiptSent::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
|
||||||
|
}
|
||||||
|
.mx_EventTile_receiptSending::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_contextual {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
|
||||||
|
/* Hack to stop the height of this pushing the messages apart.
|
||||||
|
Replaces margin-top: -6px. This interacts better with a read
|
||||||
|
marker being in between. Content overflows. */
|
||||||
|
height: 1px;
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption a {
|
||||||
|
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 {
|
||||||
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line {
|
||||||
|
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line {
|
||||||
|
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
|
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* End to end encryption stuff */
|
||||||
|
&:hover .mx_EventTile_e2eIcon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
|
left: calc(-$hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
||||||
|
display: block;
|
||||||
|
left: 41px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MImageBody {
|
||||||
|
margin-right: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_e2eIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 44px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomView_timeline_rr_enabled {
|
||||||
|
|
||||||
|
.mx_EventTile:not([data-layout=bubble]) {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
|
margin-right: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_bubbleContainer {
|
.mx_EventTile_bubbleContainer {
|
||||||
|
@ -130,123 +337,15 @@ $hover-select-border: 4px;
|
||||||
.mx_EventTile_msgOption {
|
.mx_EventTile_msgOption {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_reply {
|
&:hover {
|
||||||
margin-right: 10px;
|
.mx_EventTile_line {
|
||||||
}
|
// To avoid bubble events being highlighted
|
||||||
|
background-color: inherit !important;
|
||||||
/* HACK to override line-height which is already marked important elsewhere */
|
}
|
||||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
|
||||||
font-size: 48px !important;
|
|
||||||
line-height: 57px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
|
||||||
left: calc(-$hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
|
||||||
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
|
||||||
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* this is used for the tile for the event which is selected via the URL.
|
|
||||||
* TODO: ultimately we probably want some transition on here.
|
|
||||||
*/
|
|
||||||
.mx_EventTile_selected > .mx_EventTile_line {
|
|
||||||
border-left: $accent-color 4px solid;
|
|
||||||
padding-left: calc($left-gutter - $hover-select-border);
|
|
||||||
background-color: $event-selected-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_highlight,
|
|
||||||
.mx_EventTile_highlight .markdown-body {
|
|
||||||
color: $event-highlight-fg-color;
|
|
||||||
|
|
||||||
.mx_EventTile_line {
|
|
||||||
background-color: $event-highlight-bg-color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
|
|
||||||
.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
|
|
||||||
background-color: $event-selected-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_searchHighlight {
|
|
||||||
background-color: $accent-color;
|
|
||||||
color: $accent-fg-color;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding-left: 2px;
|
|
||||||
padding-right: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_searchHighlight a {
|
|
||||||
background-color: $accent-color;
|
|
||||||
color: $accent-fg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_receiptSent,
|
|
||||||
.mx_EventTile_receiptSending {
|
|
||||||
// We don't use `position: relative` on the element because then it won't line
|
|
||||||
// up with the other read receipts
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $tertiary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 14px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mx_EventTile_receiptSent::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
|
|
||||||
}
|
|
||||||
.mx_EventTile_receiptSending::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_contextual {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
position: relative;
|
|
||||||
width: 90px;
|
|
||||||
|
|
||||||
/* Hack to stop the height of this pushing the messages apart.
|
|
||||||
Replaces margin-top: -6px. This interacts better with a read
|
|
||||||
marker being in between. Content overflows. */
|
|
||||||
height: 1px;
|
|
||||||
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_readAvatars {
|
.mx_EventTile_readAvatars {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -277,52 +376,27 @@ $hover-select-border: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* all the overflow-y: hidden; are to trap Zalgos -
|
/* HACK to override line-height which is already marked important elsewhere */
|
||||||
but they introduce an implicit overflow-x: auto.
|
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||||
so make that explicitly hidden too to avoid random
|
font-size: 48px !important;
|
||||||
horizontal scrollbars occasionally appearing, like in
|
line-height: 57px !important;
|
||||||
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_content .mx_EventTile_edited {
|
||||||
.mx_EventTile_body {
|
user-select: none;
|
||||||
overflow-y: hidden;
|
font-size: $font-12px;
|
||||||
}
|
color: $roomtopic-color;
|
||||||
|
display: inline-block;
|
||||||
/* Spoiler stuff */
|
margin-left: 9px;
|
||||||
.mx_EventTile_spoiler {
|
|
||||||
cursor: pointer;
|
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_EventTile_e2eIcon {
|
.mx_EventTile_e2eIcon {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 6px;
|
|
||||||
left: 44px;
|
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: block;
|
display: block;
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
@ -381,87 +455,6 @@ $hover-select-border: 4px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo {
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_text {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_text a {
|
|
||||||
color: $primary-fg-color;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p {
|
|
||||||
text-align: auto;
|
|
||||||
margin-left: 3px;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter - $hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
|
||||||
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
|
|
||||||
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
|
||||||
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* End to end encryption stuff */
|
|
||||||
.mx_EventTile:hover .mx_EventTile_e2eIcon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
|
||||||
left: calc(-$hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
|
||||||
display: block;
|
|
||||||
left: 41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content .mx_EventTile_edited {
|
|
||||||
user-select: none;
|
|
||||||
font-size: $font-12px;
|
|
||||||
color: $roomtopic-color;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 9px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Various markdown overrides */
|
/* Various markdown overrides */
|
||||||
|
|
||||||
.mx_EventTile_body pre {
|
.mx_EventTile_body pre {
|
||||||
|
@ -595,6 +588,35 @@ $hover-select-border: 4px;
|
||||||
|
|
||||||
/* end of overrides */
|
/* end of overrides */
|
||||||
|
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo {
|
||||||
|
font-size: $font-12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_text {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_text a {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p {
|
||||||
|
text-align: auto;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_tileError {
|
.mx_EventTile_tileError {
|
||||||
color: red;
|
color: red;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -615,6 +637,13 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
|
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
||||||
|
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
||||||
|
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
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 |
|
@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #143A34;
|
||||||
|
$eventbubble-others-bg: #394049;
|
||||||
|
$eventbubble-bg-hover: #433C23;
|
||||||
|
$eventbubble-avatar-outline: $bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #F8FDFC;
|
||||||
|
$eventbubble-others-bg: #F7F8F9;
|
||||||
|
$eventbubble-bg-hover: rgb(242, 242, 242);
|
||||||
|
$eventbubble-avatar-outline: #fff;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #F8FDFC;
|
||||||
|
$eventbubble-others-bg: #F7F8F9;
|
||||||
|
$eventbubble-bg-hover: #FEFCF5;
|
||||||
|
$eventbubble-avatar-outline: $primary-bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { JSXElementConstructor } from "react";
|
import React, { JSXElementConstructor } from "react";
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
|
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
|
||||||
|
@ -22,3 +22,4 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
|
||||||
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
||||||
|
export type ReactAnyComponent = React.Component | React.ExoticComponent;
|
||||||
|
|
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 { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
matrixChat: ReturnType<Renderer>;
|
matrixChat: ReturnType<Renderer>;
|
||||||
|
@ -90,6 +92,7 @@ declare global {
|
||||||
mxUIStore: UIStore;
|
mxUIStore: UIStore;
|
||||||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||||
|
mxOnRecaptchaLoaded?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
@ -114,7 +117,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StorageEstimate {
|
interface StorageEstimate {
|
||||||
usageDetails?: {[key: string]: number};
|
usageDetails?: { [key: string]: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
|
@ -185,4 +188,21 @@ declare global {
|
||||||
parameterDescriptors?: AudioParamDescriptor[];
|
parameterDescriptors?: AudioParamDescriptor[];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var grecaptcha:
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
reset: (id: string) => void;
|
||||||
|
render: (
|
||||||
|
divId: string,
|
||||||
|
options: {
|
||||||
|
sitekey: string;
|
||||||
|
callback: (response: string) => void;
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
isReady: () => boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
|
@ -270,7 +270,7 @@ export class Analytics {
|
||||||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _track(data: IData) {
|
private async track(data: IData) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -304,7 +304,7 @@ export class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ping() {
|
public ping() {
|
||||||
this._track({
|
this.track({
|
||||||
ping: "1",
|
ping: "1",
|
||||||
});
|
});
|
||||||
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
||||||
|
@ -324,14 +324,14 @@ export class Analytics {
|
||||||
// But continue anyway because we still want to track the change
|
// But continue anyway because we still want to track the change
|
||||||
}
|
}
|
||||||
|
|
||||||
this._track({
|
this.track({
|
||||||
gt_ms: String(generationTimeMs),
|
gt_ms: String(generationTimeMs),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._track({
|
this.track({
|
||||||
e_c: category,
|
e_c: category,
|
||||||
e_a: action,
|
e_a: action,
|
||||||
e_n: name,
|
e_n: name,
|
||||||
|
@ -395,17 +395,17 @@ export class Analytics {
|
||||||
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||||
title: _t('Analytics'),
|
title: _t('Analytics'),
|
||||||
description: <div className="mx_AnalyticsModal">
|
description: <div className="mx_AnalyticsModal">
|
||||||
<div>{_t('The information being sent to us to help make %(brand)s better includes:', {
|
<div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
|
||||||
brand: SdkConfig.get().brand,
|
brand: SdkConfig.get().brand,
|
||||||
})}</div>
|
}) }</div>
|
||||||
<table>
|
<table>
|
||||||
{ rows.map((row) => <tr key={row[0]}>
|
{ rows.map((row) => <tr key={row[0]}>
|
||||||
<td>{_t(
|
<td>{ _t(
|
||||||
customVariables[row[0]].expl,
|
customVariables[row[0]].expl,
|
||||||
customVariables[row[0]].getTextVariables ?
|
customVariables[row[0]].getTextVariables ?
|
||||||
customVariables[row[0]].getTextVariables() :
|
customVariables[row[0]].getTextVariables() :
|
||||||
null,
|
null,
|
||||||
)}</td>
|
) }</td>
|
||||||
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
|
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
|
||||||
</tr>) }
|
</tr>) }
|
||||||
{ otherVariables.map((item, index) =>
|
{ otherVariables.map((item, index) =>
|
||||||
|
|
|
@ -129,7 +129,7 @@ export function getInitialLetter(name: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
|
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
|
||||||
return split(name, "", 1)[0];
|
return split(name, "", 1)[0].toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
|
|
|
@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||||
// (and store the ID of their native room)
|
// (and store the ID of their native room)
|
||||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||||
|
|
||||||
export enum AudioID {
|
enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
Ringback = 'ringbackAudio',
|
Ringback = 'ringbackAudio',
|
||||||
CallEnd = 'callendAudio',
|
CallEnd = 'callendAudio',
|
||||||
|
@ -142,6 +142,7 @@ export enum PlaceCallType {
|
||||||
export enum CallHandlerEvent {
|
export enum CallHandlerEvent {
|
||||||
CallsChanged = "calls_changed",
|
CallsChanged = "calls_changed",
|
||||||
CallChangeRoom = "call_change_room",
|
CallChangeRoom = "call_change_room",
|
||||||
|
SilencedCallsChanged = "silenced_calls_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallHandler extends EventEmitter {
|
export default class CallHandler extends EventEmitter {
|
||||||
|
@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter {
|
||||||
// do the async lookup when we get new information and then store these mappings here
|
// do the async lookup when we get new information and then store these mappings here
|
||||||
private assertedIdentityNativeUsers = new Map<string, string>();
|
private assertedIdentityNativeUsers = new Map<string, string>();
|
||||||
|
|
||||||
|
private silencedCalls = new Set<string>(); // callIds
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxCallHandler) {
|
||||||
window.mxCallHandler = new CallHandler();
|
window.mxCallHandler = new CallHandler();
|
||||||
|
@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public silenceCall(callId: string) {
|
||||||
|
this.silencedCalls.add(callId);
|
||||||
|
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||||
|
|
||||||
|
// Don't pause audio if we have calls which are still ringing
|
||||||
|
if (this.areAnyCallsUnsilenced()) return;
|
||||||
|
this.pause(AudioID.Ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unSilenceCall(callId: string) {
|
||||||
|
this.silencedCalls.delete(callId);
|
||||||
|
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||||
|
this.play(AudioID.Ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isCallSilenced(callId: string): boolean {
|
||||||
|
return this.silencedCalls.has(callId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is at least one unsilenced call
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
private areAnyCallsUnsilenced(): boolean {
|
||||||
|
return this.calls.size > this.silencedCalls.size;
|
||||||
|
}
|
||||||
|
|
||||||
private async checkProtocols(maxTries) {
|
private async checkProtocols(maxTries) {
|
||||||
try {
|
try {
|
||||||
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
||||||
|
@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter {
|
||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getCallById(callId: string): MatrixCall {
|
||||||
|
for (const call of this.calls.values()) {
|
||||||
|
if (call.callId === callId) return call;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getCallForRoom(roomId: string): MatrixCall {
|
getCallForRoom(roomId: string): MatrixCall {
|
||||||
return this.calls.get(roomId) || null;
|
return this.calls.get(roomId) || null;
|
||||||
}
|
}
|
||||||
|
@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newState !== CallState.Ringing) {
|
||||||
|
this.silencedCalls.delete(call.callId);
|
||||||
|
}
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing:
|
case CallState.Ringing:
|
||||||
this.play(AudioID.Ring);
|
this.play(AudioID.Ring);
|
||||||
|
@ -615,23 +656,23 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
private showICEFallbackPrompt() {
|
private showICEFallbackPrompt() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const code = sub => <code>{sub}</code>;
|
const code = sub => <code>{ sub }</code>;
|
||||||
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
||||||
title: _t("Call failed due to misconfigured server"),
|
title: _t("Call failed due to misconfigured server"),
|
||||||
description: <div>
|
description: <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Please ask the administrator of your homeserver " +
|
"Please ask the administrator of your homeserver " +
|
||||||
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
||||||
"order for calls to work reliably.",
|
"order for calls to work reliably.",
|
||||||
{ homeserverDomain: cli.getDomain() }, { code },
|
{ homeserverDomain: cli.getDomain() }, { code },
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Alternatively, you can try to use the public server at " +
|
"Alternatively, you can try to use the public server at " +
|
||||||
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
||||||
"it will share your IP address with that server. You can also manage " +
|
"it will share your IP address with that server. You can also manage " +
|
||||||
"this in Settings.",
|
"this in Settings.",
|
||||||
null, { code },
|
null, { code },
|
||||||
)}</p>
|
) }</p>
|
||||||
</div>,
|
</div>,
|
||||||
button: _t('Try using turn.matrix.org'),
|
button: _t('Try using turn.matrix.org'),
|
||||||
cancelButton: _t('OK'),
|
cancelButton: _t('OK'),
|
||||||
|
@ -649,19 +690,19 @@ export default class CallHandler extends EventEmitter {
|
||||||
if (call.type === CallType.Voice) {
|
if (call.type === CallType.Voice) {
|
||||||
title = _t("Unable to access microphone");
|
title = _t("Unable to access microphone");
|
||||||
description = <div>
|
description = <div>
|
||||||
{_t(
|
{ _t(
|
||||||
"Call failed because microphone could not be accessed. " +
|
"Call failed because microphone could not be accessed. " +
|
||||||
"Check that a microphone is plugged in and set up correctly.",
|
"Check that a microphone is plugged in and set up correctly.",
|
||||||
)}
|
) }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (call.type === CallType.Video) {
|
} else if (call.type === CallType.Video) {
|
||||||
title = _t("Unable to access webcam / microphone");
|
title = _t("Unable to access webcam / microphone");
|
||||||
description = <div>
|
description = <div>
|
||||||
{_t("Call failed because webcam or microphone could not be accessed. Check that:")}
|
{ _t("Call failed because webcam or microphone could not be accessed. Check that:") }
|
||||||
<ul>
|
<ul>
|
||||||
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
|
<li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
|
||||||
<li>{_t("Permission is granted to use the webcam")}</li>
|
<li>{ _t("Permission is granted to use the webcam") }</li>
|
||||||
<li>{_t("No other application is using the webcam")}</li>
|
<li>{ _t("No other application is using the webcam") }</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,10 +425,10 @@ export default class ContentMessages {
|
||||||
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
|
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
|
||||||
title: _t('Replying With Files'),
|
title: _t('Replying With Files'),
|
||||||
description: (
|
description: (
|
||||||
<div>{_t(
|
<div>{ _t(
|
||||||
'At this time it is not possible to reply with a file. ' +
|
'At this time it is not possible to reply with a file. ' +
|
||||||
'Would you like to upload this file without replying?',
|
'Would you like to upload this file without replying?',
|
||||||
)}</div>
|
) }</div>
|
||||||
),
|
),
|
||||||
hasCancelButton: true,
|
hasCancelButton: true,
|
||||||
button: _t("Continue"),
|
button: _t("Continue"),
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
|
||||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
|
|
||||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -58,28 +59,28 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||||
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||||
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
MatrixClientPeg.get().on('accountData', this.onAccountData);
|
||||||
MatrixClientPeg.get().on('sync', this._onSync);
|
MatrixClientPeg.get().on('sync', this.onSync);
|
||||||
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
|
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
MatrixClientPeg.get().removeListener('sync', this.onSync);
|
||||||
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
|
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
if (this.dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
@ -103,15 +104,15 @@ export default class DeviceListener {
|
||||||
this.dismissed.add(d);
|
this.dismissed.add(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissEncryptionSetup() {
|
dismissEncryptionSetup() {
|
||||||
this.dismissedThisDeviceToast = true;
|
this.dismissedThisDeviceToast = true;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
_ensureDeviceIdsAtStartPopulated() {
|
private ensureDeviceIdsAtStartPopulated() {
|
||||||
if (this.ourDeviceIdsAtStart === null) {
|
if (this.ourDeviceIdsAtStart === null) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.ourDeviceIdsAtStart = new Set(
|
this.ourDeviceIdsAtStart = new Set(
|
||||||
|
@ -120,39 +121,39 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
||||||
// If we didn't know about *any* devices before (ie. it's fresh login),
|
// If we didn't know about *any* devices before (ie. it's fresh login),
|
||||||
// then they are all pre-existing devices, so ignore this and set the
|
// then they are all pre-existing devices, so ignore this and set the
|
||||||
// devicesAtStart list to the devices that we see after the fetch.
|
// devicesAtStart list to the devices that we see after the fetch.
|
||||||
if (initialFetch) return;
|
if (initialFetch) return;
|
||||||
|
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated();
|
if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated();
|
||||||
|
|
||||||
// No need to do a recheck here: we just need to get a snapshot of our devices
|
// No need to do a recheck here: we just need to get a snapshot of our devices
|
||||||
// before we download any new ones.
|
// before we download any new ones.
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDevicesUpdated = (users: string[]) => {
|
private onDevicesUpdated = (users: string[]) => {
|
||||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDeviceVerificationChanged = (userId: string) => {
|
private onDeviceVerificationChanged = (userId: string) => {
|
||||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUserTrustStatusChanged = (userId: string) => {
|
private onUserTrustStatusChanged = (userId: string) => {
|
||||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCrossSingingKeysChanged = () => {
|
private onCrossSingingKeysChanged = () => {
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAccountData = (ev) => {
|
private onAccountData = (ev: MatrixEvent) => {
|
||||||
// User may have:
|
// User may have:
|
||||||
// * migrated SSSS to symmetric
|
// * migrated SSSS to symmetric
|
||||||
// * uploaded keys to secret storage
|
// * uploaded keys to secret storage
|
||||||
|
@ -163,32 +164,32 @@ export default class DeviceListener {
|
||||||
ev.getType().startsWith('m.cross_signing.') ||
|
ev.getType().startsWith('m.cross_signing.') ||
|
||||||
ev.getType() === 'm.megolm_backup.v1'
|
ev.getType() === 'm.megolm_backup.v1'
|
||||||
) {
|
) {
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSync = (state, prevState) => {
|
private onSync = (state, prevState) => {
|
||||||
if (state === 'PREPARED' && prevState === null) this._recheck();
|
if (state === 'PREPARED' && prevState === null) this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomStateEvents = (ev: MatrixEvent) => {
|
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||||
if (ev.getType() !== "m.room.encryption") {
|
if (ev.getType() !== "m.room.encryption") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a room changes to encrypted, re-check as it may be our first
|
// If a room changes to encrypted, re-check as it may be our first
|
||||||
// encrypted room. This also catches encrypted room creation as well.
|
// encrypted room. This also catches encrypted room creation as well.
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAction = ({ action }) => {
|
private onAction = ({ action }: ActionPayload) => {
|
||||||
if (action !== "on_logged_in") return;
|
if (action !== "on_logged_in") return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
// The server doesn't tell us when key backup is set up, so we poll
|
// The server doesn't tell us when key backup is set up, so we poll
|
||||||
// & cache the result
|
// & cache the result
|
||||||
async _getKeyBackupInfo() {
|
private async getKeyBackupInfo() {
|
||||||
const now = (new Date()).getTime();
|
const now = (new Date()).getTime();
|
||||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
@ -206,7 +207,7 @@ export default class DeviceListener {
|
||||||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _recheck() {
|
private async recheck() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
|
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
|
||||||
|
@ -235,7 +236,7 @@ export default class DeviceListener {
|
||||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||||
} else {
|
} else {
|
||||||
const backupInfo = await this._getKeyBackupInfo();
|
const backupInfo = await this.getKeyBackupInfo();
|
||||||
if (backupInfo) {
|
if (backupInfo) {
|
||||||
// No cross-signing on account but key backup available (upgrade encryption)
|
// No cross-signing on account but key backup available (upgrade encryption)
|
||||||
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
||||||
|
@ -256,7 +257,7 @@ export default class DeviceListener {
|
||||||
|
|
||||||
// This needs to be done after awaiting on downloadKeys() above, so
|
// This needs to be done after awaiting on downloadKeys() above, so
|
||||||
// we make sure we get the devices after the fetch is done.
|
// we make sure we get the devices after the fetch is done.
|
||||||
this._ensureDeviceIdsAtStartPopulated();
|
this.ensureDeviceIdsAtStartPopulated();
|
||||||
|
|
||||||
// Unverified devices that were there last time the app ran
|
// Unverified devices that were there last time the app ran
|
||||||
// (technically could just be a boolean: we don't actually
|
// (technically could just be a boolean: we don't actually
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||||
import linkifyMatrix from './linkify-matrix';
|
import linkifyMatrix from './linkify-matrix';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||||
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
|
import { getEmojiFromUnicode } from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
|
@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char: string): string {
|
export function unicodeToShortcode(char: string): string {
|
||||||
const data = getEmojiFromUnicode(char);
|
const shortcodes = getEmojiFromUnicode(char).shortcodes;
|
||||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
return shortcodes.length > 0 ? `:${shortcodes[0]}:` : '';
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unicode character for an emoji shortcode
|
|
||||||
*
|
|
||||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
|
||||||
* @return {String} The emoji character; null if none exists
|
|
||||||
*/
|
|
||||||
export function shortcodeToUnicode(shortcode: string): string {
|
|
||||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
|
||||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
|
||||||
return data ? data.unicode : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processHtmlForSending(html: string): string {
|
export function processHtmlForSending(html: string): string {
|
||||||
|
|
|
@ -149,17 +149,17 @@ export default class IdentityAuthClient {
|
||||||
title: _t("Identity server has no terms of service"),
|
title: _t("Identity server has no terms of service"),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"This action requires accessing the default identity server " +
|
"This action requires accessing the default identity server " +
|
||||||
"<server /> to validate an email address or phone number, " +
|
"<server /> to validate an email address or phone number, " +
|
||||||
"but the server does not have any terms of service.", {},
|
"but the server does not have any terms of service.", {},
|
||||||
{
|
{
|
||||||
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
|
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||||
},
|
},
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Only continue if you trust the owner of the server.",
|
"Only continue if you trust the owner of the server.",
|
||||||
)}</p>
|
) }</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
button: _t("Trust"),
|
button: _t("Trust"),
|
||||||
|
|
|
@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
|
||||||
* This module provides a singleton instance of this class so the 'current'
|
* This module provides a singleton instance of this class so the 'current'
|
||||||
* Matrix Client object is available easily.
|
* Matrix Client object is available easily.
|
||||||
*/
|
*/
|
||||||
class _MatrixClientPeg implements IMatrixClientPeg {
|
class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
// These are the default options used when when the
|
// These are the default options used when when the
|
||||||
// client is started in 'start'. These can be altered
|
// client is started in 'start'. These can be altered
|
||||||
// at any time up to after the 'will_start_client'
|
// at any time up to after the 'will_start_client'
|
||||||
|
@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.mxMatrixClientPeg) {
|
if (!window.mxMatrixClientPeg) {
|
||||||
window.mxMatrixClientPeg = new _MatrixClientPeg();
|
window.mxMatrixClientPeg = new MatrixClientPegClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
||||||
|
|
|
@ -378,7 +378,7 @@ export class ModalManager {
|
||||||
const dialog = (
|
const dialog = (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{modal.elem}
|
{ modal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -438,14 +438,14 @@ export const Commands = [
|
||||||
'Identity server',
|
'Identity server',
|
||||||
QuestionDialog, {
|
QuestionDialog, {
|
||||||
title: _t("Use an identity server"),
|
title: _t("Use an identity server"),
|
||||||
description: <p>{_t(
|
description: <p>{ _t(
|
||||||
"Use an identity server to invite by email. " +
|
"Use an identity server to invite by email. " +
|
||||||
"Click continue to use the default identity server " +
|
"Click continue to use the default identity server " +
|
||||||
"(%(defaultIdentityServerName)s) or manage in Settings.",
|
"(%(defaultIdentityServerName)s) or manage in Settings.",
|
||||||
{
|
{
|
||||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||||
},
|
},
|
||||||
)}</p>,
|
) }</p>,
|
||||||
button: _t("Continue"),
|
button: _t("Continue"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -480,7 +480,7 @@ export const Commands = [
|
||||||
aliases: ['j', 'goto'],
|
aliases: ['j', 'goto'],
|
||||||
args: '<room-address>',
|
args: '<room-address>',
|
||||||
description: _td('Joins room with given address'),
|
description: _td('Joins room with given address'),
|
||||||
runFn: function(_, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
// Note: we support 2 versions of this command. The first is
|
// Note: we support 2 versions of this command. The first is
|
||||||
// the public-facing one for most users and the other is a
|
// the public-facing one for most users and the other is a
|
||||||
|
@ -1027,7 +1027,7 @@ export const Commands = [
|
||||||
command: "msg",
|
command: "msg",
|
||||||
description: _td("Sends a message to the given user"),
|
description: _td("Sends a message to the given user"),
|
||||||
args: "<user-id> <message>",
|
args: "<user-id> <message>",
|
||||||
runFn: function(_, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
// matches the first whitespace delimited group and then the rest of the string
|
// matches the first whitespace delimited group and then the rest of the string
|
||||||
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
import { isValid3pidInvite } from "./RoomInvite";
|
import { isValid3pidInvite } from "./RoomInvite";
|
||||||
|
@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
|
|
||||||
return () => {
|
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
|
||||||
return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallHangupEvent(event: MatrixEvent): () => string | null {
|
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
|
||||||
const eventContent = event.getContent();
|
|
||||||
let getReason = () => "";
|
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
getReason = () => _t('(not supported by this browser)');
|
|
||||||
} else if (eventContent.reason) {
|
|
||||||
if (eventContent.reason === "ice_failed") {
|
|
||||||
// We couldn't establish a connection at all
|
|
||||||
getReason = () => _t('(could not connect media)');
|
|
||||||
} else if (eventContent.reason === "ice_timeout") {
|
|
||||||
// We established a connection but it died
|
|
||||||
getReason = () => _t('(connection failed)');
|
|
||||||
} else if (eventContent.reason === "user_media_failed") {
|
|
||||||
// The other side couldn't open capture devices
|
|
||||||
getReason = () => _t("(their device couldn't start the camera / microphone)");
|
|
||||||
} else if (eventContent.reason === "unknown_error") {
|
|
||||||
// An error code the other side doesn't have a way to express
|
|
||||||
// (as opposed to an error code they gave but we don't know about,
|
|
||||||
// in which case we show the error code)
|
|
||||||
getReason = () => _t("(an error occurred)");
|
|
||||||
} else if (eventContent.reason === "invite_timeout") {
|
|
||||||
getReason = () => _t('(no answer)');
|
|
||||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
|
||||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
|
||||||
// it seems Android randomly sets a reason of "user hangup" which is
|
|
||||||
// interpreted as an error code :(
|
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
|
||||||
getReason = () => '';
|
|
||||||
} else {
|
|
||||||
getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
|
|
||||||
return () => {
|
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
|
||||||
return _t('%(senderName)s declined the call.', { senderName });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
|
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
|
||||||
// FIXME: Find a better way to determine this from the event?
|
|
||||||
let isVoice = true;
|
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
|
||||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
|
||||||
isVoice = false;
|
|
||||||
}
|
|
||||||
const isSupported = MatrixClientPeg.get().supportsVoip();
|
|
||||||
|
|
||||||
// This ladder could be reduced down to a couple string variables, however other languages
|
|
||||||
// can have a hard time translating those strings. In an effort to make translations easier
|
|
||||||
// and more accurate, we break out the string-based variables to a couple booleans.
|
|
||||||
if (isVoice && isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a voice call.", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (isVoice && !isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (!isVoice && isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a video call.", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (!isVoice && !isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
|
|
||||||
|
@ -652,10 +567,6 @@ interface IHandlers {
|
||||||
|
|
||||||
const handlers: IHandlers = {
|
const handlers: IHandlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
|
||||||
'm.call.hangup': textForCallHangupEvent,
|
|
||||||
'm.call.reject': textForCallRejectEvent,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateHandlers: IHandlers = {
|
const stateHandlers: IHandlers = {
|
||||||
|
|
|
@ -370,8 +370,8 @@ export const toggleDialog = () => {
|
||||||
const sections = categoryOrder.map(category => {
|
const sections = categoryOrder.map(category => {
|
||||||
const list = shortcuts[category];
|
const list = shortcuts[category];
|
||||||
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
|
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
|
||||||
<h3>{_t(category)}</h3>
|
<h3>{ _t(category) }</h3>
|
||||||
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>
|
<div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||||
{({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
|
{ ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
|
||||||
{ children }
|
{ children }
|
||||||
</div>}
|
</div> }
|
||||||
</RovingTabIndexProvider>;
|
</RovingTabIndexProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import PropTypes from 'prop-types';
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
|
import Spinner from "../../../../components/views/elements/Spinner";
|
||||||
|
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
import dis from "../../../../dispatcher/dispatcher";
|
import dis from "../../../../dispatcher/dispatcher";
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
|
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
|
||||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
import { Action } from "../../../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
disabling: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allows the user to disable the Event Index.
|
* Allows the user to disable the Event Index.
|
||||||
*/
|
*/
|
||||||
export default class DisableEventIndexDialog extends React.Component {
|
export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
disabling: false,
|
disabling: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisable = async () => {
|
private onDisable = async (): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
disabling: true,
|
disabling: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
||||||
await EventIndexPeg.deleteEventIndex();
|
await EventIndexPeg.deleteEventIndex();
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
||||||
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
|
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
|
||||||
{this.state.disabling ? <Spinner /> : <div />}
|
{ this.state.disabling ? <Spinner /> : <div /> }
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t('Disable')}
|
primaryButton={_t('Disable')}
|
||||||
onPrimaryButtonClick={this._onDisable}
|
onPrimaryButtonClick={this.onDisable}
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
cancelButtonClass="warning"
|
cancelButtonClass="warning"
|
||||||
onCancel={this.props.onFinished}
|
onCancel={this.props.onFinished}
|
|
@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDisable = async () => {
|
private onDisable = async () => {
|
||||||
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
|
||||||
import("./DisableEventIndexDialog"),
|
Modal.createTrackedDialog("Disable message search", "Disable message search",
|
||||||
|
DisableEventIndexDialog,
|
||||||
null, null, /* priority = */ false, /* static = */ true,
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
||||||
|
|
||||||
const eventIndexingSettings = (
|
const eventIndexingSettings = (
|
||||||
<div>
|
<div>
|
||||||
{_t(
|
{ _t(
|
||||||
"%(brand)s is securely caching encrypted messages locally for them " +
|
"%(brand)s is securely caching encrypted messages locally for them " +
|
||||||
"to appear in search results:",
|
"to appear in search results:",
|
||||||
{ brand },
|
{ brand },
|
||||||
)}
|
) }
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{crawlerState}<br />
|
{ crawlerState }<br />
|
||||||
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
{ _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
|
||||||
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
{ _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
|
||||||
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
|
{ _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
|
||||||
doneRooms: formatCountLong(doneRooms),
|
doneRooms: formatCountLong(doneRooms),
|
||||||
totalRooms: formatCountLong(this.state.roomCount),
|
totalRooms: formatCountLong(this.state.roomCount),
|
||||||
})} <br />
|
}) } <br />
|
||||||
<Field
|
<Field
|
||||||
label={_t('Message downloading sleep time(ms)')}
|
label={_t('Message downloading sleep time(ms)')}
|
||||||
type='number'
|
type='number'
|
||||||
|
@ -188,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Message search")}
|
title={_t("Message search")}
|
||||||
>
|
>
|
||||||
{eventIndexingSettings}
|
{ eventIndexingSettings }
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Done")}
|
primaryButton={_t("Done")}
|
||||||
onPrimaryButtonClick={this.props.onFinished}
|
onPrimaryButtonClick={this.props.onFinished}
|
||||||
|
|
|
@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||||
{ b: sub => <b>{sub}</b> },
|
{ b: sub => <b>{ sub }</b> },
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"We'll store an encrypted copy of your keys on our server. " +
|
"We'll store an encrypted copy of your keys on our server. " +
|
||||||
"Secure your backup with a Security Phrase.",
|
"Secure your backup with a Security Phrase.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
<p>{ _t("For maximum security, this should be different from your account password.") }</p>
|
||||||
|
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||||
|
@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<summary>{ _t("Advanced") }</summary>
|
||||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||||
{_t("Set up with a Security Key")}
|
{ _t("Set up with a Security Key") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</details>
|
</details>
|
||||||
</form>;
|
</form>;
|
||||||
|
@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
let passPhraseMatch = null;
|
let passPhraseMatch = null;
|
||||||
if (matchText) {
|
if (matchText) {
|
||||||
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
||||||
<div>{matchText}</div>
|
<div>{ matchText }</div>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||||
{changeText}
|
{ changeText }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Enter your Security Phrase a second time to confirm it.",
|
"Enter your Security Phrase a second time to confirm it.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||||
<div>
|
<div>
|
||||||
|
@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{passPhraseMatch}
|
{ passPhraseMatch }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
|
@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhaseShowKey() {
|
_renderPhaseShowKey() {
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Your Security Key is a safety net - you can use it to restore " +
|
"Your Security Key is a safety net - you can use it to restore " +
|
||||||
"access to your encrypted messages if you forget your Security Phrase.",
|
"access to your encrypted messages if you forget your Security Phrase.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
|
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
||||||
{_t("Your Security Key")}
|
{ _t("Your Security Key") }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_recoveryKey">
|
<div className="mx_CreateKeyBackupDialog_recoveryKey">
|
||||||
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
|
<code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
|
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
|
||||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||||
{_t("Copy")}
|
{ _t("Copy") }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||||
{_t("Download")}
|
{ _t("Download") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
if (this.state.copied) {
|
if (this.state.copied) {
|
||||||
introText = _t(
|
introText = _t(
|
||||||
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
|
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
|
||||||
{}, { b: s => <b>{s}</b> },
|
{}, { b: s => <b>{ s }</b> },
|
||||||
);
|
);
|
||||||
} else if (this.state.downloaded) {
|
} else if (this.state.downloaded) {
|
||||||
introText = _t(
|
introText = _t(
|
||||||
"Your Security Key is in your <b>Downloads</b> folder.",
|
"Your Security Key is in your <b>Downloads</b> folder.",
|
||||||
{}, { b: s => <b>{s}</b> },
|
{}, { b: s => <b>{ s }</b> },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
{introText}
|
{ introText }
|
||||||
<ul>
|
<ul>
|
||||||
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{s}</b> })}</li>
|
<li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
|
||||||
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{s}</b> })}</li>
|
<li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
|
||||||
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{s}</b> })}</li>
|
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
|
||||||
</ul>
|
</ul>
|
||||||
<DialogButtons primaryButton={_t("Continue")}
|
<DialogButtons primaryButton={_t("Continue")}
|
||||||
onPrimaryButtonClick={this._createBackup}
|
onPrimaryButtonClick={this._createBackup}
|
||||||
hasCancel={false}>
|
hasCancel={false}>
|
||||||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
_renderPhaseDone() {
|
_renderPhaseDone() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Your keys are being backed up (the first backup could take a few minutes).",
|
"Your keys are being backed up (the first backup could take a few minutes).",
|
||||||
)}</p>
|
) }</p>
|
||||||
<DialogButtons primaryButton={_t('OK')}
|
<DialogButtons primaryButton={_t('OK')}
|
||||||
onPrimaryButtonClick={this._onDone}
|
onPrimaryButtonClick={this._onDone}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
_renderPhaseOptOutConfirm() {
|
_renderPhaseOptOutConfirm() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
{_t(
|
{ _t(
|
||||||
"Without setting up Secure Message Recovery, you won't be able to restore your " +
|
"Without setting up Secure Message Recovery, you won't be able to restore your " +
|
||||||
"encrypted message history if you log out or use another session.",
|
"encrypted message history if you log out or use another session.",
|
||||||
)}
|
) }
|
||||||
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
|
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
|
||||||
onPrimaryButtonClick={this._onSetUpClick}
|
onPrimaryButtonClick={this._onSetUpClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
content = <div>
|
content = <div>
|
||||||
<p>{_t("Unable to create key backup")}</p>
|
<p>{ _t("Unable to create key backup") }</p>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<DialogButtons primaryButton={_t('Retry')}
|
<DialogButtons primaryButton={_t('Retry')}
|
||||||
onPrimaryButtonClick={this._createBackup}
|
onPrimaryButtonClick={this._createBackup}
|
||||||
|
@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -475,9 +475,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
||||||
{_t("Generate a Security Key")}
|
{ _t("Generate a Security Key") }
|
||||||
</div>
|
</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>
|
</StyledRadioButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -494,9 +494,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
||||||
{_t("Enter a Security Phrase")}
|
{ _t("Enter a Security Phrase") }
|
||||||
</div>
|
</div>
|
||||||
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
|
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
|
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
|
||||||
|
|
||||||
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
|
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
|
||||||
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
|
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
|
||||||
"Safeguard against losing access to encrypted messages & data by " +
|
"Safeguard against losing access to encrypted messages & data by " +
|
||||||
"backing up encryption keys on your server.",
|
"backing up encryption keys on your server.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
||||||
{optionKey}
|
{ optionKey }
|
||||||
{optionPassphrase}
|
{ optionPassphrase }
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Continue")}
|
primaryButton={_t("Continue")}
|
||||||
|
@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
let nextCaption = _t("Next");
|
let nextCaption = _t("Next");
|
||||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||||
authPrompt = <div>
|
authPrompt = <div>
|
||||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
<div>{ _t("Enter your account password to confirm the upgrade:") }</div>
|
||||||
<div><Field
|
<div><Field
|
||||||
type="password"
|
type="password"
|
||||||
label={_t("Password")}
|
label={_t("Password")}
|
||||||
|
@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
} else if (!this.state.backupSigStatus.usable) {
|
} else if (!this.state.backupSigStatus.usable) {
|
||||||
authPrompt = <div>
|
authPrompt = <div>
|
||||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
<div>{ _t("Restore your key backup to upgrade your encryption") }</div>
|
||||||
</div>;
|
</div>;
|
||||||
nextCaption = _t("Restore");
|
nextCaption = _t("Restore");
|
||||||
} else {
|
} else {
|
||||||
authPrompt = <p>
|
authPrompt = <p>
|
||||||
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
|
{ _t("You'll need to authenticate with the server to confirm the upgrade.") }
|
||||||
</p>;
|
</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <form onSubmit={this._onMigrateFormSubmit}>
|
return <form onSubmit={this._onMigrateFormSubmit}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Upgrade this session to allow it to verify other sessions, " +
|
"Upgrade this session to allow it to verify other sessions, " +
|
||||||
"granting them access to encrypted messages and marking them " +
|
"granting them access to encrypted messages and marking them " +
|
||||||
"as trusted for other users.",
|
"as trusted for other users.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div>{authPrompt}</div>
|
<div>{ authPrompt }</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={nextCaption}
|
primaryButton={nextCaption}
|
||||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||||
|
@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||||
>
|
>
|
||||||
<button type="button" className="danger" onClick={this._onCancelClick}>
|
<button type="button" className="danger" onClick={this._onCancelClick}>
|
||||||
{_t('Skip')}
|
{ _t('Skip') }
|
||||||
</button>
|
</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</form>;
|
</form>;
|
||||||
|
@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhasePassPhrase() {
|
_renderPhasePassPhrase() {
|
||||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Enter a security phrase only you know, as it’s used to safeguard your data. " +
|
"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.",
|
"To be secure, you shouldn’t re-use your account password.",
|
||||||
)}</p>
|
) }</p>
|
||||||
|
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<PassphraseField
|
<PassphraseField
|
||||||
|
@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={this._onCancelClick}
|
onClick={this._onCancelClick}
|
||||||
className="danger"
|
className="danger"
|
||||||
>{_t("Cancel")}</button>
|
>{ _t("Cancel") }</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
let passPhraseMatch = null;
|
let passPhraseMatch = null;
|
||||||
if (matchText) {
|
if (matchText) {
|
||||||
passPhraseMatch = <div>
|
passPhraseMatch = <div>
|
||||||
<div>{matchText}</div>
|
<div>{ matchText }</div>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||||
{changeText}
|
{ changeText }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Enter your Security Phrase a second time to confirm it.",
|
"Enter your Security Phrase a second time to confirm it.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<Field
|
<Field
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||||
{passPhraseMatch}
|
{ passPhraseMatch }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
|
@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={this._onCancelClick}
|
onClick={this._onCancelClick}
|
||||||
className="danger"
|
className="danger"
|
||||||
>{_t("Skip")}</button>
|
>{ _t("Skip") }</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
@ -691,35 +691,35 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Store your Security Key somewhere safe, like a password manager or a safe, " +
|
"Store your Security Key somewhere safe, like a password manager or a safe, " +
|
||||||
"as it’s used to safeguard your encrypted data.",
|
"as it’s used to safeguard your encrypted data.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||||
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
|
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||||
<AccessibleButton kind='primary' className="mx_Dialog_primary"
|
<AccessibleButton kind='primary' className="mx_Dialog_primary"
|
||||||
onClick={this._onDownloadClick}
|
onClick={this._onDownloadClick}
|
||||||
disabled={this.state.phase === PHASE_STORING}
|
disabled={this.state.phase === PHASE_STORING}
|
||||||
>
|
>
|
||||||
{_t("Download")}
|
{ _t("Download") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<span>{_t("or")}</span>
|
<span>{ _t("or") }</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='primary'
|
kind='primary'
|
||||||
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
||||||
onClick={this._onCopyClick}
|
onClick={this._onCopyClick}
|
||||||
disabled={this.state.phase === PHASE_STORING}
|
disabled={this.state.phase === PHASE_STORING}
|
||||||
>
|
>
|
||||||
{this.state.copied ? _t("Copied!") : _t("Copy")}
|
{ this.state.copied ? _t("Copied!") : _t("Copy") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{continueButton}
|
{ continueButton }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,7 +732,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhaseLoadError() {
|
_renderPhaseLoadError() {
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t("Unable to query secret storage status")}</p>
|
<p>{ _t("Unable to query secret storage status") }</p>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<DialogButtons primaryButton={_t('Retry')}
|
<DialogButtons primaryButton={_t('Retry')}
|
||||||
onPrimaryButtonClick={this._onLoadRetryClick}
|
onPrimaryButtonClick={this._onLoadRetryClick}
|
||||||
|
@ -745,17 +745,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhaseSkipConfirm() {
|
_renderPhaseSkipConfirm() {
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
|
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"You can also set up Secure Backup & manage your keys in Settings.",
|
"You can also set up Secure Backup & manage your keys in Settings.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<DialogButtons primaryButton={_t('Go back')}
|
<DialogButtons primaryButton={_t('Go back')}
|
||||||
onPrimaryButtonClick={this._onGoBackClick}
|
onPrimaryButtonClick={this._onGoBackClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
>
|
>
|
||||||
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button>
|
<button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -787,7 +787,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
let content;
|
let content;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
content = <div>
|
content = <div>
|
||||||
<p>{_t("Unable to set up secret storage")}</p>
|
<p>{ _t("Unable to set up secret storage") }</p>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<DialogButtons primaryButton={_t('Retry')}
|
<DialogButtons primaryButton={_t('Retry')}
|
||||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||||
|
@ -857,7 +857,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||||
|
|
||||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||||
{_t("New Recovery Method")}
|
{ _t("New Recovery Method") }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
const newMethodDetected = <p>{_t(
|
const newMethodDetected = <p>{ _t(
|
||||||
"A new Security Phrase and key for Secure Messages have been detected.",
|
"A new Security Phrase and key for Secure Messages have been detected.",
|
||||||
)}</p>;
|
) }</p>;
|
||||||
|
|
||||||
const hackWarning = <p className="warning">{_t(
|
const hackWarning = <p className="warning">{ _t(
|
||||||
"If you didn't set the new recovery method, an " +
|
"If you didn't set the new recovery method, an " +
|
||||||
"attacker may be trying to access your account. " +
|
"attacker may be trying to access your account. " +
|
||||||
"Change your account password and set a new recovery " +
|
"Change your account password and set a new recovery " +
|
||||||
"method immediately in Settings.",
|
"method immediately in Settings.",
|
||||||
)}</p>;
|
) }</p>;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
content = <div>
|
content = <div>
|
||||||
{newMethodDetected}
|
{ newMethodDetected }
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"This session is encrypting history using the new recovery method.",
|
"This session is encrypting history using the new recovery method.",
|
||||||
)}</p>
|
) }</p>
|
||||||
{hackWarning}
|
{ hackWarning }
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("OK")}
|
primaryButton={_t("OK")}
|
||||||
onPrimaryButtonClick={this.onOkClick}
|
onPrimaryButtonClick={this.onOkClick}
|
||||||
|
@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
content = <div>
|
content = <div>
|
||||||
{newMethodDetected}
|
{ newMethodDetected }
|
||||||
{hackWarning}
|
{ hackWarning }
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Set up Secure Messages")}
|
primaryButton={_t("Set up Secure Messages")}
|
||||||
onPrimaryButtonClick={this.onSetupClick}
|
onPrimaryButtonClick={this.onSetupClick}
|
||||||
|
@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{content}
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||||
|
|
||||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||||
{_t("Recovery Method Removed")}
|
{ _t("Recovery Method Removed") }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"This session has detected that your Security Phrase and key " +
|
"This session has detected that your Security Phrase and key " +
|
||||||
"for Secure Messages have been removed.",
|
"for Secure Messages have been removed.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"If you did this accidentally, you can setup Secure Messages on " +
|
"If you did this accidentally, you can setup Secure Messages on " +
|
||||||
"this session which will re-encrypt this session's message " +
|
"this session which will re-encrypt this session's message " +
|
||||||
"history with a new recovery method.",
|
"history with a new recovery method.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<p className="warning">{_t(
|
<p className="warning">{ _t(
|
||||||
"If you didn't remove the recovery method, an " +
|
"If you didn't remove the recovery method, an " +
|
||||||
"attacker may be trying to access your account. " +
|
"attacker may be trying to access your account. " +
|
||||||
"Change your account password and set a new recovery " +
|
"Change your account password and set a new recovery " +
|
||||||
"method immediately in Settings.",
|
"method immediately in Settings.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Set up Secure Messages")}
|
primaryButton={_t("Set up Secure Messages")}
|
||||||
onPrimaryButtonClick={this.onSetupClick}
|
onPrimaryButtonClick={this.onSetupClick}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
|
||||||
import { ICompletion, ISelectionRange } from './Autocompleter';
|
import { ICompletion, ISelectionRange } from './Autocompleter';
|
||||||
import { uniq, sortBy } from 'lodash';
|
import { uniq, sortBy } from 'lodash';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
|
||||||
import { EMOJI, IEmoji } from '../emoji';
|
import { EMOJI, IEmoji } from '../emoji';
|
||||||
|
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
|
@ -36,20 +35,18 @@ const LIMIT = 20;
|
||||||
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
|
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
|
||||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
|
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
|
||||||
|
|
||||||
interface IEmojiShort {
|
interface ISortedEmoji {
|
||||||
emoji: IEmoji;
|
emoji: IEmoji;
|
||||||
shortname: string;
|
|
||||||
_orderBy: number;
|
_orderBy: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
|
const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
|
||||||
if (a.group === b.group) {
|
if (a.group === b.group) {
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
}
|
}
|
||||||
return a.group - b.group;
|
return a.group - b.group;
|
||||||
}).map((emoji, index) => ({
|
}).map((emoji, index) => ({
|
||||||
emoji,
|
emoji,
|
||||||
shortname: `:${emoji.shortcodes[0]}:`,
|
|
||||||
// Include the index so that we can preserve the original order
|
// Include the index so that we can preserve the original order
|
||||||
_orderBy: index,
|
_orderBy: index,
|
||||||
}));
|
}));
|
||||||
|
@ -64,20 +61,18 @@ function score(query, space) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
matcher: QueryMatcher<IEmojiShort>;
|
matcher: QueryMatcher<ISortedEmoji>;
|
||||||
nameMatcher: QueryMatcher<IEmojiShort>;
|
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
|
this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
|
||||||
keys: ['emoji.emoticon', 'shortname'],
|
keys: ['emoji.emoticon'],
|
||||||
funcs: [
|
funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
|
||||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
|
||||||
],
|
|
||||||
// For matching against ascii equivalents
|
// For matching against ascii equivalents
|
||||||
shouldMatchWordsOnly: false,
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
|
||||||
keys: ['emoji.annotation'],
|
keys: ['emoji.annotation'],
|
||||||
// For removing punctuation
|
// For removing punctuation
|
||||||
shouldMatchWordsOnly: true,
|
shouldMatchWordsOnly: true,
|
||||||
|
@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
const sorters = [];
|
const sorters = [];
|
||||||
// make sure that emoticons come first
|
// make sure that emoticons come first
|
||||||
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
|
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
|
||||||
|
|
||||||
// then sort by score (Infinity if matchedString not in shortname)
|
// then sort by score (Infinity if matchedString not in shortcode)
|
||||||
sorters.push((c) => score(matchedString, c.shortname));
|
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
|
||||||
// then sort by max score of all shortcodes, trim off the `:`
|
// then sort by max score of all shortcodes, trim off the `:`
|
||||||
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
|
sorters.push(c => Math.min(
|
||||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
|
||||||
|
));
|
||||||
|
// If the matchedString is not empty, sort by length of shortcode. Example:
|
||||||
// matchedString = ":bookmark"
|
// matchedString = ":bookmark"
|
||||||
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
||||||
if (matchedString.length > 1) {
|
if (matchedString.length > 1) {
|
||||||
sorters.push((c) => c.shortname.length);
|
sorters.push(c => c.emoji.shortcodes[0].length);
|
||||||
}
|
}
|
||||||
// Finally, sort by original ordering
|
// Finally, sort by original ordering
|
||||||
sorters.push((c) => c._orderBy);
|
sorters.push(c => c._orderBy);
|
||||||
completions = sortBy(uniq(completions), sorters);
|
completions = sortBy(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map(({ shortname }) => {
|
completions = completions.map(c => ({
|
||||||
const unicode = shortcodeToUnicode(shortname);
|
completion: c.emoji.unicode,
|
||||||
return {
|
component: (
|
||||||
completion: unicode,
|
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
||||||
component: (
|
<span>{ c.emoji.unicode }</span>
|
||||||
<PillCompletion title={shortname} aria-label={unicode}>
|
</PillCompletion>
|
||||||
<span>{ unicode }</span>
|
),
|
||||||
</PillCompletion>
|
range,
|
||||||
),
|
})).slice(0, LIMIT);
|
||||||
range,
|
|
||||||
};
|
|
||||||
}).slice(0, LIMIT);
|
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
limit = -1,
|
limit = -1,
|
||||||
): Promise<ICompletion[]> {
|
): Promise<ICompletion[]> {
|
||||||
// lazy-load user list into matcher
|
// lazy-load user list into matcher
|
||||||
if (!this.users) this._makeUsers();
|
if (!this.users) this.makeUsers();
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
|
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
|
||||||
|
@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return _t('Users');
|
return _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeUsers() {
|
private makeUsers() {
|
||||||
const events = this.room.getLiveTimeline().getEvents();
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
|
||||||
|
|
145
src/components/structures/CallEventGrouper.ts
Normal file
145
src/components/structures/CallEventGrouper.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}>
|
return (<div className={classes}>
|
||||||
<div className="mx_CustomRoomTagPanel_divider" />
|
<div className="mx_CustomRoomTagPanel_divider" />
|
||||||
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
|
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
|
||||||
{tags}
|
{ tags }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
|
||||||
"mx_TagTile_badge": true,
|
"mx_TagTile_badge": true,
|
||||||
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
|
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
|
||||||
});
|
});
|
||||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
|
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -125,11 +125,11 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
|
|
||||||
if (this.props.scrollbar) {
|
if (this.props.scrollbar) {
|
||||||
return <AutoHideScrollbar className={classes}>
|
return <AutoHideScrollbar className={classes}>
|
||||||
{content}
|
{ content }
|
||||||
</AutoHideScrollbar>;
|
</AutoHideScrollbar>;
|
||||||
} else {
|
} else {
|
||||||
return <div className={classes}>
|
return <div className={classes}>
|
||||||
{content}
|
{ content }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,8 +241,8 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
|
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
||||||
<h2>{_t('No files visible in this room')}</h2>
|
<h2>{ _t('No files visible in this room') }</h2>
|
||||||
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
|
<p>{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }</p>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||||
|
@ -262,7 +262,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
manageReadMarkers={false}
|
manageReadMarkers={false}
|
||||||
timelineSet={this.state.timelineSet}
|
timelineSet={this.state.timelineSet}
|
||||||
showUrlPreview = {false}
|
showUrlPreview={false}
|
||||||
onPaginationRequest={this.onPaginationRequest}
|
onPaginationRequest={this.onPaginationRequest}
|
||||||
tileShape={TileShape.FileGrid}
|
tileShape={TileShape.FileGrid}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
return <div className='mx_GenericErrorPage'>
|
return <div className='mx_GenericErrorPage'>
|
||||||
<div className='mx_GenericErrorPage_box'>
|
<div className='mx_GenericErrorPage_box'>
|
||||||
<h1>{this.props.title}</h1>
|
<h1>{ this.props.title }</h1>
|
||||||
<p>{this.props.message}</p>
|
<p>{ this.props.message }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
|
||||||
let hostingSignup = null;
|
let hostingSignup = null;
|
||||||
if (hostingSignupLink && this.state.isUserPrivileged) {
|
if (hostingSignupLink && this.state.isUserPrivileged) {
|
||||||
hostingSignup = <div className="mx_GroupView_hostingSignup">
|
hostingSignup = <div className="mx_GroupView_hostingSignup">
|
||||||
{_t(
|
{ _t(
|
||||||
"Want more than a community? <a>Get your own server</a>", {},
|
"Want more than a community? <a>Get your own server</a>", {},
|
||||||
{
|
{
|
||||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
|
||||||
},
|
},
|
||||||
)}
|
) }
|
||||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
||||||
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onSelectRoom={this.selectRoom}
|
onSelectRoom={this.selectRoom}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{dialPadButton}
|
{ dialPadButton }
|
||||||
|
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={classNames("mx_LeftPanel_exploreButton", {
|
className={classNames("mx_LeftPanel_exploreButton", {
|
||||||
|
@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
leftLeftPanel = (
|
leftLeftPanel = (
|
||||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||||
<GroupFilterPanel />
|
<GroupFilterPanel />
|
||||||
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses} ref={this.ref}>
|
<div className={containerClasses} ref={this.ref}>
|
||||||
{leftLeftPanel}
|
{ leftLeftPanel }
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{ this.renderHeader() }
|
||||||
{this.renderSearchDialExplore()}
|
{ this.renderSearchDialExplore() }
|
||||||
{this.renderBreadcrumbs()}
|
{ this.renderBreadcrumbs() }
|
||||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||||
<div className="mx_LeftPanel_roomListWrapper">
|
<div className="mx_LeftPanel_roomListWrapper">
|
||||||
<div
|
<div
|
||||||
|
@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
// overflow:scroll;, so force it out of tab order.
|
// overflow:scroll;, so force it out of tab order.
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{roomList}
|
{ roomList }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
||||||
|
|
|
@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
|
||||||
<span>{ WidgetUtils.getWidgetName(app) }</span>
|
<span>{ WidgetUtils.getWidgetName(app) }</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
||||||
{/* Code for the maximise button for once we have full screen widgets */}
|
{ /* Code for the maximise button for once we have full screen widgets */ }
|
||||||
{/*<AccessibleTooltipButton
|
{ /*<AccessibleTooltipButton
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
}}
|
}}
|
||||||
className="mx_LeftPanelWidget_maximizeButton"
|
className="mx_LeftPanelWidget_maximizeButton"
|
||||||
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
|
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
|
||||||
title={_t("Maximize")}
|
title={_t("Maximize")}
|
||||||
/>*/}
|
/>*/ }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as PropTypes from 'prop-types';
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
|
// transitioned to PWLU)
|
||||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||||
hideToSRUsers: boolean;
|
hideToSRUsers: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -140,18 +142,6 @@ interface IState {
|
||||||
class LoggedInView extends React.Component<IProps, IState> {
|
class LoggedInView extends React.Component<IProps, IState> {
|
||||||
static displayName = 'LoggedInView';
|
static displayName = 'LoggedInView';
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
page_type: PropTypes.string.isRequired,
|
|
||||||
onRoomCreated: PropTypes.func,
|
|
||||||
|
|
||||||
// Called with the credentials of a registered user (if they were a ROU that
|
|
||||||
// transitioned to PWLU)
|
|
||||||
onRegistered: PropTypes.func,
|
|
||||||
|
|
||||||
// and lots and lots of other stuff.
|
|
||||||
};
|
|
||||||
|
|
||||||
protected readonly _matrixClient: MatrixClient;
|
protected readonly _matrixClient: MatrixClient;
|
||||||
protected readonly _roomView: React.RefObject<any>;
|
protected readonly _roomView: React.RefObject<any>;
|
||||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||||
|
@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
|
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
|
|
||||||
this._matrixClient.on("accountData", this.onAccountData);
|
this._matrixClient.on("accountData", this.onAccountData);
|
||||||
this._matrixClient.on("sync", this.onSync);
|
this._matrixClient.on("sync", this.onSync);
|
||||||
|
@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resizer = this._createResizer();
|
this.resizer = this.createResizer();
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
|
@ -221,37 +211,37 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
canResetTimelineInRoom = (roomId) => {
|
public canResetTimelineInRoom = (roomId: string) => {
|
||||||
if (!this._roomView.current) {
|
if (!this._roomView.current) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this._roomView.current.canResetTimeline();
|
return this._roomView.current.canResetTimeline();
|
||||||
};
|
};
|
||||||
|
|
||||||
_createResizer() {
|
private createResizer() {
|
||||||
let size;
|
let panelSize;
|
||||||
let collapsed;
|
let panelCollapsed;
|
||||||
const collapseConfig: ICollapseConfig = {
|
const collapseConfig: ICollapseConfig = {
|
||||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||||
toggleSize: 206 - 50,
|
toggleSize: 206 - 50,
|
||||||
onCollapsed: (_collapsed) => {
|
onCollapsed: (collapsed) => {
|
||||||
collapsed = _collapsed;
|
panelCollapsed = collapsed;
|
||||||
if (_collapsed) {
|
if (collapsed) {
|
||||||
dis.dispatch({ action: "hide_left_panel" });
|
dis.dispatch({ action: "hide_left_panel" });
|
||||||
window.localStorage.setItem("mx_lhs_size", '0');
|
window.localStorage.setItem("mx_lhs_size", '0');
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({ action: "show_left_panel" });
|
dis.dispatch({ action: "show_left_panel" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResized: (_size) => {
|
onResized: (size) => {
|
||||||
size = _size;
|
panelSize = size;
|
||||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||||
},
|
},
|
||||||
onResizeStart: () => {
|
onResizeStart: () => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
},
|
},
|
||||||
onResizeStop: () => {
|
onResizeStop: () => {
|
||||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
},
|
},
|
||||||
isItemCollapsed: domNode => {
|
isItemCollapsed: domNode => {
|
||||||
|
@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadResizerPreferences() {
|
private loadResizerPreferences() {
|
||||||
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
||||||
if (isNaN(lhsSize)) {
|
if (isNaN(lhsSize)) {
|
||||||
lhsSize = 350;
|
lhsSize = 350;
|
||||||
|
@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
this.resizer.forHandleAt(0).resize(lhsSize);
|
this.resizer.forHandleAt(0).resize(lhsSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccountData = (event) => {
|
private onAccountData = (event: MatrixEvent) => {
|
||||||
if (event.getType() === "m.ignored_user_list") {
|
if (event.getType() === "m.ignored_user_list") {
|
||||||
dis.dispatch({ action: "ignore_state_changed" });
|
dis.dispatch({ action: "ignore_state_changed" });
|
||||||
}
|
}
|
||||||
|
@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
} else {
|
} else {
|
||||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateEvents = (ev, state) => {
|
onRoomStateEvents = (ev, state) => {
|
||||||
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
||||||
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
|
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -326,7 +316,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||||
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||||
if (error) {
|
if (error) {
|
||||||
usageLimitEventContent = syncError.error.data;
|
usageLimitEventContent = syncError.error.data;
|
||||||
|
@ -346,7 +336,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerNoticeEvents = async () => {
|
private updateServerNoticeEvents = async () => {
|
||||||
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
||||||
if (!serverNoticeList) return [];
|
if (!serverNoticeList) return [];
|
||||||
|
|
||||||
|
@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||||
this.setState({
|
this.setState({
|
||||||
usageLimitEventContent,
|
usageLimitEventContent,
|
||||||
usageLimitEventTs: pinnedEventTs,
|
usageLimitEventTs: pinnedEventTs,
|
||||||
|
@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPaste = (ev) => {
|
private onPaste = (ev) => {
|
||||||
let canReceiveInput = false;
|
let canReceiveInput = false;
|
||||||
let element = ev.target;
|
let element = ev.target;
|
||||||
// test for all parents because the target can be a child of a contenteditable element
|
// test for all parents because the target can be a child of a contenteditable element
|
||||||
|
@ -425,22 +415,22 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
We also listen with a native listener on the document to get keydown events when no element is focused.
|
We also listen with a native listener on the document to get keydown events when no element is focused.
|
||||||
Bubbling is irrelevant here as the target is the body element.
|
Bubbling is irrelevant here as the target is the body element.
|
||||||
*/
|
*/
|
||||||
_onReactKeyDown = (ev) => {
|
private onReactKeyDown = (ev) => {
|
||||||
// events caught while bubbling up on the root element
|
// events caught while bubbling up on the root element
|
||||||
// of this component, so something must be focused.
|
// of this component, so something must be focused.
|
||||||
this._onKeyDown(ev);
|
this.onKeyDown(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onNativeKeyDown = (ev) => {
|
private onNativeKeyDown = (ev) => {
|
||||||
// only pass this if there is no focused element.
|
// only pass this if there is no focused element.
|
||||||
// if there is, _onKeyDown will be called by the
|
// if there is, onKeyDown will be called by the
|
||||||
// react keydown handler that respects the react bubbling order.
|
// react keydown handler that respects the react bubbling order.
|
||||||
if (ev.target === document.body) {
|
if (ev.target === document.body) {
|
||||||
this._onKeyDown(ev);
|
this.onKeyDown(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (ev) => {
|
private onKeyDown = (ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
|
@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
case RoomAction.JumpToFirstMessage:
|
case RoomAction.JumpToFirstMessage:
|
||||||
case RoomAction.JumpToLatestMessage:
|
case RoomAction.JumpToLatestMessage:
|
||||||
// pass the event down to the scroll panel
|
// pass the event down to the scroll panel
|
||||||
this._onScrollKeyPressed(ev);
|
this.onScrollKeyPressed(ev);
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.FocusSearch:
|
case RoomAction.FocusSearch:
|
||||||
|
@ -565,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
* dispatch a page-up/page-down/etc to the appropriate component
|
* dispatch a page-up/page-down/etc to the appropriate component
|
||||||
* @param {Object} ev The key event
|
* @param {Object} ev The key event
|
||||||
*/
|
*/
|
||||||
_onScrollKeyPressed = (ev) => {
|
private onScrollKeyPressed = (ev) => {
|
||||||
if (this._roomView.current) {
|
if (this._roomView.current) {
|
||||||
this._roomView.current.handleScrollKey(ev);
|
this._roomView.current.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
|
@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
<div
|
<div
|
||||||
onPaste={this._onPaste}
|
onPaste={this.onPaste}
|
||||||
onKeyDown={this._onReactKeyDown}
|
onKeyDown={this.onReactKeyDown}
|
||||||
className='mx_MatrixChat_wrapper'
|
className='mx_MatrixChat_wrapper'
|
||||||
aria-hidden={this.props.hideToSRUsers}
|
aria-hidden={this.props.hideToSRUsers}
|
||||||
>
|
>
|
||||||
|
@ -644,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
<HostSignupContainer />
|
<HostSignupContainer />
|
||||||
{audioFeedArraysForCalls}
|
{ audioFeedArraysForCalls }
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillUpdate(props, state) {
|
UNSAFE_componentWillUpdate(props, state) {
|
||||||
if (this.shouldTrackPageChange(this.state, state)) {
|
if (this.shouldTrackPageChange(this.state, state)) {
|
||||||
this.startPageChangeTimer();
|
this.startPageChangeTimer();
|
||||||
|
@ -1112,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
if (memberCount === 1) {
|
if (memberCount === 1) {
|
||||||
warnings.push((
|
warnings.push((
|
||||||
<span className="warning" key="only_member_warning">
|
<span className="warning" key="only_member_warning">
|
||||||
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
{ ' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||||
{ _t("You are the only person here. " +
|
{ _t("You are the only person here. " +
|
||||||
"If you leave, no one will be able to join in the future, including you.") }
|
"If you leave, no one will be able to join in the future, including you.") }
|
||||||
</span>
|
</span>
|
||||||
|
@ -1127,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
if (rule !== "public") {
|
if (rule !== "public") {
|
||||||
warnings.push((
|
warnings.push((
|
||||||
<span className="warning" key="non_public_warning">
|
<span className="warning" key="non_public_warning">
|
||||||
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
{ ' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||||
{ isSpace
|
{ isSpace
|
||||||
? _t("This space is not public. You will not be able to rejoin without an invite.")
|
? _t("This space is not public. You will not be able to rejoin without an invite.")
|
||||||
: _t("This room is not public. You will not be able to rejoin without an invite.") }
|
: _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||||
|
@ -1155,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
: _t(
|
: _t(
|
||||||
"Are you sure you want to leave the room '%(roomName)s'?",
|
"Are you sure you want to leave the room '%(roomName)s'?",
|
||||||
{ roomName: roomToLeave.name },
|
{ roomName: roomToLeave.name },
|
||||||
)}
|
) }
|
||||||
{ warnings }
|
{ warnings }
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
dis.dispatch({ action: 'timeline_resize' });
|
dis.dispatch({ action: 'timeline_resize' });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomCreated(roomId: string) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: "view_room",
|
|
||||||
room_id: roomId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRegisterClick = () => {
|
onRegisterClick = () => {
|
||||||
this.showScreen("register");
|
this.showScreen("register");
|
||||||
};
|
};
|
||||||
|
@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
{...this.state}
|
{...this.state}
|
||||||
ref={this.loggedInView}
|
ref={this.loggedInView}
|
||||||
matrixClient={MatrixClientPeg.get()}
|
matrixClient={MatrixClientPeg.get()}
|
||||||
onRoomCreated={this.onRoomCreated}
|
|
||||||
onRegistered={this.onRegistered}
|
onRegistered={this.onRegistered}
|
||||||
currentRoomId={this.state.currentRoomId}
|
currentRoomId={this.state.currentRoomId}
|
||||||
/>
|
/>
|
||||||
|
@ -2053,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
let errorBox;
|
let errorBox;
|
||||||
if (this.state.syncError && !isStoreError) {
|
if (this.state.syncError && !isStoreError) {
|
||||||
errorBox = <div className="mx_MatrixChat_syncError">
|
errorBox = <div className="mx_MatrixChat_syncError">
|
||||||
{messageForSyncError(this.state.syncError)}
|
{ messageForSyncError(this.state.syncError) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
view = (
|
view = (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
{errorBox}
|
{ errorBox }
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||||
{_t('Logout')}
|
{ _t('Logout') }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2124,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ErrorBoundary>
|
return <ErrorBoundary>
|
||||||
{view}
|
{ view }
|
||||||
</ErrorBoundary>;
|
</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||||
|
import CallEventGrouper from "./CallEventGrouper";
|
||||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||||
import EventListSummary from '../views/elements/EventListSummary';
|
import EventListSummary from '../views/elements/EventListSummary';
|
||||||
|
@ -232,6 +233,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
private readonly showTypingNotificationsWatcherRef: string;
|
private readonly showTypingNotificationsWatcherRef: string;
|
||||||
private eventNodes: Record<string, HTMLElement>;
|
private eventNodes: Record<string, HTMLElement>;
|
||||||
|
|
||||||
|
// A map of <callId, CallEventGrouper>
|
||||||
|
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -576,6 +580,20 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
const last = (mxEv === lastShownEvent);
|
const last = (mxEv === lastShownEvent);
|
||||||
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
|
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
|
||||||
|
|
||||||
|
if (
|
||||||
|
mxEv.getType().indexOf("m.call.") === 0 ||
|
||||||
|
mxEv.getType().indexOf("org.matrix.call.") === 0
|
||||||
|
) {
|
||||||
|
const callId = mxEv.getContent().call_id;
|
||||||
|
if (this.callEventGroupers.has(callId)) {
|
||||||
|
this.callEventGroupers.get(callId).add(mxEv);
|
||||||
|
} else {
|
||||||
|
const callEventGrouper = new CallEventGrouper();
|
||||||
|
callEventGrouper.add(mxEv);
|
||||||
|
this.callEventGroupers.set(callId, callEventGrouper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (grouper) {
|
if (grouper) {
|
||||||
if (grouper.shouldGroup(mxEv)) {
|
if (grouper.shouldGroup(mxEv)) {
|
||||||
grouper.add(mxEv, this.showHiddenEvents);
|
grouper.add(mxEv, this.showHiddenEvents);
|
||||||
|
@ -653,8 +671,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let willWantDateSeparator = false;
|
let willWantDateSeparator = false;
|
||||||
|
let lastInSection = true;
|
||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
||||||
|
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
|
@ -690,6 +710,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
// it's successful: we received it.
|
// it's successful: we received it.
|
||||||
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
|
const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
|
||||||
|
|
||||||
// use txnId as key if available so that we don't remount during sending
|
// use txnId as key if available so that we don't remount during sending
|
||||||
ret.push(
|
ret.push(
|
||||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
|
@ -712,7 +734,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
last={last}
|
last={last}
|
||||||
lastInSection={willWantDateSeparator}
|
lastInSection={lastInSection}
|
||||||
lastSuccessful={isLastSuccessful}
|
lastSuccessful={isLastSuccessful}
|
||||||
isSelectedEvent={highlight}
|
isSelectedEvent={highlight}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
@ -720,6 +742,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
enableFlair={this.props.enableFlair}
|
enableFlair={this.props.enableFlair}
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
|
callEventGrouper={callEventGrouper}
|
||||||
|
hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
|
||||||
/>
|
/>
|
||||||
</TileErrorBoundary>,
|
</TileErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -121,7 +121,7 @@ export default class MyGroups extends React.Component {
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
{ /*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||||
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
|
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -137,7 +137,7 @@ export default class MyGroups extends React.Component {
|
||||||
{ 'i': (sub) => <i>{ sub }</i> })
|
{ 'i': (sub) => <i>{ sub }</i> })
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>*/}
|
</div>*/ }
|
||||||
</div>
|
</div>
|
||||||
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
|
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
|
|
|
@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
|
||||||
const toasts = this.state.toasts.map((t, i) => {
|
const toasts = this.state.toasts.map((t, i) => {
|
||||||
return (
|
return (
|
||||||
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
|
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
|
||||||
{React.createElement(t, {})}
|
{ React.createElement(t, {}) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_NonUrgentToastContainer" role="alert">
|
<div className="mx_NonUrgentToastContainer" role="alert">
|
||||||
{toasts}
|
{ toasts }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,8 @@ interface IProps {
|
||||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{_t('You’re all caught up')}</h2>
|
<h2>{ _t('You’re all caught up') }</h2>
|
||||||
<p>{_t('You have no visible notifications.')}</p>
|
<p>{ _t('You have no visible notifications.') }</p>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this.unregisterGroupStore();
|
this.unregisterGroupStore();
|
||||||
this.initGroupStore(newProps.groupId);
|
this.initGroupStore(newProps.groupId);
|
||||||
|
@ -174,7 +175,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => {
|
||||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -589,7 +589,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||||
return [
|
return [
|
||||||
<div
|
<div
|
||||||
key={ `${room.room_id}_avatar` }
|
key={`${room.room_id}_avatar`}
|
||||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
className="mx_RoomDirectory_roomAvatar"
|
className="mx_RoomDirectory_roomAvatar"
|
||||||
>
|
>
|
||||||
|
@ -603,7 +603,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
key={ `${room.room_id}_description` }
|
key={`${room.room_id}_description`}
|
||||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
className="mx_RoomDirectory_roomDescription"
|
className="mx_RoomDirectory_roomDescription"
|
||||||
>
|
>
|
||||||
|
@ -626,14 +626,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
key={ `${room.room_id}_memberCount` }
|
key={`${room.room_id}_memberCount`}
|
||||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
className="mx_RoomDirectory_roomMemberCount"
|
className="mx_RoomDirectory_roomMemberCount"
|
||||||
>
|
>
|
||||||
{ room.num_joined_members }
|
{ room.num_joined_members }
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
key={ `${room.room_id}_preview` }
|
key={`${room.room_id}_preview`}
|
||||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||||
className="mx_RoomDirectory_preview"
|
className="mx_RoomDirectory_preview"
|
||||||
|
@ -641,7 +641,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
{ previewButton }
|
{ previewButton }
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
key={ `${room.room_id}_join` }
|
key={`${room.room_id}_join`}
|
||||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
className="mx_RoomDirectory_join"
|
className="mx_RoomDirectory_join"
|
||||||
>
|
>
|
||||||
|
@ -796,7 +796,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
showJoinButton={showJoinButton}
|
showJoinButton={showJoinButton}
|
||||||
initialText={this.props.initialText}
|
initialText={this.props.initialText}
|
||||||
/>
|
/>
|
||||||
{dropdown}
|
{ dropdown }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const explanation =
|
const explanation =
|
||||||
|
@ -814,16 +814,16 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}) : _t("Explore rooms");
|
}) : _t("Explore rooms");
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className={'mx_RoomDirectory_dialog'}
|
className="mx_RoomDirectory_dialog"
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this.onFinished}
|
onFinished={this.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className="mx_RoomDirectory">
|
<div className="mx_RoomDirectory">
|
||||||
{explanation}
|
{ explanation }
|
||||||
<div className="mx_RoomDirectory_list">
|
<div className="mx_RoomDirectory_list">
|
||||||
{listHeader}
|
{ listHeader }
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -209,9 +209,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{icon}
|
{ icon }
|
||||||
{input}
|
{ input }
|
||||||
{clearButton}
|
{ clearButton }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,17 +222,17 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
|
|
||||||
let buttonRow = <>
|
let buttonRow = <>
|
||||||
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
{_t("Delete all")}
|
{ _t("Delete all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||||
{_t("Retry all")}
|
{ _t("Retry all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
if (this.state.isResending) {
|
if (this.state.isResending) {
|
||||||
buttonRow = <>
|
buttonRow = <>
|
||||||
<InlineSpinner w={20} h={20} />
|
<InlineSpinner w={20} h={20} />
|
||||||
{/* span for css */}
|
{ /* span for css */ }
|
||||||
<span>{_t("Sending")}</span>
|
<span>{ _t("Sending") }</span>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_unsentButtonBar">
|
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||||
{buttonRow}
|
{ buttonRow }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -270,10 +270,10 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
height="24" title="/!\ " alt="/!\ " />
|
height="24" title="/!\ " alt="/!\ " />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
{_t('Connectivity to the server has been lost.')}
|
{ _t('Connectivity to the server has been lost.') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
{_t('Sent messages will be stored until your connection has returned.')}
|
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1892,10 +1892,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomView_auxPanel_hiddenHighlights"
|
className="mx_RoomView_auxPanel_hiddenHighlights"
|
||||||
onClick={this.onHiddenHighlightsClick}
|
onClick={this.onHiddenHighlightsClick}
|
||||||
>
|
>
|
||||||
{_t(
|
{ _t(
|
||||||
"You have %(count)s unread notifications in a prior version of this room.",
|
"You have %(count)s unread notifications in a prior version of this room.",
|
||||||
{ count: hiddenHighlightCount },
|
{ count: hiddenHighlightCount },
|
||||||
)}
|
) }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2007,7 +2007,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
onUserScroll={this.onUserScroll}
|
onUserScroll={this.onUserScroll}
|
||||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||||
showUrlPreview = {this.state.showUrlPreview}
|
showUrlPreview={this.state.showUrlPreview}
|
||||||
className={messagePanelClassNames}
|
className={messagePanelClassNames}
|
||||||
membersLoaded={this.state.membersLoaded}
|
membersLoaded={this.state.membersLoaded}
|
||||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||||
|
@ -2057,7 +2057,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={this.state}>
|
<RoomContext.Provider value={this.state}>
|
||||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||||
{showChatEffects && this.roomView.current &&
|
{ showChatEffects && this.roomView.current &&
|
||||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||||
}
|
}
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
@ -2076,22 +2076,22 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<div className="mx_RoomView_body">
|
<div className="mx_RoomView_body">
|
||||||
{auxPanel}
|
{ auxPanel }
|
||||||
<div className={timelineClasses}>
|
<div className={timelineClasses}>
|
||||||
{fileDropTarget}
|
{ fileDropTarget }
|
||||||
{topUnreadMessagesBar}
|
{ topUnreadMessagesBar }
|
||||||
{jumpToBottom}
|
{ jumpToBottom }
|
||||||
{messagePanel}
|
{ messagePanel }
|
||||||
{searchResultsPanel}
|
{ searchResultsPanel }
|
||||||
</div>
|
</div>
|
||||||
<div className={statusBarAreaClass}>
|
<div className={statusBarAreaClass}>
|
||||||
<div className="mx_RoomView_statusAreaBox">
|
<div className="mx_RoomView_statusAreaBox">
|
||||||
<div className="mx_RoomView_statusAreaBox_line" />
|
<div className="mx_RoomView_statusAreaBox_line" />
|
||||||
{statusBar}
|
{ statusBar }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{previewBar}
|
{ previewBar }
|
||||||
{messageComposer}
|
{ messageComposer }
|
||||||
</div>
|
</div>
|
||||||
</MainSplit>
|
</MainSplit>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default class SearchBox extends React.Component {
|
||||||
key="button"
|
key="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={ () => {this._clearSearch("button"); } }>
|
onClick={() => {this._clearSearch("button"); }}>
|
||||||
</AccessibleButton>) : undefined;
|
</AccessibleButton>) : undefined;
|
||||||
|
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
|
@ -153,12 +153,12 @@ export default class SearchBox extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
ref={this._search}
|
ref={this._search}
|
||||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||||
value={ this.state.searchTerm }
|
value={this.state.searchTerm}
|
||||||
onFocus={ this._onFocus }
|
onFocus={this._onFocus}
|
||||||
onChange={ this.onChange }
|
onChange={this.onChange}
|
||||||
onKeyDown={ this._onKeyDown }
|
onKeyDown={this._onKeyDown}
|
||||||
onBlur={this._onBlur}
|
onBlur={this._onBlur}
|
||||||
placeholder={ placeholder }
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={this.props.autoFocus}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -404,7 +404,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
if (summaryError) {
|
if (summaryError) {
|
||||||
return <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
@ -569,7 +569,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
return <>
|
return <>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={ _t("Search names and descriptions") }
|
placeholder={_t("Search names and descriptions")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
initialValue={initialText}
|
initialValue={initialText}
|
||||||
|
@ -608,7 +608,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }
|
||||||
{ _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
|
{ _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
|
||||||
null,
|
null,
|
||||||
{ a: sub => {
|
{ a: sub => {
|
||||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{ sub }</AccessibleButton>;
|
||||||
} },
|
} },
|
||||||
) }
|
) }
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ const SpaceInfo = ({ space }) => {
|
||||||
return <div className="mx_SpaceRoomView_info">
|
return <div className="mx_SpaceRoomView_info">
|
||||||
{ visibilitySection }
|
{ visibilitySection }
|
||||||
{ joinRule === "public" && <RoomMemberCount room={space}>
|
{ joinRule === "public" && <RoomMemberCount room={space}>
|
||||||
{(count) => count > 0 ? (
|
{ (count) => count > 0 ? (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -158,7 +158,7 @@ const SpaceInfo = ({ space }) => {
|
||||||
>
|
>
|
||||||
{ _t("%(count)s members", { count }) }
|
{ _t("%(count)s members", { count }) }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
) : null}
|
) : null }
|
||||||
</RoomMemberCount> }
|
</RoomMemberCount> }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
@ -291,7 +291,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
</h1>
|
</h1>
|
||||||
<SpaceInfo space={space} />
|
<SpaceInfo space={space} />
|
||||||
<RoomTopic room={space}>
|
<RoomTopic room={space}>
|
||||||
{(topic, ref) =>
|
{ (topic, ref) =>
|
||||||
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
|
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
|
||||||
{ topic }
|
{ topic }
|
||||||
</div>
|
</div>
|
||||||
|
@ -417,12 +417,12 @@ const SpaceLanding = ({ space }) => {
|
||||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||||
<div className="mx_SpaceRoomView_landing_name">
|
<div className="mx_SpaceRoomView_landing_name">
|
||||||
<RoomName room={space}>
|
<RoomName room={space}>
|
||||||
{(name) => {
|
{ (name) => {
|
||||||
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
|
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
|
||||||
<h1>{ name }</h1>
|
<h1>{ name }</h1>
|
||||||
</div> };
|
</div> };
|
||||||
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
|
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
|
||||||
}}
|
} }
|
||||||
</RoomName>
|
</RoomName>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomView_landing_info">
|
<div className="mx_SpaceRoomView_landing_info">
|
||||||
|
@ -432,11 +432,11 @@ const SpaceLanding = ({ space }) => {
|
||||||
{ settingsButton }
|
{ settingsButton }
|
||||||
</div>
|
</div>
|
||||||
<RoomTopic room={space}>
|
<RoomTopic room={space}>
|
||||||
{(topic, ref) => (
|
{ (topic, ref) => (
|
||||||
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
|
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
|
||||||
{ topic }
|
{ topic }
|
||||||
</div>
|
</div>
|
||||||
)}
|
) }
|
||||||
</RoomTopic>
|
</RoomTopic>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -456,7 +456,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
const numFields = 3;
|
const numFields = 3;
|
||||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
const fields = new Array(numFields).fill(0).map((x, i) => {
|
||||||
const name = "roomName" + i;
|
const name = "roomName" + i;
|
||||||
return <Field
|
return <Field
|
||||||
key={name}
|
key={name}
|
||||||
|
@ -623,7 +623,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||||
const numFields = 3;
|
const numFields = 3;
|
||||||
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
||||||
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
||||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
const fields = new Array(numFields).fill(0).map((x, i) => {
|
||||||
const name = "emailAddress" + i;
|
const name = "emailAddress" + i;
|
||||||
return <Field
|
return <Field
|
||||||
key={name}
|
key={name}
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
tabLocation: TabLocation.LEFT,
|
tabLocation: TabLocation.LEFT,
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getActiveTabIndex() {
|
private getActiveTabIndex() {
|
||||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||||
return this.state.activeTabIndex;
|
return this.state.activeTabIndex;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
* @param {Tab} tab the tab to show
|
* @param {Tab} tab the tab to show
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _setActiveTab(tab: Tab) {
|
private setActiveTab(tab: Tab) {
|
||||||
const idx = this.props.tabs.indexOf(tab);
|
const idx = this.props.tabs.indexOf(tab);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
if (this.props.onChange) this.props.onChange(tab.id);
|
if (this.props.onChange) this.props.onChange(tab.id);
|
||||||
|
@ -94,23 +94,23 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTabLabel(tab: Tab) {
|
private renderTabLabel(tab: Tab) {
|
||||||
let classes = "mx_TabbedView_tabLabel ";
|
let classes = "mx_TabbedView_tabLabel ";
|
||||||
|
|
||||||
const idx = this.props.tabs.indexOf(tab);
|
const idx = this.props.tabs.indexOf(tab);
|
||||||
if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
|
if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
|
||||||
|
|
||||||
let tabIcon = null;
|
let tabIcon = null;
|
||||||
if (tab.icon) {
|
if (tab.icon) {
|
||||||
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
|
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickHandler = () => this._setActiveTab(tab);
|
const onClickHandler = () => this.setActiveTab(tab);
|
||||||
|
|
||||||
const label = _t(tab.label);
|
const label = _t(tab.label);
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
|
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
|
||||||
{tabIcon}
|
{ tabIcon }
|
||||||
<span className="mx_TabbedView_tabLabel_text">
|
<span className="mx_TabbedView_tabLabel_text">
|
||||||
{ label }
|
{ label }
|
||||||
</span>
|
</span>
|
||||||
|
@ -118,19 +118,19 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTabPanel(tab: Tab): React.ReactNode {
|
private renderTabPanel(tab: Tab): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||||
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
||||||
{tab.body}
|
{ tab.body }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
|
||||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
|
||||||
|
|
||||||
const tabbedViewClasses = classNames({
|
const tabbedViewClasses = classNames({
|
||||||
'mx_TabbedView': true,
|
'mx_TabbedView': true,
|
||||||
|
@ -141,9 +141,9 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<div className={tabbedViewClasses}>
|
<div className={tabbedViewClasses}>
|
||||||
<div className="mx_TabbedView_tabLabels">
|
<div className="mx_TabbedView_tabLabels">
|
||||||
{labels}
|
{ labels }
|
||||||
</div>
|
</div>
|
||||||
{panel}
|
{ panel }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move into constructor
|
// TODO: [REACT-WARNING] Move into constructor
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.manageReadReceipts) {
|
||||||
this.updateReadReceiptOnUserActivity();
|
this.updateReadReceiptOnUserActivity();
|
||||||
|
@ -290,7 +290,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.timelineSet !== this.props.timelineSet) {
|
if (newProps.timelineSet !== this.props.timelineSet) {
|
||||||
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
||||||
|
@ -1448,7 +1448,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||||
return (
|
return (
|
||||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||||
<div className="mx_RoomView_empty">{this.props.empty}</div>
|
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,14 +37,14 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
// toasts may dismiss themselves in their didMount if they find
|
// toasts may dismiss themselves in their didMount if they find
|
||||||
// they're already irrelevant by the time they're mounted, and
|
// they're already irrelevant by the time they're mounted, and
|
||||||
// our own componentDidMount is too late.
|
// our own componentDidMount is too late.
|
||||||
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
ToastStore.sharedInstance().on('update', this.onToastStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
|
ToastStore.sharedInstance().removeListener('update', this.onToastStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onToastStoreUpdate = () => {
|
private onToastStoreUpdate = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
toasts: ToastStore.sharedInstance().getToasts(),
|
toasts: ToastStore.sharedInstance().getToasts(),
|
||||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||||
|
@ -75,10 +75,10 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
});
|
});
|
||||||
toast = (<div className={toastClasses}>
|
toast = (<div className={toastClasses}>
|
||||||
<div className="mx_Toast_title">
|
<div className="mx_Toast_title">
|
||||||
<h2>{title}</h2>
|
<h2>{ title }</h2>
|
||||||
<span>{countIndicator}</span>
|
<span>{ countIndicator }</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
<div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
containerClasses = classNames("mx_ToastContainer", {
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
|
@ -88,7 +88,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
return toast
|
return toast
|
||||||
? (
|
? (
|
||||||
<div className={containerClasses} role="alert">
|
<div className={containerClasses} role="alert">
|
||||||
{toast}
|
{ toast }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
|
@ -104,7 +104,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
||||||
const uploadSize = filesize(this.state.currentUpload.total);
|
const uploadSize = filesize(this.state.currentUpload.total);
|
||||||
return (
|
return (
|
||||||
<div className="mx_UploadBar">
|
<div className="mx_UploadBar">
|
||||||
<div className="mx_UploadBar_filename">{uploadText} ({uploadSize})</div>
|
<div className="mx_UploadBar_filename">{ uploadText } ({ uploadSize })</div>
|
||||||
<AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
|
<AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
|
||||||
<ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
|
<ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -342,20 +342,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
topSection = (
|
topSection = (
|
||||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||||
{_t("Got an account? <a>Sign in</a>", {}, {
|
{ _t("Got an account? <a>Sign in</a>", {}, {
|
||||||
a: sub => (
|
a: sub => (
|
||||||
<AccessibleButton kind="link" onClick={this.onSignInClick}>
|
<AccessibleButton kind="link" onClick={this.onSignInClick}>
|
||||||
{sub}
|
{ sub }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
),
|
),
|
||||||
})}
|
}) }
|
||||||
{_t("New here? <a>Create an account</a>", {}, {
|
{ _t("New here? <a>Create an account</a>", {}, {
|
||||||
a: sub => (
|
a: sub => (
|
||||||
<AccessibleButton kind="link" onClick={this.onRegisterClick}>
|
<AccessibleButton kind="link" onClick={this.onRegisterClick}>
|
||||||
{sub}
|
{ sub }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
),
|
),
|
||||||
})}
|
}) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hostSignupConfig) {
|
} else if (hostSignupConfig) {
|
||||||
|
@ -394,17 +394,17 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
let primaryHeader = (
|
let primaryHeader = (
|
||||||
<div className="mx_UserMenu_contextMenu_name">
|
<div className="mx_UserMenu_contextMenu_name">
|
||||||
<span className="mx_UserMenu_contextMenu_displayName">
|
<span className="mx_UserMenu_contextMenu_displayName">
|
||||||
{OwnProfileStore.instance.displayName}
|
{ OwnProfileStore.instance.displayName }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_UserMenu_contextMenu_userId">
|
<span className="mx_UserMenu_contextMenu_userId">
|
||||||
{MatrixClientPeg.get().getUserId()}
|
{ MatrixClientPeg.get().getUserId() }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
let primaryOptionList = (
|
let primaryOptionList = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{homeButton}
|
{ homeButton }
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconBell"
|
iconClassName="mx_UserMenu_iconBell"
|
||||||
label={_t("Notification settings")}
|
label={_t("Notification settings")}
|
||||||
|
@ -420,11 +420,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
label={_t("All settings")}
|
label={_t("All settings")}
|
||||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||||
/>
|
/>
|
||||||
{/* <IconizedContextMenuOption
|
{ /* <IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconArchive"
|
iconClassName="mx_UserMenu_iconArchive"
|
||||||
label={_t("Archived rooms")}
|
label={_t("Archived rooms")}
|
||||||
onClick={this.onShowArchived}
|
onClick={this.onShowArchived}
|
||||||
/> */}
|
/> */ }
|
||||||
{ feedbackButton }
|
{ feedbackButton }
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
<IconizedContextMenuOptionList red>
|
<IconizedContextMenuOptionList red>
|
||||||
|
@ -443,7 +443,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
primaryHeader = (
|
primaryHeader = (
|
||||||
<div className="mx_UserMenu_contextMenu_name">
|
<div className="mx_UserMenu_contextMenu_name">
|
||||||
<span className="mx_UserMenu_contextMenu_displayName">
|
<span className="mx_UserMenu_contextMenu_displayName">
|
||||||
{prototypeCommunityName}
|
{ prototypeCommunityName }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -470,13 +470,13 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
primaryOptionList = (
|
primaryOptionList = (
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{settingsOption}
|
{ settingsOption }
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconMembers"
|
iconClassName="mx_UserMenu_iconMembers"
|
||||||
label={_t("Members")}
|
label={_t("Members")}
|
||||||
onClick={this.onCommunityMembersClick}
|
onClick={this.onCommunityMembersClick}
|
||||||
/>
|
/>
|
||||||
{inviteOption}
|
{ inviteOption }
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
secondarySection = (
|
secondarySection = (
|
||||||
|
@ -485,10 +485,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
<div className="mx_UserMenu_contextMenu_header">
|
<div className="mx_UserMenu_contextMenu_header">
|
||||||
<div className="mx_UserMenu_contextMenu_name">
|
<div className="mx_UserMenu_contextMenu_name">
|
||||||
<span className="mx_UserMenu_contextMenu_displayName">
|
<span className="mx_UserMenu_contextMenu_displayName">
|
||||||
{OwnProfileStore.instance.displayName}
|
{ OwnProfileStore.instance.displayName }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_UserMenu_contextMenu_userId">
|
<span className="mx_UserMenu_contextMenu_userId">
|
||||||
{MatrixClientPeg.get().getUserId()}
|
{ MatrixClientPeg.get().getUserId() }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -540,7 +540,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
className={classes}
|
className={classes}
|
||||||
>
|
>
|
||||||
<div className="mx_UserMenu_contextMenu_header">
|
<div className="mx_UserMenu_contextMenu_header">
|
||||||
{primaryHeader}
|
{ primaryHeader }
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_UserMenu_contextMenu_themeButton"
|
className="mx_UserMenu_contextMenu_themeButton"
|
||||||
onClick={this.onSwitchThemeClick}
|
onClick={this.onSwitchThemeClick}
|
||||||
|
@ -553,9 +553,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
</div>
|
</div>
|
||||||
{topSection}
|
{ topSection }
|
||||||
{primaryOptionList}
|
{ primaryOptionList }
|
||||||
{secondarySection}
|
{ secondarySection }
|
||||||
</IconizedContextMenu>;
|
</IconizedContextMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -570,27 +570,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let isPrototype = false;
|
let isPrototype = false;
|
||||||
let menuName = _t("User menu");
|
let menuName = _t("User menu");
|
||||||
let name = <span className="mx_UserMenu_userName">{displayName}</span>;
|
let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
|
||||||
let buttons = (
|
let buttons = (
|
||||||
<span className="mx_UserMenu_headerButtons">
|
<span className="mx_UserMenu_headerButtons">
|
||||||
{/* masked image in CSS */}
|
{ /* masked image in CSS */ }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
let dnd;
|
let dnd;
|
||||||
if (this.state.selectedSpace) {
|
if (this.state.selectedSpace) {
|
||||||
name = (
|
name = (
|
||||||
<div className="mx_UserMenu_doubleName">
|
<div className="mx_UserMenu_doubleName">
|
||||||
<span className="mx_UserMenu_userName">{displayName}</span>
|
<span className="mx_UserMenu_userName">{ displayName }</span>
|
||||||
<RoomName room={this.state.selectedSpace}>
|
<RoomName room={this.state.selectedSpace}>
|
||||||
{(roomName) => <span className="mx_UserMenu_subUserName">{roomName}</span>}
|
{ (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
|
||||||
</RoomName>
|
</RoomName>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (prototypeCommunityName) {
|
} else if (prototypeCommunityName) {
|
||||||
name = (
|
name = (
|
||||||
<div className="mx_UserMenu_doubleName">
|
<div className="mx_UserMenu_doubleName">
|
||||||
<span className="mx_UserMenu_userName">{prototypeCommunityName}</span>
|
<span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
|
||||||
<span className="mx_UserMenu_subUserName">{displayName}</span>
|
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
menuName = _t("Community and user menu");
|
menuName = _t("Community and user menu");
|
||||||
|
@ -598,8 +598,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||||
name = (
|
name = (
|
||||||
<div className="mx_UserMenu_doubleName">
|
<div className="mx_UserMenu_doubleName">
|
||||||
<span className="mx_UserMenu_userName">{_t("Home")}</span>
|
<span className="mx_UserMenu_userName">{ _t("Home") }</span>
|
||||||
<span className="mx_UserMenu_subUserName">{displayName}</span>
|
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
isPrototype = true;
|
isPrototype = true;
|
||||||
|
@ -647,20 +647,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
className="mx_UserMenu_userAvatar"
|
className="mx_UserMenu_userAvatar"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{name}
|
{ name }
|
||||||
{this.state.pendingRoomJoin.size > 0 && (
|
{ this.state.pendingRoomJoin.size > 0 && (
|
||||||
<InlineSpinner>
|
<InlineSpinner>
|
||||||
<TooltipButton helpText={_t(
|
<TooltipButton helpText={_t(
|
||||||
"Currently joining %(count)s rooms",
|
"Currently joining %(count)s rooms",
|
||||||
{ count: this.state.pendingRoomJoin.size },
|
{ count: this.state.pendingRoomJoin.size },
|
||||||
)} />
|
)} />
|
||||||
</InlineSpinner>
|
</InlineSpinner>
|
||||||
)}
|
) }
|
||||||
{dnd}
|
{ dnd }
|
||||||
{buttons}
|
{ buttons }
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
{this.renderContextMenu()}
|
{ this.renderContextMenu() }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,23 +63,23 @@ export default class ViewSource extends React.Component {
|
||||||
<>
|
<>
|
||||||
<details open className="mx_ViewSource_details">
|
<details open className="mx_ViewSource_details">
|
||||||
<summary>
|
<summary>
|
||||||
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
|
<span className="mx_ViewSource_heading">{ _t("Decrypted event source") }</span>
|
||||||
</summary>
|
</summary>
|
||||||
<SyntaxHighlight className="json">{JSON.stringify(decryptedEventSource, null, 2)}</SyntaxHighlight>
|
<SyntaxHighlight className="json">{ JSON.stringify(decryptedEventSource, null, 2) }</SyntaxHighlight>
|
||||||
</details>
|
</details>
|
||||||
<details className="mx_ViewSource_details">
|
<details className="mx_ViewSource_details">
|
||||||
<summary>
|
<summary>
|
||||||
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
|
<span className="mx_ViewSource_heading">{ _t("Original event source") }</span>
|
||||||
</summary>
|
</summary>
|
||||||
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
|
<SyntaxHighlight className="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
|
||||||
</details>
|
</details>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
|
<div className="mx_ViewSource_heading">{ _t("Original event source") }</div>
|
||||||
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
|
<SyntaxHighlight className="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ export default class ViewSource extends React.Component {
|
||||||
if (isStateEvent) {
|
if (isStateEvent) {
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Consumer>
|
<MatrixClientContext.Consumer>
|
||||||
{(cli) => (
|
{ (cli) => (
|
||||||
<SendCustomEvent
|
<SendCustomEvent
|
||||||
room={cli.getRoom(roomId)}
|
room={cli.getRoom(roomId)}
|
||||||
forceStateEvent={true}
|
forceStateEvent={true}
|
||||||
|
@ -121,7 +121,7 @@ export default class ViewSource extends React.Component {
|
||||||
stateKey: mxEvent.getStateKey(),
|
stateKey: mxEvent.getStateKey(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
) }
|
||||||
</MatrixClientContext.Consumer>
|
</MatrixClientContext.Consumer>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,7 +142,7 @@ export default class ViewSource extends React.Component {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Consumer>
|
<MatrixClientContext.Consumer>
|
||||||
{(cli) => (
|
{ (cli) => (
|
||||||
<SendCustomEvent
|
<SendCustomEvent
|
||||||
room={cli.getRoom(roomId)}
|
room={cli.getRoom(roomId)}
|
||||||
forceStateEvent={false}
|
forceStateEvent={false}
|
||||||
|
@ -153,7 +153,7 @@ export default class ViewSource extends React.Component {
|
||||||
evContent: JSON.stringify(newContent, null, "\t"),
|
evContent: JSON.stringify(newContent, null, "\t"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
) }
|
||||||
</MatrixClientContext.Consumer>
|
</MatrixClientContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -176,16 +176,16 @@ export default class ViewSource extends React.Component {
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
|
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
|
||||||
<div>
|
<div>
|
||||||
<div>Room ID: {roomId}</div>
|
<div>Room ID: { roomId }</div>
|
||||||
<div>Event ID: {eventId}</div>
|
<div>Event ID: { eventId }</div>
|
||||||
<div className="mx_ViewSource_separator" />
|
<div className="mx_ViewSource_separator" />
|
||||||
{isEditing ? this.editSourceContent() : this.viewSourceContent()}
|
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
|
||||||
</div>
|
</div>
|
||||||
{!isEditing && canEdit && (
|
{ !isEditing && canEdit && (
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={() => this.onEdit()}>{_t("Edit")}</button>
|
<button onClick={() => this.onEdit()}>{ _t("Edit") }</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,8 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
<CompleteSecurityBody>
|
<CompleteSecurityBody>
|
||||||
<h2 className="mx_CompleteSecurity_header">
|
<h2 className="mx_CompleteSecurity_header">
|
||||||
{icon}
|
{ icon }
|
||||||
{title}
|
{ title }
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mx_CompleteSecurity_body">
|
<div className="mx_CompleteSecurity_body">
|
||||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||||
|
|
|
@ -101,7 +101,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
@ -239,14 +239,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
serverDeadSection = (
|
serverDeadSection = (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{this.state.serverDeadError}
|
{ this.state.serverDeadError }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{errorText}
|
{ errorText }
|
||||||
{serverDeadSection}
|
{ serverDeadSection }
|
||||||
<ServerPicker
|
<ServerPicker
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
onServerConfigChange={this.props.onServerConfigChange}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
|
@ -289,10 +289,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span>{_t(
|
<span>{ _t(
|
||||||
'A verification email will be sent to your inbox to confirm ' +
|
'A verification email will be sent to your inbox to confirm ' +
|
||||||
'setting your new password.',
|
'setting your new password.',
|
||||||
)}</span>
|
) }</span>
|
||||||
<input
|
<input
|
||||||
className="mx_Login_submit"
|
className="mx_Login_submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -300,7 +300,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||||
{_t('Sign in instead')}
|
{ _t('Sign in instead') }
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -312,8 +312,8 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
renderEmailSent() {
|
renderEmailSent() {
|
||||||
return <div>
|
return <div>
|
||||||
{_t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
||||||
"link it contains, click below.", { emailAddress: this.state.email })}
|
"link it contains, click below.", { emailAddress: this.state.email }) }
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
value={_t('I have verified my email address')} />
|
value={_t('I have verified my email address')} />
|
||||||
|
@ -322,12 +322,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
renderDone() {
|
renderDone() {
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t("Your password has been reset.")}</p>
|
<p>{ _t("Your password has been reset.") }</p>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"You have been logged out of all sessions and will no longer receive " +
|
"You have been logged out of all sessions and will no longer receive " +
|
||||||
"push notifications. To re-enable notifications, sign in again on each " +
|
"push notifications. To re-enable notifications, sign in again on each " +
|
||||||
"device.",
|
"device.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
value={_t('Return to login screen')} />
|
value={_t('Return to login screen')} />
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -358,7 +358,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<AuthBody>
|
<AuthBody>
|
||||||
<h2> { _t('Set a new password') } </h2>
|
<h2> { _t('Set a new password') } </h2>
|
||||||
{resetPasswordJsx}
|
{ resetPasswordJsx }
|
||||||
</AuthBody>
|
</AuthBody>
|
||||||
</AuthPage>
|
</AuthPage>
|
||||||
);
|
);
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this.initLoginLogic(this.props.serverConfig);
|
this.initLoginLogic(this.props.serverConfig);
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
@ -239,8 +239,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
);
|
);
|
||||||
errorText = (
|
errorText = (
|
||||||
<div>
|
<div>
|
||||||
<div>{errorTop}</div>
|
<div>{ errorTop }</div>
|
||||||
<div className="mx_Login_smallError">{errorDetail}</div>
|
<div className="mx_Login_smallError">{ errorDetail }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||||
|
@ -251,10 +251,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||||
<div className="mx_Login_smallError">
|
<div className="mx_Login_smallError">
|
||||||
{_t(
|
{ _t(
|
||||||
'Please note you are logging into the %(hs)s server, not matrix.org.',
|
'Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||||
{ hs: this.props.serverConfig.hsName },
|
{ hs: this.props.serverConfig.hsName },
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -565,7 +565,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
});
|
});
|
||||||
serverDeadSection = (
|
serverDeadSection = (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{this.state.serverDeadError}
|
{ this.state.serverDeadError }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -578,15 +578,15 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
{ this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
|
{ this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
|
{ this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
|
||||||
{_t("If you've joined lots of rooms, this might take a while")}
|
{ _t("If you've joined lots of rooms, this might take a while") }
|
||||||
</div> }
|
</div> }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (SettingsStore.getValue(UIFeature.Registration)) {
|
} else if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||||
footer = (
|
footer = (
|
||||||
<span className="mx_AuthBody_changeFlow">
|
<span className="mx_AuthBody_changeFlow">
|
||||||
{_t("New? <a>Create account</a>", {}, {
|
{ _t("New? <a>Create account</a>", {}, {
|
||||||
a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
|
a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
|
||||||
})}
|
}) }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -596,8 +596,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
|
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
|
||||||
<AuthBody>
|
<AuthBody>
|
||||||
<h2>
|
<h2>
|
||||||
{_t('Sign in')}
|
{ _t('Sign in') }
|
||||||
{loader}
|
{ loader }
|
||||||
</h2>
|
</h2>
|
||||||
{ errorTextSection }
|
{ errorTextSection }
|
||||||
{ serverDeadSection }
|
{ serverDeadSection }
|
||||||
|
|
|
@ -141,7 +141,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
@ -290,8 +290,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
msg = <div>
|
msg = <div>
|
||||||
<p>{errorTop}</p>
|
<p>{ errorTop }</p>
|
||||||
<p>{errorDetail}</p>
|
<p>{ errorDetail }</p>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
let msisdnAvailable = false;
|
let msisdnAvailable = false;
|
||||||
|
@ -482,13 +482,13 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
/>
|
/>
|
||||||
<h3 className="mx_AuthBody_centered">
|
<h3 className="mx_AuthBody_centered">
|
||||||
{_t(
|
{ _t(
|
||||||
"%(ssoButtons)s Or %(usernamePassword)s",
|
"%(ssoButtons)s Or %(usernamePassword)s",
|
||||||
{
|
{
|
||||||
ssoButtons: "",
|
ssoButtons: "",
|
||||||
usernamePassword: "",
|
usernamePassword: "",
|
||||||
},
|
},
|
||||||
).trim()}
|
).trim() }
|
||||||
</h3>
|
</h3>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
@ -526,15 +526,15 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
serverDeadSection = (
|
serverDeadSection = (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{this.state.serverDeadError}
|
{ this.state.serverDeadError }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signIn = <span className="mx_AuthBody_changeFlow">
|
const signIn = <span className="mx_AuthBody_changeFlow">
|
||||||
{_t("Already have an account? <a>Sign in here</a>", {}, {
|
{ _t("Already have an account? <a>Sign in here</a>", {}, {
|
||||||
a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
|
a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
|
||||||
})}
|
}) }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
// Only show the 'go back' button if you're not looking at the form
|
// Only show the 'go back' button if you're not looking at the form
|
||||||
|
@ -550,43 +550,43 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
let regDoneText;
|
let regDoneText;
|
||||||
if (this.state.differentLoggedInUserId) {
|
if (this.state.differentLoggedInUserId) {
|
||||||
regDoneText = <div>
|
regDoneText = <div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Your new account (%(newAccountId)s) is registered, but you're already " +
|
"Your new account (%(newAccountId)s) is registered, but you're already " +
|
||||||
"logged into a different account (%(loggedInUserId)s).", {
|
"logged into a different account (%(loggedInUserId)s).", {
|
||||||
newAccountId: this.state.registeredUsername,
|
newAccountId: this.state.registeredUsername,
|
||||||
loggedInUserId: this.state.differentLoggedInUserId,
|
loggedInUserId: this.state.differentLoggedInUserId,
|
||||||
},
|
},
|
||||||
)}</p>
|
) }</p>
|
||||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
|
<p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
|
||||||
const sessionLoaded = await this.onLoginClickWithCheck(event);
|
const sessionLoaded = await this.onLoginClickWithCheck(event);
|
||||||
if (sessionLoaded) {
|
if (sessionLoaded) {
|
||||||
dis.dispatch({ action: "view_welcome_page" });
|
dis.dispatch({ action: "view_welcome_page" });
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{_t("Continue with previous account")}
|
{ _t("Continue with previous account") }
|
||||||
</AccessibleButton></p>
|
</AccessibleButton></p>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.formVals.password) {
|
} else if (this.state.formVals.password) {
|
||||||
// We're the client that started the registration
|
// We're the client that started the registration
|
||||||
regDoneText = <h3>{_t(
|
regDoneText = <h3>{ _t(
|
||||||
"<a>Log in</a> to your new account.", {},
|
"<a>Log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{ sub }</a>,
|
||||||
},
|
},
|
||||||
)}</h3>;
|
) }</h3>;
|
||||||
} else {
|
} else {
|
||||||
// We're not the original client: the user probably got to us by clicking the
|
// We're not the original client: the user probably got to us by clicking the
|
||||||
// email validation link. We can't offer a 'go straight to your account' link
|
// email validation link. We can't offer a 'go straight to your account' link
|
||||||
// as we don't have the original creds.
|
// as we don't have the original creds.
|
||||||
regDoneText = <h3>{_t(
|
regDoneText = <h3>{ _t(
|
||||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{ sub }</a>,
|
||||||
},
|
},
|
||||||
)}</h3>;
|
) }</h3>;
|
||||||
}
|
}
|
||||||
body = <div>
|
body = <div>
|
||||||
<h2>{_t("Registration Successful")}</h2>
|
<h2>{ _t("Registration Successful") }</h2>
|
||||||
{ regDoneText }
|
{ regDoneText }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -152,7 +152,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
let useRecoveryKeyButton;
|
let useRecoveryKeyButton;
|
||||||
if (recoveryKeyPrompt) {
|
if (recoveryKeyPrompt) {
|
||||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
||||||
{recoveryKeyPrompt}
|
{ recoveryKeyPrompt }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,15 +165,15 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||||
)}</p>
|
) }</p>
|
||||||
|
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
{verifyButton}
|
{ verifyButton }
|
||||||
{useRecoveryKeyButton}
|
{ useRecoveryKeyButton }
|
||||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||||
{_t("Skip")}
|
{ _t("Skip") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -181,25 +181,25 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
} else if (phase === Phase.Done) {
|
} else if (phase === Phase.Done) {
|
||||||
let message;
|
let message;
|
||||||
if (this.state.backupInfo) {
|
if (this.state.backupInfo) {
|
||||||
message = <p>{_t(
|
message = <p>{ _t(
|
||||||
"Your new session is now verified. It has access to your " +
|
"Your new session is now verified. It has access to your " +
|
||||||
"encrypted messages, and other users will see it as trusted.",
|
"encrypted messages, and other users will see it as trusted.",
|
||||||
)}</p>;
|
) }</p>;
|
||||||
} else {
|
} else {
|
||||||
message = <p>{_t(
|
message = <p>{ _t(
|
||||||
"Your new session is now verified. Other users will see it as trusted.",
|
"Your new session is now verified. Other users will see it as trusted.",
|
||||||
)}</p>;
|
) }</p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified" />
|
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified" />
|
||||||
{message}
|
{ message }
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
onClick={this.onDoneClick}
|
onClick={this.onDoneClick}
|
||||||
>
|
>
|
||||||
{_t("Done")}
|
{ _t("Done") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -207,23 +207,23 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||||
} else if (phase === Phase.ConfirmSkip) {
|
} else if (phase === Phase.ConfirmSkip) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"Without verifying, you won’t have access to all your messages " +
|
"Without verifying, you won’t have access to all your messages " +
|
||||||
"and may appear as untrusted to others.",
|
"and may appear as untrusted to others.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="warning"
|
className="warning"
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
onClick={this.onSkipConfirmClick}
|
onClick={this.onSkipConfirmClick}
|
||||||
>
|
>
|
||||||
{_t("Skip")}
|
{ _t("Skip") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="danger"
|
kind="danger"
|
||||||
onClick={this.onSkipBackClick}
|
onClick={this.onSkipBackClick}
|
||||||
>
|
>
|
||||||
{_t("Go Back")}
|
{ _t("Go Back") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -219,7 +219,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
|
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
error = <span className='mx_Login_error'>{this.state.errorText}</span>;
|
error = <span className='mx_Login_error'>{ this.state.errorText }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!introText) {
|
if (!introText) {
|
||||||
|
@ -228,8 +228,8 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onPasswordLogin}>
|
<form onSubmit={this.onPasswordLogin}>
|
||||||
<p>{introText}</p>
|
<p>{ introText }</p>
|
||||||
{error}
|
{ error }
|
||||||
<Field
|
<Field
|
||||||
type="password"
|
type="password"
|
||||||
label={_t("Password")}
|
label={_t("Password")}
|
||||||
|
@ -243,10 +243,10 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy}
|
||||||
>
|
>
|
||||||
{_t("Sign In")}
|
{ _t("Sign In") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this.onForgotPassword} kind="link">
|
<AccessibleButton onClick={this.onForgotPassword} kind="link">
|
||||||
{_t("Forgotten your password?")}
|
{ _t("Forgotten your password?") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -262,7 +262,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{introText}</p>
|
<p>{ introText }</p>
|
||||||
<SSOButtons
|
<SSOButtons
|
||||||
matrixClient={MatrixClientPeg.get()}
|
matrixClient={MatrixClientPeg.get()}
|
||||||
flow={flow}
|
flow={flow}
|
||||||
|
@ -277,10 +277,10 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
// Default: assume unsupported/error
|
// Default: assume unsupported/error
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{ _t(
|
||||||
"You cannot sign in to your account. Please contact your " +
|
"You cannot sign in to your account. Please contact your " +
|
||||||
"homeserver admin for more information.",
|
"homeserver admin for more information.",
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -291,25 +291,25 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<AuthBody>
|
<AuthBody>
|
||||||
<h2>
|
<h2>
|
||||||
{_t("You're signed out")}
|
{ _t("You're signed out") }
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<h3>{_t("Sign in")}</h3>
|
<h3>{ _t("Sign in") }</h3>
|
||||||
<div>
|
<div>
|
||||||
{this.renderSignInSection()}
|
{ this.renderSignInSection() }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>{_t("Clear personal data")}</h3>
|
<h3>{ _t("Clear personal data") }</h3>
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{ _t(
|
||||||
"Warning: Your personal data (including encryption keys) is still stored " +
|
"Warning: Your personal data (including encryption keys) is still stored " +
|
||||||
"in this session. Clear it if you're finished using this session, or want to sign " +
|
"in this session. Clear it if you're finished using this session, or want to sign " +
|
||||||
"in to another account.",
|
"in to another account.",
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton onClick={this.onClearAll} kind="danger">
|
<AccessibleButton onClick={this.onClearAll} kind="danger">
|
||||||
{_t("Clear all data")}
|
{ _t("Clear all data") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</AuthBody>
|
</AuthBody>
|
||||||
|
|
|
@ -101,11 +101,11 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
<div className='mx_AudioPlayer_mediaInfo'>
|
<div className='mx_AudioPlayer_mediaInfo'>
|
||||||
<span className='mx_AudioPlayer_mediaName'>
|
<span className='mx_AudioPlayer_mediaName'>
|
||||||
{this.props.mediaName || _t("Unnamed audio")}
|
{ this.props.mediaName || _t("Unnamed audio") }
|
||||||
</span>
|
</span>
|
||||||
<div className='mx_AudioPlayer_byline'>
|
<div className='mx_AudioPlayer_byline'>
|
||||||
<DurationClock playback={this.props.playback} />
|
<DurationClock playback={this.props.playback} />
|
||||||
{/* easiest way to introduce a gap between the components */}
|
{ /* easiest way to introduce a gap between the components */ }
|
||||||
{ this.renderFileSize() }
|
{ this.renderFileSize() }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,6 +43,6 @@ export default class Clock extends React.Component<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
|
const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
|
||||||
const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
|
const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
|
||||||
return <span className='mx_Clock'>{minutes}:{seconds}</span>;
|
return <span className='mx_Clock'>{ minutes }:{ seconds }</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return <div className='mx_Waveform'>
|
return <div className='mx_Waveform'>
|
||||||
{this.props.relHeights.map((h, i) => {
|
{ this.props.relHeights.map((h, i) => {
|
||||||
const progress = this.props.progress;
|
const progress = this.props.progress;
|
||||||
const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0;
|
const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0;
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -57,7 +57,7 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||||
return <span key={i} style={{
|
return <span key={i} style={{
|
||||||
"--barHeight": h,
|
"--barHeight": h,
|
||||||
} as WaveformCSSProperties} className={classes} />;
|
} as WaveformCSSProperties} className={classes} />;
|
||||||
})}
|
}) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthBody")
|
@replaceableComponent("views.auth.AuthBody")
|
||||||
export default class AuthBody extends React.PureComponent {
|
export default class AuthBody extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_AuthBody">
|
return <div className="mx_AuthBody">
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
|
@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthFooter")
|
@replaceableComponent("views.auth.AuthFooter")
|
||||||
export default class AuthFooter extends React.Component {
|
export default class AuthFooter extends React.Component {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthFooter">
|
<div className="mx_AuthFooter">
|
||||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
|
@ -16,20 +16,17 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AuthHeaderLogo from "./AuthHeaderLogo";
|
||||||
|
import LanguageSelector from "./LanguageSelector";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
disableLanguageSelector?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthHeader")
|
@replaceableComponent("views.auth.AuthHeader")
|
||||||
export default class AuthHeader extends React.Component {
|
export default class AuthHeader extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public render(): React.ReactNode {
|
||||||
disableLanguageSelector: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
|
|
||||||
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthHeader">
|
<div className="mx_AuthHeader">
|
||||||
<AuthHeaderLogo />
|
<AuthHeaderLogo />
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthHeaderLogo")
|
@replaceableComponent("views.auth.AuthHeaderLogo")
|
||||||
export default class AuthHeaderLogo extends React.PureComponent {
|
export default class AuthHeaderLogo extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_AuthHeaderLogo">
|
return <div className="mx_AuthHeaderLogo">
|
||||||
Matrix
|
Matrix
|
||||||
</div>;
|
</div>;
|
|
@ -17,18 +17,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AuthFooter from "./AuthFooter";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthPage")
|
@replaceableComponent("views.auth.AuthPage")
|
||||||
export default class AuthPage extends React.PureComponent {
|
export default class AuthPage extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
const AuthFooter = sdk.getComponent('auth.AuthFooter');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthPage">
|
<div className="mx_AuthPage">
|
||||||
<div className="mx_AuthPage_modal">
|
<div className="mx_AuthPage_modal">
|
||||||
{this.props.children}
|
{ this.props.children }
|
||||||
</div>
|
</div>
|
||||||
<AuthFooter />
|
<AuthFooter />
|
||||||
</div>
|
</div>
|
|
@ -15,66 +15,74 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const DIV_ID = 'mx_recaptcha';
|
const DIV_ID = 'mx_recaptcha';
|
||||||
|
|
||||||
|
interface ICaptchaFormProps {
|
||||||
|
sitePublicKey: string;
|
||||||
|
onCaptchaResponse: (response: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICaptchaFormState {
|
||||||
|
errorText?: string;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pure UI component which displays a captcha form.
|
* A pure UI component which displays a captcha form.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.auth.CaptchaForm")
|
@replaceableComponent("views.auth.CaptchaForm")
|
||||||
export default class CaptchaForm extends React.Component {
|
export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
|
||||||
static propTypes = {
|
|
||||||
sitePublicKey: PropTypes.string,
|
|
||||||
|
|
||||||
// called with the captcha response
|
|
||||||
onCaptchaResponse: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onCaptchaResponse: () => {},
|
onCaptchaResponse: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
private captchaWidgetId?: string;
|
||||||
|
private recaptchaContainer = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
constructor(props: ICaptchaFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
errorText: null,
|
errorText: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._captchaWidgetId = null;
|
|
||||||
|
|
||||||
this._recaptchaContainer = createRef();
|
|
||||||
|
|
||||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
|
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
||||||
// so we do this instead.
|
// so we do this instead.
|
||||||
if (global.grecaptcha) {
|
if (this.isRecaptchaReady()) {
|
||||||
// already loaded
|
// already loaded
|
||||||
this._onCaptchaLoaded();
|
this.onCaptchaLoaded();
|
||||||
} else {
|
} else {
|
||||||
console.log("Loading recaptcha script...");
|
console.log("Loading recaptcha script...");
|
||||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
|
||||||
const scriptTag = document.createElement('script');
|
const scriptTag = document.createElement('script');
|
||||||
scriptTag.setAttribute(
|
scriptTag.setAttribute(
|
||||||
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`,
|
||||||
);
|
);
|
||||||
this._recaptchaContainer.current.appendChild(scriptTag);
|
this.recaptchaContainer.current.appendChild(scriptTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._resetRecaptcha();
|
this.resetRecaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderRecaptcha(divId) {
|
// Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba
|
||||||
if (!global.grecaptcha) {
|
private isRecaptchaReady(): boolean {
|
||||||
|
return typeof window !== "undefined" &&
|
||||||
|
typeof global.grecaptcha !== "undefined" &&
|
||||||
|
typeof global.grecaptcha.render === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRecaptcha(divId: string) {
|
||||||
|
if (!this.isRecaptchaReady()) {
|
||||||
console.error("grecaptcha not loaded!");
|
console.error("grecaptcha not loaded!");
|
||||||
throw new Error("Recaptcha did not load successfully");
|
throw new Error("Recaptcha did not load successfully");
|
||||||
}
|
}
|
||||||
|
@ -84,26 +92,26 @@ export default class CaptchaForm extends React.Component {
|
||||||
console.error("No public key for recaptcha!");
|
console.error("No public key for recaptcha!");
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"This server has not supplied enough information for Recaptcha "
|
"This server has not supplied enough information for Recaptcha "
|
||||||
+ "authentication");
|
+ "authentication");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info("Rendering to %s", divId);
|
console.info("Rendering to %s", divId);
|
||||||
this._captchaWidgetId = global.grecaptcha.render(divId, {
|
this.captchaWidgetId = global.grecaptcha.render(divId, {
|
||||||
sitekey: publicKey,
|
sitekey: publicKey,
|
||||||
callback: this.props.onCaptchaResponse,
|
callback: this.props.onCaptchaResponse,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetRecaptcha() {
|
private resetRecaptcha() {
|
||||||
if (this._captchaWidgetId !== null) {
|
if (this.captchaWidgetId !== null) {
|
||||||
global.grecaptcha.reset(this._captchaWidgetId);
|
global.grecaptcha.reset(this.captchaWidgetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCaptchaLoaded() {
|
private onCaptchaLoaded() {
|
||||||
console.log("Loaded recaptcha script.");
|
console.log("Loaded recaptcha script.");
|
||||||
try {
|
try {
|
||||||
this._renderRecaptcha(DIV_ID);
|
this.renderRecaptcha(DIV_ID);
|
||||||
// clear error if re-rendered
|
// clear error if re-rendered
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
|
@ -128,10 +136,10 @@ export default class CaptchaForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={this._recaptchaContainer}>
|
<div ref={this.recaptchaContainer}>
|
||||||
<p>{_t(
|
<p>{ _t(
|
||||||
"This homeserver would like to make sure you are not a robot.",
|
"This homeserver would like to make sure you are not a robot.",
|
||||||
)}</p>
|
) }</p>
|
||||||
<div id={DIV_ID} />
|
<div id={DIV_ID} />
|
||||||
{ error }
|
{ error }
|
||||||
</div>
|
</div>
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.CompleteSecurityBody")
|
@replaceableComponent("views.auth.CompleteSecurityBody")
|
||||||
export default class CompleteSecurityBody extends React.PureComponent {
|
export default class CompleteSecurityBody extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_CompleteSecurityBody">
|
return <div className="mx_CompleteSecurityBody">
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
|
@ -15,21 +15,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
|
||||||
|
|
||||||
import { COUNTRIES, getEmojiFlag } from '../../../phonenumber';
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Dropdown from "../elements/Dropdown";
|
||||||
|
|
||||||
const COUNTRIES_BY_ISO2 = {};
|
const COUNTRIES_BY_ISO2 = {};
|
||||||
for (const c of COUNTRIES) {
|
for (const c of COUNTRIES) {
|
||||||
COUNTRIES_BY_ISO2[c.iso2] = c;
|
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
function countryMatchesSearchQuery(query, country) {
|
function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean {
|
||||||
// Remove '+' if present (when searching for a prefix)
|
// Remove '+' if present (when searching for a prefix)
|
||||||
if (query[0] === '+') {
|
if (query[0] === '+') {
|
||||||
query = query.slice(1);
|
query = query.slice(1);
|
||||||
|
@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.CountryDropdown")
|
interface IProps {
|
||||||
export default class CountryDropdown extends React.Component {
|
value?: string;
|
||||||
constructor(props) {
|
onOptionChange: (country: PhoneNumberCountryDefinition) => void;
|
||||||
super(props);
|
isSmall: boolean; // if isSmall, show +44 in the selected value
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
showPrefix: boolean;
|
||||||
this._onOptionChange = this._onOptionChange.bind(this);
|
className?: string;
|
||||||
this._getShortOption = this._getShortOption.bind(this);
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
let defaultCountry = COUNTRIES[0];
|
interface IState {
|
||||||
|
searchQuery: string;
|
||||||
|
defaultCountry: PhoneNumberCountryDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.CountryDropdown")
|
||||||
|
export default class CountryDropdown extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
|
||||||
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
||||||
if (defaultCountryCode) {
|
if (defaultCountryCode) {
|
||||||
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
||||||
|
@ -62,7 +71,7 @@ export default class CountryDropdown extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (!this.props.value) {
|
if (!this.props.value) {
|
||||||
// If no value is given, we start with the default
|
// If no value is given, we start with the default
|
||||||
// country selected, but our parent component
|
// country selected, but our parent component
|
||||||
|
@ -71,21 +80,21 @@ export default class CountryDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchChange(search) {
|
private onSearchChange = (search: string): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery: search,
|
searchQuery: search,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_onOptionChange(iso2) {
|
private onOptionChange = (iso2: string): void => {
|
||||||
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
||||||
}
|
};
|
||||||
|
|
||||||
_flagImgForIso2(iso2) {
|
private flagImgForIso2(iso2: string): React.ReactNode {
|
||||||
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getShortOption(iso2) {
|
private getShortOption = (iso2: string): React.ReactNode => {
|
||||||
if (!this.props.isSmall) {
|
if (!this.props.isSmall) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component {
|
||||||
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
||||||
}
|
}
|
||||||
return <span className="mx_CountryDropdown_shortOption">
|
return <span className="mx_CountryDropdown_shortOption">
|
||||||
{ this._flagImgForIso2(iso2) }
|
{ this.flagImgForIso2(iso2) }
|
||||||
{ countryPrefix }
|
{ countryPrefix }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
let displayedCountries;
|
let displayedCountries;
|
||||||
if (this.state.searchQuery) {
|
if (this.state.searchQuery) {
|
||||||
displayedCountries = COUNTRIES.filter(
|
displayedCountries = COUNTRIES.filter(
|
||||||
|
@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component {
|
||||||
|
|
||||||
const options = displayedCountries.map((country) => {
|
const options = displayedCountries.map((country) => {
|
||||||
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
||||||
{ this._flagImgForIso2(country.iso2) }
|
{ this.flagImgForIso2(country.iso2) }
|
||||||
{ _t(country.name) } (+{ country.prefix })
|
{ _t(country.name) } (+{ country.prefix })
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component {
|
||||||
return <Dropdown
|
return <Dropdown
|
||||||
id="mx_CountryDropdown"
|
id="mx_CountryDropdown"
|
||||||
className={this.props.className + " mx_CountryDropdown"}
|
className={this.props.className + " mx_CountryDropdown"}
|
||||||
onOptionChange={this._onOptionChange}
|
onOptionChange={this.onOptionChange}
|
||||||
onSearchChange={this._onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
menuWidth={298}
|
menuWidth={298}
|
||||||
getShortOption={this._getShortOption}
|
getShortOption={this.getShortOption}
|
||||||
value={value}
|
value={value}
|
||||||
searchEnabled={true}
|
searchEnabled={true}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
@ -149,13 +156,3 @@ export default class CountryDropdown extends React.Component {
|
||||||
</Dropdown>;
|
</Dropdown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CountryDropdown.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
isSmall: PropTypes.bool,
|
|
||||||
// if isSmall, show +44 in the selected value
|
|
||||||
showPrefix: PropTypes.bool,
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
|
@ -417,12 +417,12 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
||||||
if (this.props.showContinue !== false) {
|
if (this.props.showContinue !== false) {
|
||||||
// XXX: button classes
|
// XXX: button classes
|
||||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||||
onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
onClick={this.trySubmit} disabled={!allChecked}>{ _t("Accept") }</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
|
<p>{ _t("Please review and accept the policies of this homeserver:") }</p>
|
||||||
{ checkboxes }
|
{ checkboxes }
|
||||||
{ errorSection }
|
{ errorSection }
|
||||||
{ submitButton }
|
{ submitButton }
|
||||||
|
@ -613,7 +613,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
||||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||||
value={this.state.token}
|
value={this.state.token}
|
||||||
onChange={this.onTokenChange}
|
onChange={this.onTokenChange}
|
||||||
aria-label={ _t("Code")}
|
aria-label={_t("Code")}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<input type="submit" value={_t("Submit")}
|
<input type="submit" value={_t("Submit")}
|
||||||
|
@ -621,7 +621,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
||||||
disabled={!enableSubmit}
|
disabled={!enableSubmit}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
{errorSection}
|
{ errorSection }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -717,21 +717,21 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.props.onCancel}
|
onClick={this.props.onCancel}
|
||||||
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
|
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
|
||||||
>{_t("Cancel")}</AccessibleButton>
|
>{ _t("Cancel") }</AccessibleButton>
|
||||||
);
|
);
|
||||||
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
|
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
|
||||||
continueButton = (
|
continueButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onStartAuthClick}
|
onClick={this.onStartAuthClick}
|
||||||
kind={this.props.continueKind || 'primary'}
|
kind={this.props.continueKind || 'primary'}
|
||||||
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
|
>{ this.props.continueText || _t("Single Sign On") }</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
continueButton = (
|
continueButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onConfirmClick}
|
onClick={this.onConfirmClick}
|
||||||
kind={this.props.continueKind || 'primary'}
|
kind={this.props.continueKind || 'primary'}
|
||||||
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
|
>{ this.props.continueText || _t("Confirm") }</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -753,8 +753,8 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{ errorSection }
|
{ errorSection }
|
||||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||||
{cancelButton}
|
{ cancelButton }
|
||||||
{continueButton}
|
{ continueButton }
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
@ -825,7 +825,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||||
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||||
_t("Start authentication")
|
_t("Start authentication")
|
||||||
}</a>
|
}</a>
|
||||||
{errorSection}
|
{ errorSection }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig";
|
||||||
import { getCurrentLanguage } from "../../../languageHandler";
|
import { getCurrentLanguage } from "../../../languageHandler";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import LanguageDropdown from "../elements/LanguageDropdown";
|
||||||
|
|
||||||
function onChange(newLang) {
|
function onChange(newLang: string): void {
|
||||||
if (getCurrentLanguage() !== newLang) {
|
if (getCurrentLanguage() !== newLang) {
|
||||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||||
PlatformPeg.get().reload();
|
PlatformPeg.get().reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LanguageSelector({ disabled }) {
|
interface IProps {
|
||||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
export default function LanguageSelector({ disabled }: IProps): JSX.Element {
|
||||||
|
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||||
return <LanguageDropdown
|
return <LanguageDropdown
|
||||||
className="mx_AuthBody_language"
|
className="mx_AuthBody_language"
|
||||||
onOptionChange={onChange}
|
onOptionChange={onChange}
|
|
@ -416,7 +416,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={this.onForgotPasswordClick}
|
onClick={this.onForgotPasswordClick}
|
||||||
>
|
>
|
||||||
{_t("Forgot password?")}
|
{ _t("Forgot password?") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,16 +441,16 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
disabled={this.props.disableSubmit}
|
disabled={this.props.disableSubmit}
|
||||||
>
|
>
|
||||||
<option key={LoginField.MatrixId} value={LoginField.MatrixId}>
|
<option key={LoginField.MatrixId} value={LoginField.MatrixId}>
|
||||||
{_t('Username')}
|
{ _t('Username') }
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
key={LoginField.Email}
|
key={LoginField.Email}
|
||||||
value={LoginField.Email}
|
value={LoginField.Email}
|
||||||
>
|
>
|
||||||
{_t('Email address')}
|
{ _t('Email address') }
|
||||||
</option>
|
</option>
|
||||||
<option key={LoginField.Password} value={LoginField.Password}>
|
<option key={LoginField.Password} value={LoginField.Password}>
|
||||||
{_t('Phone')}
|
{ _t('Phone') }
|
||||||
</option>
|
</option>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -460,8 +460,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
{loginType}
|
{ loginType }
|
||||||
{loginField}
|
{ loginField }
|
||||||
<Field
|
<Field
|
||||||
className={pwFieldClass}
|
className={pwFieldClass}
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -474,7 +474,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
onValidate={this.onPasswordValidate}
|
onValidate={this.onPasswordValidate}
|
||||||
ref={field => this[LoginField.Password] = field}
|
ref={field => this[LoginField.Password] = field}
|
||||||
/>
|
/>
|
||||||
{forgotPasswordJsx}
|
{ forgotPasswordJsx }
|
||||||
{ !this.props.busy && <input className="mx_Login_submit"
|
{ !this.props.busy && <input className="mx_Login_submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
value={_t('Sign in')}
|
value={_t('Sign in')}
|
||||||
|
|
|
@ -537,15 +537,15 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
{this.renderUsername()}
|
{ this.renderUsername() }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
{this.renderPassword()}
|
{ this.renderPassword() }
|
||||||
{this.renderPasswordConfirm()}
|
{ this.renderPasswordConfirm() }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
{this.renderEmail()}
|
{ this.renderEmail() }
|
||||||
{this.renderPhoneNumber()}
|
{ this.renderPhoneNumber() }
|
||||||
</div>
|
</div>
|
||||||
{ emailHelperText }
|
{ emailHelperText }
|
||||||
{ registerButton }
|
{ registerButton }
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from "../../../index";
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import AuthPage from "./AuthPage";
|
import AuthPage from "./AuthPage";
|
||||||
import { _td } from "../../../languageHandler";
|
import { _td } from "../../../languageHandler";
|
||||||
|
@ -25,21 +25,26 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import LanguageSelector from "./LanguageSelector";
|
||||||
|
|
||||||
// translatable strings for Welcome pages
|
// translatable strings for Welcome pages
|
||||||
_td("Sign in with SSO");
|
_td("Sign in with SSO");
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.Welcome")
|
@replaceableComponent("views.auth.Welcome")
|
||||||
export default class Welcome extends React.PureComponent {
|
export default class Welcome extends React.PureComponent<IProps> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
CountlyAnalytics.instance.track("onboarding_welcome");
|
CountlyAnalytics.instance.track("onboarding_welcome");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
// FIXME: Using an import will result in wrench-element-tests failures
|
||||||
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
|
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
||||||
|
|
||||||
const pagesConfig = SdkConfig.get().embeddedPages;
|
const pagesConfig = SdkConfig.get().embeddedPages;
|
||||||
let pageUrl = null;
|
let pageUrl = null;
|
|
@ -205,8 +205,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||||
/>
|
/>
|
||||||
{icon}
|
{ icon }
|
||||||
{badge}
|
{ badge }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
label={_t("User Status")}
|
label={_t("User Status")}
|
||||||
>
|
>
|
||||||
{avatar}
|
{ avatar }
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
|
|
|
@ -65,15 +65,15 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||||
let transferItem;
|
let transferItem;
|
||||||
if (this.props.call.opponentCanBeTransferred()) {
|
if (this.props.call.opponentCanBeTransferred()) {
|
||||||
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
||||||
{_t("Transfer")}
|
{ _t("Transfer") }
|
||||||
</MenuItem>;
|
</MenuItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||||
{holdUnholdCaption}
|
{ holdUnholdCaption }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{transferItem}
|
{ transferItem }
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,8 @@ export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
|
||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||||
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
|
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
|
||||||
</MenuItemRadio>;
|
</MenuItemRadio>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -85,15 +85,15 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
|
||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||||
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
|
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
|
||||||
</MenuItemCheckbox>;
|
</MenuItemCheckbox>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
|
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
|
||||||
return <MenuItem {...props} label={label}>
|
return <MenuItem {...props} label={label}>
|
||||||
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
||||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||||
</MenuItem>;
|
</MenuItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({ firs
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={classes}>
|
return <div className={classes}>
|
||||||
{children}
|
{ children }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEventTileOps {
|
export interface IEventTileOps {
|
||||||
isWidgetHidden(): boolean;
|
isWidgetHidden(): boolean;
|
||||||
unhideWidget(): void;
|
unhideWidget(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOperableEventTile {
|
||||||
|
getEventTileOps(): IEventTileOps;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/* the MatrixEvent associated with the context menu */
|
/* the MatrixEvent associated with the context menu */
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -268,7 +272,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
resendReactionsButton = (
|
resendReactionsButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconResend"
|
iconClassName="mx_MessageContextMenu_iconResend"
|
||||||
label={ _t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount }) }
|
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
|
||||||
onClick={this.onResendReactionsClick}
|
onClick={this.onResendReactionsClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -298,7 +302,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
pinButton = (
|
pinButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconPin"
|
iconClassName="mx_MessageContextMenu_iconPin"
|
||||||
label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
|
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
|
||||||
onClick={this.onPinClick}
|
onClick={this.onPinClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -333,7 +337,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||||
onClick={this.onPermalinkClick}
|
onClick={this.onPermalinkClick}
|
||||||
label= {_t('Share')}
|
label={_t('Share')}
|
||||||
element="a"
|
element="a"
|
||||||
{
|
{
|
||||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||||
|
@ -364,7 +368,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconLink"
|
iconClassName="mx_MessageContextMenu_iconLink"
|
||||||
onClick={this.closeMenu}
|
onClick={this.closeMenu}
|
||||||
label={ _t('Source URL') }
|
label={_t('Source URL')}
|
||||||
element="a"
|
element="a"
|
||||||
{
|
{
|
||||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||||
|
|
|
@ -99,20 +99,20 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||||
onClick={this._onClearClick}
|
onClick={this._onClearClick}
|
||||||
>
|
>
|
||||||
<span>{_t("Clear status")}</span>
|
<span>{ _t("Clear status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
onClick={this._onSubmit}
|
onClick={this._onSubmit}
|
||||||
>
|
>
|
||||||
<span>{_t("Update status")}</span>
|
<span>{ _t("Update status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
disabled={!this.state.message} onClick={this._onSubmit}
|
disabled={!this.state.message} onClick={this._onSubmit}
|
||||||
>
|
>
|
||||||
<span>{_t("Set status")}</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,8 +130,8 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
onChange={this._onStatusChange}
|
onChange={this._onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
{actionButton}
|
{ actionButton }
|
||||||
{spinner}
|
{ spinner }
|
||||||
</div>
|
</div>
|
||||||
</form>;
|
</form>;
|
||||||
|
|
||||||
|
|
|
@ -219,7 +219,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
return <div className="mx_AddExistingToSpace">
|
return <div className="mx_AddExistingToSpace">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={ _t("Filter your rooms and spaces") }
|
placeholder={_t("Filter your rooms and spaces")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
autoComplete={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|
|
@ -620,7 +620,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
let inputLabel;
|
let inputLabel;
|
||||||
if (this.props.description) {
|
if (this.props.description) {
|
||||||
inputLabel = <div className="mx_AddressPickerDialog_label">
|
inputLabel = <div className="mx_AddressPickerDialog_label">
|
||||||
<label htmlFor="textinput">{this.props.description}</label>
|
<label htmlFor="textinput">{ this.props.description }</label>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,7 +690,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
&& this.props.validAddressTypes.includes('email')) {
|
&& this.props.validAddressTypes.includes('email')) {
|
||||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||||
if (defaultIdentityServerUrl) {
|
if (defaultIdentityServerUrl) {
|
||||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
|
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||||
"Use an identity server to invite by email. " +
|
"Use an identity server to invite by email. " +
|
||||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||||
"or manage in <settings>Settings</settings>.",
|
"or manage in <settings>Settings</settings>.",
|
||||||
|
@ -698,25 +698,25 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
|
||||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||||
},
|
},
|
||||||
)}</div>;
|
) }</div>;
|
||||||
} else {
|
} else {
|
||||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
|
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||||
"Use an identity server to invite by email. " +
|
"Use an identity server to invite by email. " +
|
||||||
"Manage in <settings>Settings</settings>.",
|
"Manage in <settings>Settings</settings>.",
|
||||||
{}, {
|
{}, {
|
||||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||||
},
|
},
|
||||||
)}</div>;
|
) }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
|
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
|
||||||
onFinished={this.props.onFinished} title={this.props.title}>
|
onFinished={this.props.onFinished} title={this.props.title}>
|
||||||
{inputLabel}
|
{ inputLabel }
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
||||||
{ error }
|
{ error }
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const errorList = this.props.unknownProfileUsers
|
const errorList = this.props.unknownProfileUsers
|
||||||
.map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);
|
.map(address => <li key={address.userId}>{ address.userId }: { address.errorText }</li>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_RetryInvitesDialog'
|
<BaseDialog className='mx_RetryInvitesDialog'
|
||||||
|
@ -60,8 +60,8 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div id='mx_Dialog_content'>
|
<div id='mx_Dialog_content'>
|
||||||
{/* eslint-disable-next-line */}
|
<p>{ _t("Unable to find profiles for the Matrix IDs listed below - " +
|
||||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
"would you like to invite them anyway?") }</p>
|
||||||
<ul>
|
<ul>
|
||||||
{ errorList }
|
{ errorList }
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -149,7 +149,7 @@ export default class BaseDialog extends React.Component {
|
||||||
'mx_Dialog_headerWithCancel': !!cancelButton,
|
'mx_Dialog_headerWithCancel': !!cancelButton,
|
||||||
})}>
|
})}>
|
||||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||||
{headerImage}
|
{ headerImage }
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.headerButton }
|
{ this.props.headerButton }
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue