diff --git a/package.json b/package.json index 6e10bafaea..b73462d188 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", "lint:js": "eslint --max-warnings 0 src test", + "lint:js-fix": "eslint --fix src test", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", @@ -64,8 +65,8 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase-data": "^5.1.1", - "emojibase-regex": "^4.1.1", + "emojibase-data": "^6.2.0", + "emojibase-regex": "^5.1.3", "escape-html": "^1.0.3", "file-saver": "^2.0.5", "filesize": "6.1.0", diff --git a/res/css/_components.scss b/res/css/_components.scss index 93d6b2b36f..01ce87c5c6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -87,6 +87,7 @@ @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_LeaveSpaceDialog.scss"; +@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @@ -160,6 +161,7 @@ @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; +@import "./views/messages/_CallEvent.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; @@ -172,7 +174,6 @@ @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; -@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 7b975110e1..c180a8a02d 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -45,9 +45,14 @@ limitations under the License. /* Overrides for the attachment body tiles */ -.mx_FilePanel .mx_EventTile { +.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) { word-break: break-word; - margin-top: 32px; + margin-top: 10px; + padding-top: 0; + + .mx_EventTile_line { + padding-left: 0; + } } .mx_FilePanel .mx_EventTile .mx_MImageBody { @@ -118,10 +123,6 @@ limitations under the License. padding-left: 0px; } -.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line { - background-color: $primary-bg-color; -} - .mx_FilePanel_empty::before { mask-image: url('$(res)/img/element-icons/room/files.svg'); } diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index e54feca175..d271cd2bcc 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -84,7 +84,7 @@ limitations under the License. display: inline; } -.mx_NotificationPanel .mx_EventTile_senderDetails { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails { padding-left: 36px; // align with the room name position: relative; @@ -105,7 +105,7 @@ limitations under the License. padding-left: 5px; } -.mx_NotificationPanel .mx_EventTile_line { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line { margin-right: 0px; padding-left: 36px; // align with the room name padding-top: 0px; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 7925686bf1..bc343f535c 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -190,7 +190,6 @@ limitations under the License. position: relative; padding: 8px 16px; border-radius: 8px; - min-height: 56px; box-sizing: border-box; display: grid; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 48b565be7f..e4832d9430 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_landing { + display: flex; + flex-direction: column; + > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { border-radius: 12px; @@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SearchBox { margin: 0 0 20px; + flex: 0; } .mx_SpaceFeedbackPrompt { @@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px; display: none; } } + + .mx_SpaceRoomDirectory_list { + // we don't want this container to get forced into the flexbox layout + display: contents; + } } .mx_SpaceRoomView_privateScope { diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 9a65ad008f..77dcebbb9a 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AudioPlayer_container { +.mx_MediaBody.mx_AudioPlayer_container { padding: 16px 12px 12px 12px; - max-width: 267px; // use max to make the control fit in the files/pinned panels .mx_AudioPlayer_primaryContainer { display: flex; diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index 5548f6198e..773fc50fb9 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -18,10 +18,10 @@ limitations under the License. // are shared amongst multiple voice message components. // Container for live recording and playback controls -.mx_VoiceMessagePrimaryContainer { - // 7px top and bottom for visual design. 12px left & right, but the waveform (right) - // has a 1px padding on it that we want to account for. - padding: 7px 12px 7px 11px; +.mx_MediaBody.mx_VoiceMessagePrimaryContainer { + // The waveform (right) has a 1px padding on it that we want to account for, otherwise + // inherit from mx_MediaBody + padding-right: 11px; // Cheat at alignment a bit display: flex; diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index 65e4493f19..cbddd97e18 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -27,7 +27,6 @@ limitations under the License. // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; user-select: none; - line-height: 1; } .mx_BaseAvatar_initial { diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 2678f7b4ad..5321d8ff69 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -65,7 +65,7 @@ limitations under the License. .mx_CreateRoomDialog_aliasContainer { display: flex; // put margin on container so it can collapse with siblings - margin: 10px 0; + margin: 24px 0 10px; .mx_RoomAliasField { margin: 0; @@ -101,10 +101,6 @@ limitations under the License. margin-left: 30px; } - .mx_CreateRoomDialog_topic { - margin-bottom: 36px; - } - .mx_Dialog_content > .mx_SettingsFlag { margin-top: 24px; } @@ -113,5 +109,56 @@ limitations under the License. margin: 0 85px 0 0; font-size: $font-12px; } + + .mx_Dropdown { + margin-bottom: 8px; + font-weight: normal; + font-family: $font-family; + font-size: $font-14px; + color: $primary-fg-color; + + .mx_Dropdown_input { + border: 1px solid $input-border-color; + } + + .mx_Dropdown_option { + font-size: $font-14px; + line-height: $font-32px; + height: 32px; + min-height: 32px; + + > div { + padding-left: 30px; + position: relative; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 6px; + top: 8px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $secondary-fg-color; + } + } + } + + .mx_CreateRoomDialog_dropdown_invite::before { + mask-image: url('$(res)/img/element-icons/lock.svg'); + mask-size: contain; + } + + .mx_CreateRoomDialog_dropdown_public::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 12px; + } + + .mx_CreateRoomDialog_dropdown_restricted::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-size: contain; + } + } } diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 95d7ce74c4..e018f60172 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -36,6 +36,10 @@ limitations under the License. flex-shrink: 0; overflow-y: auto; + .mx_EventTile[data-layout=bubble] { + margin-top: 20px; + } + div { pointer-events: none; } diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss new file mode 100644 index 0000000000..91df76675a --- /dev/null +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -0,0 +1,150 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_ManageRestrictedJoinRuleDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_ManageRestrictedJoinRuleDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 60vh; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ManageRestrictedJoinRuleDialog_content { + flex-grow: 1; + } + + .mx_ManageRestrictedJoinRuleDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_ManageRestrictedJoinRuleDialog_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry { + display: flex; + margin-top: 12px; + + > div { + flex-grow: 1; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 4px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_name { + margin: 0 8px; + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_description { + margin-top: 8px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_info { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_ManageRestrictedJoinRuleDialog_footer { + margin-top: 20px; + + .mx_ManageRestrictedJoinRuleDialog_footer_buttons { + display: flex; + width: max-content; + margin-left: auto; + + .mx_AccessibleButton { + display: inline-block; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } +} diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 2a2508c17c..3b67e0191e 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -27,7 +27,7 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 3px; + border-radius: 4px; border: 1px solid $strong-input-border-color; font-size: $font-12px; user-select: none; @@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus { z-index: 2; margin: 0; padding: 0px; - border-radius: 3px; + border-radius: 4px; border: 1px solid $input-focused-border-color; background-color: $primary-bg-color; max-height: 200px; diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss index 5858a60629..5329e7f1f8 100644 --- a/res/css/views/elements/_InfoTooltip.scss +++ b/res/css/views/elements/_InfoTooltip.scss @@ -30,5 +30,12 @@ limitations under the License. mask-position: center; content: ''; vertical-align: middle; +} + +.mx_InfoTooltip_icon_info::before { mask-image: url('$(res)/img/element-icons/info.svg'); } + +.mx_InfoTooltip_icon_warning::before { + mask-image: url('$(res)/img/element-icons/warning.svg'); +} diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index 44532ea6a7..032cb49359 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -19,8 +19,9 @@ limitations under the License. margin-left: 0; margin-right: 0; margin-bottom: 8px; - padding-left: 10px; - border-left: 4px solid $button-bg-color; + padding: 0 10px; + border-left: 2px solid $button-bg-color; + border-radius: 2px; .mx_ReplyThread_show { cursor: pointer; diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss new file mode 100644 index 0000000000..f2ceb086c4 --- /dev/null +++ b/res/css/views/messages/_CallEvent.scss @@ -0,0 +1,162 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_CallEvent { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + background-color: $dark-panel-bg-color; + border-radius: 8px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; + height: 60px; + + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + + .mx_CallEvent_info { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 12px; + + .mx_CallEvent_info_basic { + display: flex; + flex-direction: column; + margin-left: 10px; // To match mx_CallEvent + + .mx_CallEvent_sender { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.8rem; + margin-bottom: 3px; + } + + .mx_CallEvent_type { + font-weight: 400; + color: $secondary-fg-color; + font-size: 1.2rem; + line-height: $font-13px; + display: flex; + align-items: center; + + .mx_CallEvent_type_icon { + height: 13px; + width: 13px; + margin-right: 5px; + + &::before { + content: ''; + position: absolute; + height: 13px; + width: 13px; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + } + } + + .mx_CallEvent_content { + display: flex; + flex-direction: row; + align-items: center; + color: $secondary-fg-color; + margin-right: 16px; + + .mx_CallEvent_content_button { + height: 24px; + padding: 0px 12px; + margin-left: 8px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } + } + + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } + + .mx_CallEvent_content_tooltip { + margin-right: 5px; + } + + .mx_CallEvent_iconButton { + display: inline-flex; + margin-right: 8px; + + &::before { + content: ''; + + height: 16px; + width: 16px; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_CallEvent_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_CallEvent_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } + } +} diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index b91c461ce5..403f671673 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -60,12 +60,6 @@ limitations under the License. } .mx_MFileBody_info { - background-color: $message-body-panel-bg-color; - border-radius: 12px; - width: 243px; // same width as a playable voice message, accounting for padding - padding: 6px 12px; - color: $message-body-panel-fg-color; - .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; border-radius: 20px; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 0a199c1f45..a748435cd8 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,23 +16,15 @@ limitations under the License. $timelineImageBorderRadius: 4px; -.mx_MImageBody { - display: block; -} - .mx_MImageBody_thumbnail { - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; + object-fit: contain; border-radius: $timelineImageBorderRadius; display: flex; justify-content: center; align-items: center; - > canvas { + > div > canvas { border-radius: $timelineImageBorderRadius; } } diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss index 12e441750c..7f4bfd3fdc 100644 --- a/res/css/views/messages/_MediaBody.scss +++ b/res/css/views/messages/_MediaBody.scss @@ -20,9 +20,11 @@ limitations under the License. .mx_MediaBody { background-color: $message-body-panel-bg-color; border-radius: 12px; + max-width: 243px; // use max-width instead of width so it fits within right panels color: $message-body-panel-fg-color; font-size: $font-14px; line-height: $font-24px; -} + padding: 6px 12px; +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index e2fafe6c62..69f3c672b7 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -107,3 +107,12 @@ limitations under the License. .mx_MessageActionBar_cancelButton::after { mask-image: url('$(res)/img/element-icons/trashcan.svg'); } + +.mx_MessageActionBar_downloadButton::after { + mask-size: 14px; + mask-image: url('$(res)/img/download.svg'); +} + +.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after { + background-color: transparent; // hide the download icon mask +} diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index c66f635ffe..1d43c3030d 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_EventTile[data-layout=bubble], -.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { +.mx_EventListSummary[data-layout=bubble] { --avatarSize: 32px; --gutterSize: 11px; --cornerRadius: 12px; @@ -38,7 +38,8 @@ limitations under the License. padding-top: 0; } - &:hover { + &:hover, + &.mx_EventTile_selected { &::before { content: ''; position: absolute; @@ -80,7 +81,7 @@ limitations under the License. .mx_MessageActionBar { right: 0; - transform: translate3d(50%, 50%, 0); + transform: translate3d(90%, 50%, 0); } --backgroundColor: $eventbubble-others-bg; @@ -91,12 +92,17 @@ limitations under the License. float: right; > a { left: auto; - right: -48px; + right: -68px; } } .mx_SenderProfile { display: none; } + + .mx_ReplyTile .mx_SenderProfile { + display: block; + } + .mx_ReactionsRow { float: right; clear: right; @@ -126,7 +132,9 @@ limitations under the License. margin: 0 -12px 0 -9px; > a { position: absolute; - left: -48px; + padding: 10px 20px; + top: 0; + left: -68px; } } @@ -148,12 +156,24 @@ limitations under the License. position: absolute; top: 0; line-height: 1; + z-index: 9; img { box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } + &.mx_EventTile_noSender { + .mx_EventTile_avatar { + top: -19px; + } + } + + .mx_BaseAvatar, + .mx_EventTile_avatar { + line-height: 1; + } + &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; @@ -204,89 +224,6 @@ limitations under the License. border-left-color: $eventbubble-reply-color; } - &.mx_EventTile_bubbleContainer, - &.mx_EventTile_info, - & ~ .mx_EventListSummary[data-expanded=false] { - --backgroundColor: transparent; - --gutterSize: 0; - - display: flex; - align-items: center; - justify-content: center; - - .mx_EventTile_avatar { - position: static; - order: -1; - margin-right: 5px; - } - } - - & ~ .mx_EventListSummary { - --maxWidth: 80%; - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - .mx_EventListSummary_toggle { - float: none; - margin: 0; - order: 9; - margin-left: 5px; - } - .mx_EventListSummary_avatars { - padding-top: 0; - } - - &::after { - content: ""; - clear: both; - } - - .mx_EventTile { - margin: 0 6px; - } - - .mx_EventTile_line { - margin: 0 5px; - > a { - left: auto; - right: 0; - transform: translateX(calc(100% + 5px)); - } - } - - .mx_MessageActionBar { - transform: translate3d(50%, 0, 0); - } - } - - & ~ .mx_EventListSummary[data-expanded=false] { - padding: 0 34px; - } - - /* events that do not require bubble layout */ - & ~ .mx_EventListSummary, - &.mx_EventTile_bad { - .mx_EventTile_line { - background: transparent; - } - - &:hover { - &::before { - background: transparent; - } - } - } - - & + .mx_EventListSummary { - .mx_EventTile { - margin-top: 0; - padding: 0; - } - } - - .mx_EventListSummary_toggle { - margin-right: 55px; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -321,3 +258,78 @@ limitations under the License. max-width: 100%; } } + +.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], +.mx_EventTile.mx_EventTile_info[data-layout=bubble], +.mx_EventListSummary[data-layout=bubble][data-expanded=false] { + --backgroundColor: transparent; + --gutterSize: 0; + + display: flex; + align-items: center; + justify-content: center; + padding: 5px 0; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } +} + +.mx_EventListSummary[data-layout=bubble] { + --maxWidth: 80%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: calc(var(--gutterSize) + var(--avatarSize)); + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + margin-left: 5px; + margin-right: 55px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + padding: 2px 0; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(90%, 0, 0); + } +} + +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + padding: 0 34px; +} + +/* events that do not require bubble layout */ +.mx_EventListSummary[data-layout=bubble], +.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index d6ad37f6bb..4a419244ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -132,10 +132,15 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line { + &.mx_EventTile_info .mx_EventTile_line, + & ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { padding-left: calc($left-gutter + 18px); } + & ~ .mx_EventListSummary .mx_EventTile_line { + padding-left: calc($left-gutter); + } + &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -208,43 +213,11 @@ $hover-select-border: 4px; text-decoration: none; } - /* all the overflow-y: hidden; are to trap Zalgos - - but they introduce an implicit overflow-x: auto. - so make that explicitly hidden too to avoid random - horizontal scrollbars occasionally appearing, like in - https://github.com/vector-im/vector-web/issues/1154 - */ - .mx_EventTile_content { - display: block; - overflow-y: hidden; - overflow-x: hidden; - margin-right: 34px; - } - /* De-zalgoing */ .mx_EventTile_body { overflow-y: hidden; } - /* Spoiler stuff */ - .mx_EventTile_spoiler { - cursor: pointer; - } - - .mx_EventTile_spoiler_reason { - color: $event-timestamp-color; - font-size: $font-11px; - } - - .mx_EventTile_spoiler_content { - filter: blur(5px) saturate(0.1) sepia(1); - transition-duration: 0.5s; - } - - .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - filter: none; - } - &:hover.mx_EventTile_verified .mx_EventTile_line, &:hover.mx_EventTile_unverified .mx_EventTile_line, &:hover.mx_EventTile_unknown .mx_EventTile_line { @@ -307,6 +280,36 @@ $hover-select-border: 4px; } } +/* all the overflow-y: hidden; are to trap Zalgos - + but they introduce an implicit overflow-x: auto. + so make that explicitly hidden too to avoid random + horizontal scrollbars occasionally appearing, like in + https://github.com/vector-im/vector-web/issues/1154 */ +.mx_EventTile_content { + overflow-y: hidden; + overflow-x: hidden; + margin-right: 34px; +} + +/* Spoiler stuff */ +.mx_EventTile_spoiler { + cursor: pointer; +} + +.mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: $font-11px; +} + +.mx_EventTile_spoiler_content { + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; +} + +.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + filter: none; +} + .mx_RoomView_timeline_rr_enabled { .mx_EventTile:not([data-layout=bubble]) { @@ -333,6 +336,13 @@ $hover-select-border: 4px; .mx_EventTile_msgOption { grid-column: 2; } + + &:hover { + .mx_EventTile_line { + // To avoid bubble events being highlighted + background-color: inherit !important; + } + } } .mx_EventTile_readAvatars { @@ -462,6 +472,10 @@ $hover-select-border: 4px; background-color: $header-panel-bg-color; } + pre code > * { + display: inline-block; + } + pre { // have to use overlay rather than auto otherwise Linux and Windows // Chrome gets very confused about vertical spacing: @@ -559,6 +573,12 @@ $hover-select-border: 4px; color: $accent-color-alt; } +.mx_EventTile_content .markdown-body blockquote { + border-left: 2px solid $blockquote-bar-color; + border-radius: 2px; + padding: 0 10px; +} + .mx_EventTile_content .markdown-body .hljs { display: inline !important; } diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index ddee81a914..ebb7f99e45 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -26,6 +26,7 @@ $left-gutter: 64px; > .mx_EventTile_avatar { position: absolute; + z-index: 9; } .mx_MessageTimestamp { diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 97190807ca..578c0325d2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -116,6 +116,11 @@ $irc-line-height: $font-18px; .mx_EditMessageComposer_buttons { position: relative; } + + .mx_ReactionsRow { + padding-left: 0; + padding-right: 0; + } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index 0832337ecd..24900ee14b 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -19,7 +19,8 @@ limitations under the License. margin-right: 15px; margin-bottom: 15px; display: flex; - border-left: 4px solid $preview-widget-bar-color; + border-left: 2px solid $preview-widget-bar-color; + border-radius: 2px; color: $preview-widget-fg-color; } @@ -33,7 +34,7 @@ limitations under the License. .mx_LinkPreviewWidget_caption { margin-left: 15px; flex: 1 1 auto; - overflow-x: hidden; // cause it to wrap rather than clip + overflow: hidden; // cause it to wrap rather than clip } .mx_LinkPreviewWidget_title { diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index f3e204e415..fd21e5f348 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -60,8 +60,6 @@ limitations under the License. $reply-lines: 2; $line-height: $font-22px; - pointer-events: none; - text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 9f6a8d52ce..4b7eb54188 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -29,8 +29,10 @@ limitations under the License. display: flex; flex-direction: column; // min-height at this level so the mx_BasicMessageComposer_input - // still stays vertically centered when less than 50px - min-height: 50px; + // still stays vertically centered when less than 55px. + // We also set this to ensure the voice message recording widget + // doesn't cause a jump. + min-height: 55px; .mx_BasicMessageComposer_input { padding: 3px 0; diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 892f5fe744..0d679af4e5 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -47,14 +47,14 @@ limitations under the License. color: $settings-subsection-fg-color; font-size: $font-14px; display: block; - margin: 10px 100px 10px 0; // Align with the rest of the view + margin: 10px 80px 10px 0; // Align with the rest of the view } .mx_SettingsTab_section { margin-bottom: 24px; .mx_SettingsFlag { - margin-right: 100px; + margin-right: 80px; margin-bottom: 10px; } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index 23dcc532b2..2aab201352 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,6 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SecurityRoomSettingsTab { + .mx_SettingsTab_showAdvanced { + padding: 0; + margin-bottom: 16px; + } + + .mx_SecurityRoomSettingsTab_spacesWithAccess { + > h4 { + color: $secondary-fg-color; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + text-transform: uppercase; + } + + > span { + font-weight: 500; + font-size: $font-14px; + line-height: 32px; // matches height of avatar for v-align + color: $secondary-fg-color; + display: inline-block; + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_BaseAvatar { + margin-right: 8px; + } + + & + span { + margin-left: 16px; + } + } + } +} + .mx_SecurityRoomSettingsTab_warning { display: block; @@ -26,5 +64,51 @@ limitations under the License. } .mx_SecurityRoomSettingsTab_encryptionSection { - margin-bottom: 25px; + padding-bottom: 24px; + border-bottom: 1px solid $menu-border-color; + margin-bottom: 32px; +} + +.mx_SecurityRoomSettingsTab_upgradeRequired { + margin-left: 16px; + padding: 4px 16px; + border: 1px solid $accent-color; + border-radius: 8px; + color: $accent-color; + font-size: $font-12px; + line-height: $font-15px; +} + +.mx_SecurityRoomSettingsTab_joinRule { + .mx_RadioButton { + padding-top: 16px; + margin-bottom: 8px; + + .mx_RadioButton_content { + margin-left: 14px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + display: block; + } + } + + > span { + display: inline-block; + margin-left: 34px; + margin-bottom: 16px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + + & + .mx_RadioButton { + border-top: 1px solid $menu-border-color; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } } diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 94983a60bf..ca5a6f0a66 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,8 +15,7 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_fontSlider_preview, -.mx_AppearanceUserSettingsTab_Layout { +.mx_AppearanceUserSettingsTab_fontSlider_preview { @mixin mx_Settings_fullWidthField; } @@ -45,6 +44,11 @@ limitations under the License. border-radius: 10px; padding: 0 16px 9px 16px; pointer-events: none; + display: flow-root; + + .mx_EventTile[data-layout=bubble] { + margin-top: 30px; + } .mx_EventTile_msgOption { display: none; @@ -154,13 +158,10 @@ limitations under the License. .mx_AppearanceUserSettingsTab_Layout_RadioButtons { display: flex; flex-direction: row; + gap: 24px; color: $primary-fg-color; - .mx_AppearanceUserSettingsTab_spacer { - width: 24px; - } - > .mx_AppearanceUserSettingsTab_Layout_RadioButton { flex-grow: 0; flex-shrink: 1; @@ -210,6 +211,21 @@ limitations under the License. .mx_RadioButton_checked { background-color: rgba($accent-color, 0.08); } + + .mx_EventTile { + margin: 0; + &[data-layout=bubble] { + margin-right: 40px; + } + &[data-layout=irc] { + > a { + display: none; + } + } + .mx_EventTile_line { + max-width: 90%; + } + } } .mx_AppearanceUserSettingsTab_Advanced { diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 index a52e5a3800..128aac8139 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 index 660a93193d..a95e89c094 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg new file mode 100644 index 0000000000..eef5193140 --- /dev/null +++ b/res/img/element-icons/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/globe.svg b/res/img/feather-customised/globe.svg deleted file mode 100644 index 8af7dc41dc..0000000000 --- a/res/img/feather-customised/globe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/voip/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 0000000000..a2f3bc73ac --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 0000000000..5e3993584e --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 2a4ebff034..655492661c 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #394049; // "Dark Tile" -$message-body-panel-icon-fg-color: #21262C; // "Separator" -$message-body-panel-icon-bg-color: $tertiary-fg-color; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: #21262C; // "System Dark" $voice-record-stop-border-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; @@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD; .hljs-tag { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } + +.hljs-addition { + background: #1a4b59; +} + +.hljs-deletion { + background: #53232a; +} diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 555ef4f66c..0c0197cfb0 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #394049; -$message-body-panel-icon-fg-color: $primary-bg-color; -$message-body-panel-icon-bg-color: $secondary-fg-color; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: #21262C; // See non-legacy dark for variable information $voice-record-stop-border-color: #6F7882; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index f349a804a8..b7d45452ff 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: #F4F6FA; // See non-legacy _light for variable information $voice-record-stop-symbol-color: #ff4b55; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index ef5f4d8c86..32722515d8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; // "Separator" $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: #F4F6FA; // These two don't change between themes. They are the $warning-color, but we don't // want custom themes to affect them by accident. diff --git a/src/@types/common.ts b/src/@types/common.ts index 1fb9ba4303..36ef7a9ace 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { JSXElementConstructor } from "react"; +import React, { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude]?: never}; @@ -22,3 +22,4 @@ export type XOR = (T | U) extends object ? (Without & U) | (Without< export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; +export type ReactAnyComponent = React.Component | React.ExoticComponent; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7192eb81cc..9d6bc2c6fb 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; +/* eslint-disable @typescript-eslint/naming-convention */ + declare global { interface Window { matrixChat: ReturnType; @@ -90,6 +92,7 @@ declare global { mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; + mxOnRecaptchaLoaded?: () => void; } interface Document { @@ -114,7 +117,7 @@ declare global { } interface StorageEstimate { - usageDetails?: {[key: string]: number}; + usageDetails?: { [key: string]: number }; } interface HTMLAudioElement { @@ -185,4 +188,21 @@ declare global { parameterDescriptors?: AudioParamDescriptor[]; } ); + + // eslint-disable-next-line no-var + var grecaptcha: + | undefined + | { + reset: (id: string) => void; + render: ( + divId: string, + options: { + sitekey: string; + callback: (response: string) => void; + }, + ) => string; + isReady: () => boolean; + }; } + +/* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts new file mode 100644 index 0000000000..96f671c52f --- /dev/null +++ b/src/@types/svg.d.ts @@ -0,0 +1,20 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +declare module "*.svg" { + const path: string; + export default path; +} diff --git a/src/Analytics.tsx b/src/Analytics.tsx index ce8287de56..fc4664039f 100644 --- a/src/Analytics.tsx +++ b/src/Analytics.tsx @@ -270,7 +270,7 @@ export class Analytics { localStorage.removeItem(LAST_VISIT_TS_KEY); } - private async _track(data: IData) { + private async track(data: IData) { if (this.disabled) return; const now = new Date(); @@ -304,7 +304,7 @@ export class Analytics { } public ping() { - this._track({ + this.track({ ping: "1", }); localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts @@ -324,14 +324,14 @@ export class Analytics { // But continue anyway because we still want to track the change } - this._track({ + this.track({ gt_ms: String(generationTimeMs), }); } public trackEvent(category: string, action: string, name?: string, value?: string) { if (this.disabled) return; - this._track({ + this.track({ e_c: category, e_a: action, e_n: name, @@ -395,17 +395,17 @@ export class Analytics { Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { title: _t('Analytics'), description:
-
{_t('The information being sent to us to help make %(brand)s better includes:', { +
{ _t('The information being sent to us to help make %(brand)s better includes:', { brand: SdkConfig.get().brand, - })}
+ }) }
{ rows.map((row) => - + ) } { row[1] !== undefined && } ) } { otherVariables.map((item, index) => diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index f90854ee64..72be3f8e67 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3; // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; -export enum AudioID { +enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', @@ -142,6 +142,7 @@ export enum PlaceCallType { export enum CallHandlerEvent { CallsChanged = "calls_changed", CallChangeRoom = "call_change_room", + SilencedCallsChanged = "silenced_calls_changed", } export default class CallHandler extends EventEmitter { @@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter { // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); + private silencedCalls = new Set(); // callIds + static sharedInstance() { if (!window.mxCallHandler) { window.mxCallHandler = new CallHandler(); @@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter { } } + public silenceCall(callId: string) { + this.silencedCalls.add(callId); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + + // Don't pause audio if we have calls which are still ringing + if (this.areAnyCallsUnsilenced()) return; + this.pause(AudioID.Ring); + } + + public unSilenceCall(callId: string) { + this.silencedCalls.delete(callId); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + this.play(AudioID.Ring); + } + + public isCallSilenced(callId: string): boolean { + return this.silencedCalls.has(callId); + } + + /** + * Returns true if there is at least one unsilenced call + * @returns {boolean} + */ + private areAnyCallsUnsilenced(): boolean { + return this.calls.size > this.silencedCalls.size; + } + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); @@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter { }, true); }; + public getCallById(callId: string): MatrixCall { + for (const call of this.calls.values()) { + if (call.callId === callId) return call; + } + return null; + } + getCallForRoom(roomId: string): MatrixCall { return this.calls.get(roomId) || null; } @@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter { break; } + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + switch (newState) { case CallState.Ringing: this.play(AudioID.Ring); @@ -450,28 +491,18 @@ export default class CallHandler extends EventEmitter { break; case CallState.Ended: { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { this.play(AudioID.Busy); let title; let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { title = _t("User Busy"); description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { + } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { title = _t("Call Failed"); description = _t("The call could not be established"); } @@ -480,7 +511,7 @@ export default class CallHandler extends EventEmitter { title, description, }); } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting ) { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { title: _t("Answered Elsewhere"), @@ -615,23 +646,23 @@ export default class CallHandler extends EventEmitter { private showICEFallbackPrompt() { const cli = MatrixClientPeg.get(); - const code = sub => {sub}; + const code = sub => { sub }; Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { title: _t("Call failed due to misconfigured server"), description:
-

{_t( +

{ _t( "Please ask the administrator of your homeserver " + "(%(homeserverDomain)s) to configure a TURN server in " + "order for calls to work reliably.", { homeserverDomain: cli.getDomain() }, { code }, - )}

-

{_t( + ) }

+

{ _t( "Alternatively, you can try to use the public server at " + "turn.matrix.org, but this will not be as reliable, and " + "it will share your IP address with that server. You can also manage " + "this in Settings.", null, { code }, - )}

+ ) }

, button: _t('Try using turn.matrix.org'), cancelButton: _t('OK'), @@ -649,19 +680,19 @@ export default class CallHandler extends EventEmitter { if (call.type === CallType.Voice) { title = _t("Unable to access microphone"); description =
- {_t( + { _t( "Call failed because microphone could not be accessed. " + "Check that a microphone is plugged in and set up correctly.", - )} + ) }
; } else if (call.type === CallType.Video) { title = _t("Unable to access webcam / microphone"); description =
- {_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:") }
    -
  • {_t("A microphone and webcam are plugged in and set up correctly")}
  • -
  • {_t("Permission is granted to use the webcam")}
  • -
  • {_t("No other application is using the webcam")}
  • +
  • { _t("A microphone and webcam are plugged in and set up correctly") }
  • +
  • { _t("Permission is granted to use the webcam") }
  • +
  • { _t("No other application is using the webcam") }
; } diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 0c65a7bd35..c5bcb226ff 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -425,10 +425,10 @@ export default class ContentMessages { const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'), description: ( -
{_t( +
{ _t( 'At this time it is not possible to reply with a file. ' + 'Would you like to upload this file without replying?', - )}
+ ) }
), hasCancelButton: true, button: _t("Continue"), diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index d033063677..51c624e3c3 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan import { isSecureBackupRequired } from './utils/WellKnownUtils'; import { isLoggedIn } from './components/structures/MatrixChat'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { ActionPayload } from "./dispatcher/payloads"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -58,28 +59,28 @@ export default class DeviceListener { } start() { - MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices); - MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); - MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); - MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); - MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); - MatrixClientPeg.get().on('accountData', this._onAccountData); - MatrixClientPeg.get().on('sync', this._onSync); - MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents); - this.dispatcherRef = dis.register(this._onAction); - this._recheck(); + MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices); + MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated); + MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged); + MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged); + MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged); + MatrixClientPeg.get().on('accountData', this.onAccountData); + MatrixClientPeg.get().on('sync', this.onSync); + MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); + this.dispatcherRef = dis.register(this.onAction); + this.recheck(); } stop() { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices); - MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); - MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); - MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); - MatrixClientPeg.get().removeListener('accountData', this._onAccountData); - MatrixClientPeg.get().removeListener('sync', this._onSync); - MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents); + MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices); + MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated); + MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged); + MatrixClientPeg.get().removeListener('accountData', this.onAccountData); + MatrixClientPeg.get().removeListener('sync', this.onSync); + MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); } if (this.dispatcherRef) { dis.unregister(this.dispatcherRef); @@ -103,15 +104,15 @@ export default class DeviceListener { this.dismissed.add(d); } - this._recheck(); + this.recheck(); } dismissEncryptionSetup() { this.dismissedThisDeviceToast = true; - this._recheck(); + this.recheck(); } - _ensureDeviceIdsAtStartPopulated() { + private ensureDeviceIdsAtStartPopulated() { if (this.ourDeviceIdsAtStart === null) { const cli = MatrixClientPeg.get(); this.ourDeviceIdsAtStart = new Set( @@ -120,39 +121,39 @@ export default class DeviceListener { } } - _onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { + private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { // If we didn't know about *any* devices before (ie. it's fresh login), // then they are all pre-existing devices, so ignore this and set the // devicesAtStart list to the devices that we see after the fetch. if (initialFetch) return; const myUserId = MatrixClientPeg.get().getUserId(); - if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated(); + if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated(); // No need to do a recheck here: we just need to get a snapshot of our devices // before we download any new ones. }; - _onDevicesUpdated = (users: string[]) => { + private onDevicesUpdated = (users: string[]) => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; - this._recheck(); + this.recheck(); }; - _onDeviceVerificationChanged = (userId: string) => { + private onDeviceVerificationChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; - this._recheck(); + this.recheck(); }; - _onUserTrustStatusChanged = (userId: string) => { + private onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; - this._recheck(); + this.recheck(); }; - _onCrossSingingKeysChanged = () => { - this._recheck(); + private onCrossSingingKeysChanged = () => { + this.recheck(); }; - _onAccountData = (ev) => { + private onAccountData = (ev: MatrixEvent) => { // User may have: // * migrated SSSS to symmetric // * uploaded keys to secret storage @@ -163,32 +164,32 @@ export default class DeviceListener { ev.getType().startsWith('m.cross_signing.') || ev.getType() === 'm.megolm_backup.v1' ) { - this._recheck(); + this.recheck(); } }; - _onSync = (state, prevState) => { - if (state === 'PREPARED' && prevState === null) this._recheck(); + private onSync = (state, prevState) => { + if (state === 'PREPARED' && prevState === null) this.recheck(); }; - _onRoomStateEvents = (ev: MatrixEvent) => { + private onRoomStateEvents = (ev: MatrixEvent) => { if (ev.getType() !== "m.room.encryption") { return; } // If a room changes to encrypted, re-check as it may be our first // encrypted room. This also catches encrypted room creation as well. - this._recheck(); + this.recheck(); }; - _onAction = ({ action }) => { + private onAction = ({ action }: ActionPayload) => { if (action !== "on_logged_in") return; - this._recheck(); + this.recheck(); }; // The server doesn't tell us when key backup is set up, so we poll // & cache the result - async _getKeyBackupInfo() { + private async getKeyBackupInfo() { const now = (new Date()).getTime(); if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); @@ -206,7 +207,7 @@ export default class DeviceListener { return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId)); } - async _recheck() { + private async recheck() { const cli = MatrixClientPeg.get(); if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; @@ -235,7 +236,7 @@ export default class DeviceListener { // Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); } else { - const backupInfo = await this._getKeyBackupInfo(); + const backupInfo = await this.getKeyBackupInfo(); if (backupInfo) { // No cross-signing on account but key backup available (upgrade encryption) showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); @@ -256,7 +257,7 @@ export default class DeviceListener { // This needs to be done after awaiting on downloadKeys() above, so // we make sure we get the devices after the fetch is done. - this._ensureDeviceIdsAtStartPopulated(); + this.ensureDeviceIdsAtStartPopulated(); // Unverified devices that were there last time the app ran // (technically could just be a boolean: we don't actually diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index a37b7f0ac9..af5d2b3019 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import linkifyMatrix from './linkify-matrix'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; +import { getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -57,7 +57,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; @@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const data = getEmojiFromUnicode(char); - return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); -} - -/** - * Returns the unicode character for an emoji shortcode - * - * @param {String} shortcode The shortcode (such as :thumbup:) - * @return {String} The emoji character; null if none exists - */ -export function shortcodeToUnicode(shortcode: string): string { - shortcode = shortcode.slice(1, shortcode.length - 1); - const data = SHORTCODE_TO_EMOJI.get(shortcode); - return data ? data.unicode : null; + const shortcodes = getEmojiFromUnicode(char)?.shortcodes; + return shortcodes?.length ? `:${shortcodes[0]}:` : ''; } export function processHtmlForSending(html: string): string { diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 447c5edd30..e91e1d72cf 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -149,17 +149,17 @@ export default class IdentityAuthClient { title: _t("Identity server has no terms of service"), description: (
-

{_t( +

{ _t( "This action requires accessing the default identity server " + " to validate an email address or phone number, " + "but the server does not have any terms of service.", {}, { - server: () => {abbreviateUrl(identityServerUrl)}, + server: () => { abbreviateUrl(identityServerUrl) }, }, - )}

-

{_t( + ) }

+

{ _t( "Only continue if you trust the owner of the server.", - )}

+ ) }

), button: _t("Trust"), diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index e9364b1b47..f43351aab2 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -105,7 +105,7 @@ export interface IMatrixClientPeg { * This module provides a singleton instance of this class so the 'current' * Matrix Client object is available easily. */ -class _MatrixClientPeg implements IMatrixClientPeg { +class MatrixClientPegClass implements IMatrixClientPeg { // These are the default options used when when the // client is started in 'start'. These can be altered // at any time up to after the 'will_start_client' @@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { } if (!window.mxMatrixClientPeg) { - window.mxMatrixClientPeg = new _MatrixClientPeg(); + window.mxMatrixClientPeg = new MatrixClientPegClass(); } export const MatrixClientPeg = window.mxMatrixClientPeg; diff --git a/src/Modal.tsx b/src/Modal.tsx index 55fc871d67..1e84078ddb 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -378,7 +378,7 @@ export class ModalManager { const dialog = (
- {modal.elem} + { modal.elem }
diff --git a/src/Registration.js b/src/Registration.js index 70dcd38454..c59d244149 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) { description: _t("Use your account or create a new one to continue."), button: _t("Create Account"), extraButtons: [ - , + , ], onFinished: (proceed) => { if (proceed) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 7753ff6f75..b4deb6d8c4 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; -import { inviteUsersToRoom } from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; @@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import { upgradeRoom } from './utils/RoomUpgrade'; import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; import ErrorDialog from './components/views/dialogs/ErrorDialog'; import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; @@ -277,50 +277,8 @@ export const Commands = [ /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { - if (!resp.continue) return; - - let checkForUpgradeFn; - try { - const upgradePromise = cli.upgradeRoom(roomId, args); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (resp.invite) { - checkForUpgradeFn = async (newRoom) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - await upgradePromise; - } catch (e) { - console.error(e); - - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { - title: _t('Error upgrading room'), - description: _t( - 'Double check that your server supports the room version chosen and try again.'), - }); - } + if (!resp?.continue) return; + await upgradeRoom(room, args, resp.invite); })); } return reject(this.getUsage()); @@ -480,14 +438,14 @@ export const Commands = [ 'Identity server', QuestionDialog, { title: _t("Use an identity server"), - description:

{_t( + description:

{ _t( "Use an identity server to invite by email. " + "Click continue to use the default identity server " + "(%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, - )}

, + ) }

, button: _t("Continue"), }, ); @@ -522,7 +480,7 @@ export const Commands = [ aliases: ['j', 'goto'], args: '', description: _td('Joins room with given address'), - runFn: function(_, args) { + runFn: function(roomId, args) { if (args) { // Note: we support 2 versions of this command. The first is // the public-facing one for most users and the other is a @@ -1069,7 +1027,7 @@ export const Commands = [ command: "msg", description: _td("Sends a message to the given user"), args: " ", - runFn: function(_, args) { + runFn: function(roomId, args) { if (args) { // matches the first whitespace delimited group and then the rest of the string const matches = args.match(/^(\S+?)(?: +(.*))?$/s); diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 0056a37c85..7bad8eb50e 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import { MatrixClientPeg } from './MatrixClientPeg'; import { _t } from './languageHandler'; import * as Roles from './Roles'; import { isValid3pidInvite } from "./RoomInvite"; @@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { }); } -function textForCallAnswerEvent(event: MatrixEvent): () => string | null { - return () => { - const senderName = event.sender ? event.sender.name : _t('Someone'); - const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); - return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported; - }; -} - -function textForCallHangupEvent(event: MatrixEvent): () => string | null { - const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); - const eventContent = event.getContent(); - let getReason = () => ""; - if (!MatrixClientPeg.get().supportsVoip()) { - getReason = () => _t('(not supported by this browser)'); - } else if (eventContent.reason) { - if (eventContent.reason === "ice_failed") { - // We couldn't establish a connection at all - getReason = () => _t('(could not connect media)'); - } else if (eventContent.reason === "ice_timeout") { - // We established a connection but it died - getReason = () => _t('(connection failed)'); - } else if (eventContent.reason === "user_media_failed") { - // The other side couldn't open capture devices - getReason = () => _t("(their device couldn't start the camera / microphone)"); - } else if (eventContent.reason === "unknown_error") { - // An error code the other side doesn't have a way to express - // (as opposed to an error code they gave but we don't know about, - // in which case we show the error code) - getReason = () => _t("(an error occurred)"); - } else if (eventContent.reason === "invite_timeout") { - getReason = () => _t('(no answer)'); - } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { - // workaround for https://github.com/vector-im/element-web/issues/5178 - // it seems Android randomly sets a reason of "user hangup" which is - // interpreted as an error code :( - // https://github.com/vector-im/riot-android/issues/2623 - // Also the correct hangup code as of VoIP v1 (with underscore) - getReason = () => ''; - } else { - getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason }); - } - } - return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason(); -} - -function textForCallRejectEvent(event: MatrixEvent): () => string | null { - return () => { - const senderName = event.sender ? event.sender.name : _t('Someone'); - return _t('%(senderName)s declined the call.', { senderName }); - }; -} - -function textForCallInviteEvent(event: MatrixEvent): () => string | null { - const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); - // FIXME: Find a better way to determine this from the event? - let isVoice = true; - if (event.getContent().offer && event.getContent().offer.sdp && - event.getContent().offer.sdp.indexOf('m=video') !== -1) { - isVoice = false; - } - const isSupported = MatrixClientPeg.get().supportsVoip(); - - // This ladder could be reduced down to a couple string variables, however other languages - // can have a hard time translating those strings. In an effort to make translations easier - // and more accurate, we break out the string-based variables to a couple booleans. - if (isVoice && isSupported) { - return () => _t("%(senderName)s placed a voice call.", { - senderName: getSenderName(), - }); - } else if (isVoice && !isSupported) { - return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", { - senderName: getSenderName(), - }); - } else if (!isVoice && isSupported) { - return () => _t("%(senderName)s placed a video call.", { - senderName: getSenderName(), - }); - } else if (!isVoice && !isSupported) { - return () => _t("%(senderName)s placed a video call. (not supported by this browser)", { - senderName: getSenderName(), - }); - } -} - function textForThreePidInviteEvent(event: MatrixEvent): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -652,10 +567,6 @@ interface IHandlers { const handlers: IHandlers = { 'm.room.message': textForMessageEvent, - 'm.call.invite': textForCallInviteEvent, - 'm.call.answer': textForCallAnswerEvent, - 'm.call.hangup': textForCallHangupEvent, - 'm.call.reject': textForCallRejectEvent, }; const stateHandlers: IHandlers = { diff --git a/src/UserAddress.ts b/src/UserAddress.ts index a2c546deb7..248814aa01 100644 --- a/src/UserAddress.ts +++ b/src/UserAddress.ts @@ -14,35 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from "prop-types"; - const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; - export enum AddressType { Email = "email", MatrixUserId = "mx-user-id", MatrixRoomId = "mx-room-id", } +export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId]; + // PropType definition for an object describing // an address that can be invited to a room (which // could be a third party identifier or a matrix ID) // along with some additional information about the // address / target. -export const UserAddressType = PropTypes.shape({ - addressType: PropTypes.oneOf(addressTypes).isRequired, - address: PropTypes.string.isRequired, - displayName: PropTypes.string, - avatarMxc: PropTypes.string, +export interface IUserAddress { + addressType: AddressType; + address: string; + displayName?: string; + avatarMxc?: string; // true if the address is known to be a valid address (eg. is a real // user we've seen) or false otherwise (eg. is just an address the // user has entered) - isKnown: PropTypes.bool, -}); + isKnown?: boolean; +} export function getAddressType(inputText: string): AddressType | null { if (emailRegex.test(inputText)) { diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index c5cf85facd..9cc7b60c99 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -370,8 +370,8 @@ export const toggleDialog = () => { const sections = categoryOrder.map(category => { const list = shortcuts[category]; return
-

{_t(category)}

-
{list.map(shortcut => )}
+

{ _t(category) }

+
{ list.map(shortcut => ) }
; }); diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 8d882fadea..90538760bb 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -62,9 +62,9 @@ const Toolbar: React.FC = ({ children, ...props }) => { }; return - {({ onKeyDownHandler }) =>
+ { ({ onKeyDownHandler }) =>
{ children } -
} +
}
; }; diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx similarity index 71% rename from src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index a19494c753..4d8f5e5663 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -15,8 +15,10 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; + +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import Spinner from "../../../../components/views/elements/Spinner"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; @@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import { Action } from "../../../../dispatcher/actions"; import { SettingLevel } from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + disabling: boolean; +} /* * Allows the user to disable the Event Index. */ -export default class DisableEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - } - - constructor(props) { +export default class DisableEventIndexDialog extends React.Component { + constructor(props: IProps) { super(props); - this.state = { disabling: false, }; } - _onDisable = async () => { + private onDisable = async (): Promise => { this.setState({ disabling: true, }); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - this.props.onFinished(); + this.props.onFinished(true); dis.fire(Action.ViewUserSettings); - } - - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Spinner = sdk.getComponent('elements.Spinner'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + }; + public render(): React.ReactNode { return ( - {_t("If disabled, messages from encrypted rooms won't appear in search results.")} - {this.state.disabling ? :
} + { _t("If disabled, messages from encrypted rooms won't appear in search results.") } + { this.state.disabling ? :
} { - Modal.createTrackedDialogAsync("Disable message search", "Disable message search", - import("./DisableEventIndexDialog"), + const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default; + Modal.createTrackedDialog("Disable message search", "Disable message search", + DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true, ); }; @@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component - {_t( + { _t( "%(brand)s is securely caching encrypted messages locally for them " + "to appear in search results:", { brand }, - )} + ) }
- {crawlerState}
- {_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
- {_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
- {_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { + { crawlerState }
+ { _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }
+ { _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }
+ { _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", { doneRooms: formatCountLong(doneRooms), totalRooms: formatCountLong(this.state.roomCount), - })}
+ }) }
- {eventIndexingSettings} + { eventIndexingSettings } -

{_t( +

{ _t( "Warning: You should only set up key backup from a trusted computer.", {}, - { b: sub => {sub} }, - )}

-

{_t( + { b: sub => { sub } }, + ) }

+

{ _t( "We'll store an encrypted copy of your keys on our server. " + "Secure your backup with a Security Phrase.", - )}

-

{_t("For maximum security, this should be different from your account password.")}

+ ) }

+

{ _t("For maximum security, this should be different from your account password.") }

@@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { />
- {_t("Advanced")} - - {_t("Set up with a Security Key")} + { _t("Advanced") } + + { _t("Set up with a Security Key") }
; @@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent { let passPhraseMatch = null; if (matchText) { passPhraseMatch =
-
{matchText}
+
{ matchText }
- {changeText} + { changeText }
; } const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
-

{_t( +

{ _t( "Enter your Security Phrase a second time to confirm it.", - )}

+ ) }

@@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { autoFocus={true} />
- {passPhraseMatch} + { passPhraseMatch }
-

{_t( +

{ _t( "Your Security Key is a safety net - you can use it to restore " + "access to your encrypted messages if you forget your Security Phrase.", - )}

-

{_t( + ) }

+

{ _t( "Keep a copy of it somewhere secure, like a password manager or even a safe.", - )}

+ ) }

- {_t("Your Security Key")} + { _t("Your Security Key") }
- {this._keyBackupInfo.recovery_key} + { this._keyBackupInfo.recovery_key }
@@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent { if (this.state.copied) { introText = _t( "Your Security Key has been copied to your clipboard, paste it to:", - {}, { b: s => {s} }, + {}, { b: s => { s } }, ); } else if (this.state.downloaded) { introText = _t( "Your Security Key is in your Downloads folder.", - {}, { b: s => {s} }, + {}, { b: s => { s } }, ); } const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
- {introText} + { introText }
    -
  • {_t("Print it and store it somewhere safe", {}, { b: s => {s} })}
  • -
  • {_t("Save it on a USB key or backup drive", {}, { b: s => {s} })}
  • -
  • {_t("Copy it to your personal cloud storage", {}, { b: s => {s} })}
  • +
  • { _t("Print it and store it somewhere safe", {}, { b: s => { s } }) }
  • +
  • { _t("Save it on a USB key or backup drive", {}, { b: s => { s } }) }
  • +
  • { _t("Copy it to your personal cloud storage", {}, { b: s => { s } }) }
- +
; } @@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _renderPhaseDone() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
-

{_t( +

{ _t( "Your keys are being backed up (the first backup could take a few minutes).", - )}

+ ) }

- {_t( + { _t( "Without setting up Secure Message Recovery, you won't be able to restore your " + "encrypted message history if you log out or use another session.", - )} + ) } -

{_t("Unable to create key backup")}

+

{ _t("Unable to create key backup") }

- {content} + { content }
); diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index e1254929db..641df4f897 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -474,10 +474,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- - {_t("Generate a Security Key")} + + { _t("Generate a Security Key") }
-
{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}
+
{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }
); } @@ -493,10 +493,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- - {_t("Enter a Security Phrase")} + + { _t("Enter a Security Phrase") }
-
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
+
{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }
); } @@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null; return -

{_t( +

{ _t( "Safeguard against losing access to encrypted messages & data by " + "backing up encryption keys on your server.", - )}

+ ) }

- {optionKey} - {optionPassphrase} + { optionKey } + { optionPassphrase }
-
{_t("Enter your account password to confirm the upgrade:")}
+
{ _t("Enter your account password to confirm the upgrade:") }
; } else if (!this.state.backupSigStatus.usable) { authPrompt =
-
{_t("Restore your key backup to upgrade your encryption")}
+
{ _t("Restore your key backup to upgrade your encryption") }
; nextCaption = _t("Restore"); } else { authPrompt =

- {_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.") }

; } return -

{_t( +

{ _t( "Upgrade this session to allow it to verify other sessions, " + "granting them access to encrypted messages and marking them " + "as trusted for other users.", - )}

-
{authPrompt}
+ ) }

+
{ authPrompt }
; @@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhasePassPhrase() { return
-

{_t( +

{ _t( "Enter a security phrase only you know, as it’s used to safeguard your data. " + "To be secure, you shouldn’t re-use your account password.", - )}

+ ) }

{_t("Cancel")} + >{ _t("Cancel") } ; } @@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let passPhraseMatch = null; if (matchText) { passPhraseMatch =
-
{matchText}
+
{ matchText }
- {changeText} + { changeText }
; } return
-

{_t( +

{ _t( "Enter your Security Phrase a second time to confirm it.", - )}

+ ) }

- {passPhraseMatch} + { passPhraseMatch }
{_t("Skip")} + >{ _t("Skip") } ; } @@ -691,35 +691,36 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } return
-

{_t( +

{ _t( "Store your Security Key somewhere safe, like a password manager or a safe, " + "as it’s used to safeguard your encrypted data.", - )}

+ ) }

- {this._recoveryKey.encodedPrivateKey} + { this._recoveryKey.encodedPrivateKey }
- - {_t("Download")} + { _t("Download") } - {_t("or")} + { _t("or") } - {this.state.copied ? _t("Copied!") : _t("Copy")} + { this.state.copied ? _t("Copied!") : _t("Copy") }
- {continueButton} + { continueButton }
; } @@ -732,7 +733,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhaseLoadError() { return
-

{_t("Unable to query secret storage status")}

+

{ _t("Unable to query secret storage status") }

-

{_t( +

{ _t( "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", - )}

-

{_t( + ) }

+

{ _t( "You can also set up Secure Backup & manage your keys in Settings.", - )}

+ ) }

- +
; } @@ -787,7 +788,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { content =
-

{_t("Unable to set up secret storage")}

+

{ _t("Unable to set up secret storage") }

- {content} + { content }
); diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 0435d81968..dbed9f3968 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
-
@@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
-
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index 6017d07047..0936ad696d 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
- +
- )} + ) } ); } diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 2f37e60450..8c3d5e80a0 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -79,8 +79,8 @@ export default class CompleteSecurity extends React.Component {

- {icon} - {title} + { icon } + { title }

diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 6382e143f9..f978a6cded 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -101,7 +101,7 @@ export default class ForgotPassword extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase + // eslint-disable-next-line public UNSAFE_componentWillReceiveProps(newProps: IProps): void { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; @@ -239,14 +239,14 @@ export default class ForgotPassword extends React.Component { }); serverDeadSection = (
- {this.state.serverDeadError} + { this.state.serverDeadError }
); } return
- {errorText} - {serverDeadSection} + { errorText } + { serverDeadSection } { autoComplete="new-password" />
- {_t( + { _t( 'A verification email will be sent to your inbox to confirm ' + 'setting your new password.', - )} + ) } { /> - {_t('Sign in instead')} + { _t('Sign in instead') }
; } @@ -312,23 +312,29 @@ export default class ForgotPassword extends React.Component { renderEmailSent() { return
- {_t("An email has been sent to %(emailAddress)s. Once you've followed the " + - "link it contains, click below.", { emailAddress: this.state.email })} + { _t("An email has been sent to %(emailAddress)s. Once you've followed the " + + "link it contains, click below.", { emailAddress: this.state.email }) }
-
; } renderDone() { return
-

{_t("Your password has been reset.")}

-

{_t( +

{ _t("Your password has been reset.") }

+

{ _t( "You have been logged out of all sessions and will no longer receive " + "push notifications. To re-enable notifications, sign in again on each " + "device.", - )}

- +
; } @@ -358,7 +364,7 @@ export default class ForgotPassword extends React.Component {

{ _t('Set a new password') }

- {resetPasswordJsx} + { resetPasswordJsx }
); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 9f12521a34..7a05d8c6c6 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -144,7 +144,7 @@ export default class LoginComponent extends React.PureComponent } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillMount() { this.initLoginLogic(this.props.serverConfig); } @@ -154,7 +154,7 @@ export default class LoginComponent extends React.PureComponent } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillReceiveProps(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; @@ -239,8 +239,8 @@ export default class LoginComponent extends React.PureComponent ); errorText = (
-
{errorTop}
-
{errorDetail}
+
{ errorTop }
+
{ errorDetail }
); } else if (error.httpStatus === 401 || error.httpStatus === 403) { @@ -251,10 +251,10 @@ export default class LoginComponent extends React.PureComponent
{ _t('Incorrect username and/or password.') }
- {_t( + { _t( 'Please note you are logging into the %(hs)s server, not matrix.org.', { hs: this.props.serverConfig.hsName }, - )} + ) }
); @@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent "Either use HTTPS or enable unsafe scripts.", {}, { 'a': (sub) => { - return { sub } @@ -565,7 +567,7 @@ export default class LoginComponent extends React.PureComponent }); serverDeadSection = (
- {this.state.serverDeadError} + { this.state.serverDeadError }
); } @@ -578,15 +580,15 @@ export default class LoginComponent extends React.PureComponent { this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
{ this.props.isSyncing &&
- {_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") }
}
; } else if (SettingsStore.getValue(UIFeature.Registration)) { footer = ( - {_t("New? Create account", {}, { + { _t("New? Create account", {}, { a: sub => { sub }, - })} + }) } ); } @@ -596,8 +598,8 @@ export default class LoginComponent extends React.PureComponent

- {_t('Sign in')} - {loader} + { _t('Sign in') } + { loader }

{ errorTextSection } { serverDeadSection } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 8d32981e57..2b97650d4b 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -141,7 +141,7 @@ export default class Registration extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillReceiveProps(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; @@ -290,8 +290,8 @@ export default class Registration extends React.Component { }, ); msg =
-

{errorTop}

-

{errorDetail}

+

{ errorTop }

+

{ errorDetail }

; } else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) { let msisdnAvailable = false; @@ -482,13 +482,13 @@ export default class Registration extends React.Component { fragmentAfterLogin={this.props.fragmentAfterLogin} />

- {_t( + { _t( "%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: "", }, - ).trim()} + ).trim() }

; } @@ -526,15 +526,15 @@ export default class Registration extends React.Component { }); serverDeadSection = (
- {this.state.serverDeadError} + { this.state.serverDeadError }
); } const signIn = - {_t("Already have an account? Sign in here", {}, { + { _t("Already have an account? Sign in here", {}, { a: sub => { sub }, - })} + }) } ; // Only show the 'go back' button if you're not looking at the form @@ -550,43 +550,47 @@ export default class Registration extends React.Component { let regDoneText; if (this.state.differentLoggedInUserId) { regDoneText =
-

{_t( +

{ _t( "Your new account (%(newAccountId)s) is registered, but you're already " + "logged into a different account (%(loggedInUserId)s).", { newAccountId: this.state.registeredUsername, loggedInUserId: this.state.differentLoggedInUserId, }, - )}

-

{ - const sessionLoaded = await this.onLoginClickWithCheck(event); - if (sessionLoaded) { - dis.dispatch({ action: "view_welcome_page" }); - } - }}> - {_t("Continue with previous account")} + ) }

+

{ + const sessionLoaded = await this.onLoginClickWithCheck(event); + if (sessionLoaded) { + dis.dispatch({ action: "view_welcome_page" }); + } + }} + > + { _t("Continue with previous account") }

; } else if (this.state.formVals.password) { // We're the client that started the registration - regDoneText =

{_t( + regDoneText =

{ _t( "Log in to your new account.", {}, { - a: (sub) => {sub}, + a: (sub) => { sub }, }, - )}

; + ) }; } else { // 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 // as we don't have the original creds. - regDoneText =

{_t( + regDoneText =

{ _t( "You can now close this window or log in to your new account.", {}, { - a: (sub) => {sub}, + a: (sub) => { sub }, }, - )}

; + ) }; } body =
-

{_t("Registration Successful")}

+

{ _t("Registration Successful") }

{ regDoneText }
; } else { diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index c7ce74077b..6731156807 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -152,7 +152,7 @@ export default class SetupEncryptionBody extends React.Component let useRecoveryKeyButton; if (recoveryKeyPrompt) { useRecoveryKeyButton = - {recoveryKeyPrompt} + { recoveryKeyPrompt } ; } @@ -165,15 +165,15 @@ export default class SetupEncryptionBody extends React.Component return (
-

{_t( +

{ _t( "Verify your identity to access encrypted messages and prove your identity to others.", - )}

+ ) }

- {verifyButton} - {useRecoveryKeyButton} + { verifyButton } + { useRecoveryKeyButton } - {_t("Skip")} + { _t("Skip") }
@@ -181,25 +181,25 @@ export default class SetupEncryptionBody extends React.Component } else if (phase === Phase.Done) { let message; if (this.state.backupInfo) { - message =

{_t( + message =

{ _t( "Your new session is now verified. It has access to your " + "encrypted messages, and other users will see it as trusted.", - )}

; + ) }

; } else { - message =

{_t( + message =

{ _t( "Your new session is now verified. Other users will see it as trusted.", - )}

; + ) }

; } return (
- {message} + { message }
- {_t("Done")} + { _t("Done") }
@@ -207,23 +207,23 @@ export default class SetupEncryptionBody extends React.Component } else if (phase === Phase.ConfirmSkip) { return (
-

{_t( +

{ _t( "Without verifying, you won’t have access to all your messages " + "and may appear as untrusted to others.", - )}

+ ) }

- {_t("Skip")} + { _t("Skip") } - {_t("Go Back")} + { _t("Go Back") }
diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index d232f55dd1..fffec949fe 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -219,7 +219,7 @@ export default class SoftLogout extends React.Component { if (this.state.loginView === LOGIN_VIEW.PASSWORD) { let error = null; if (this.state.errorText) { - error = {this.state.errorText}; + error = { this.state.errorText }; } if (!introText) { @@ -228,8 +228,8 @@ export default class SoftLogout extends React.Component { return (
-

{introText}

- {error} +

{ introText }

+ { error } { type="submit" disabled={this.state.busy} > - {_t("Sign In")} + { _t("Sign In") } - {_t("Forgotten your password?")} + { _t("Forgotten your password?") } ); @@ -262,7 +262,7 @@ export default class SoftLogout extends React.Component { return (
-

{introText}

+

{ introText }

{ // Default: assume unsupported/error return (

- {_t( + { _t( "You cannot sign in to your account. Please contact your " + "homeserver admin for more information.", - )} + ) }

); } @@ -291,25 +291,25 @@ export default class SoftLogout extends React.Component {

- {_t("You're signed out")} + { _t("You're signed out") }

-

{_t("Sign in")}

+

{ _t("Sign in") }

- {this.renderSignInSection()} + { this.renderSignInSection() }
-

{_t("Clear personal data")}

+

{ _t("Clear personal data") }

- {_t( + { _t( "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 to another account.", - )} + ) }

- {_t("Clear all data")} + { _t("Clear all data") }
diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 66efa64658..b83f89fe5b 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { createRef, ReactNode, RefObject } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlayPauseButton from "./PlayPauseButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { formatBytes } from "../../../utils/FormattingUtils"; @@ -25,44 +23,13 @@ import { Key } from "../../../Keyboard"; import { _t } from "../../../languageHandler"; import SeekBar from "./SeekBar"; import PlaybackClock from "./PlaybackClock"; - -interface IProps { - // Playback instance to render. Cannot change during component lifecycle: create - // an all-new component instead. - playback: Playback; - - mediaName: string; -} - -interface IState { - playbackPhase: PlaybackState; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.AudioPlayer") -export default class AudioPlayer extends React.PureComponent { +export default class AudioPlayer extends AudioPlayerBase { private playPauseRef: RefObject = createRef(); private seekRef: RefObject = createRef(); - constructor(props: IProps) { - super(props); - - this.state = { - playbackPhase: PlaybackState.Decoding, // default assumption - }; - - // We don't need to de-register: the class handles this for us internally - this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); - - // Don't wait for the promise to complete - it will emit a progress update when it - // is done, and it's not meant to take long anyhow. - // noinspection JSIgnoredPromiseFromCall - this.props.playback.prepare(); - } - - private onPlaybackUpdate = (ev: PlaybackState) => { - this.setState({ playbackPhase: ev }); - }; - private onKeyDown = (ev: React.KeyboardEvent) => { // stopPropagation() prevents the FocusComposer catch-all from triggering, // but we need to do it on key down instead of press (even though the user @@ -88,37 +55,39 @@ export default class AudioPlayer extends React.PureComponent { return `(${formatBytes(bytes)})`; } - public render(): ReactNode { + protected renderComponent(): ReactNode { // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // events for accessibility - return
-
- -
- - {this.props.mediaName || _t("Unnamed audio")} - -
- -   {/* easiest way to introduce a gap between the components */} - { this.renderFileSize() } + return ( +
+
+ +
+ + { this.props.mediaName || _t("Unnamed audio") } + +
+ +   { /* easiest way to introduce a gap between the components */ } + { this.renderFileSize() } +
+
+ + +
-
- - -
-
; + ); } } diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx new file mode 100644 index 0000000000..d8fc9d507f --- /dev/null +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -0,0 +1,70 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Playback, PlaybackState } from "../../../audio/Playback"; +import { TileShape } from "../rooms/EventTile"; +import React, { ReactNode } from "react"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { _t } from "../../../languageHandler"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; + + mediaName?: string; + tileShape?: TileShape; +} + +interface IState { + playbackPhase: PlaybackState; + error?: boolean; +} + +@replaceableComponent("views.audio_messages.AudioPlayerBase") +export default abstract class AudioPlayerBase extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = { + playbackPhase: PlaybackState.Decoding, // default assumption + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + + // Don't wait for the promise to complete - it will emit a progress update when it + // is done, and it's not meant to take long anyhow. + this.props.playback.prepare().catch(e => { + console.error("Error processing audio file:", e); + this.setState({ error: true }); + }); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({ playbackPhase: ev }); + }; + + protected abstract renderComponent(): ReactNode; + + public render(): ReactNode { + return <> + { this.renderComponent() } + { this.state.error &&
{ _t("Error downloading audio") }
} + ; + } +} diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index 7f387715f8..cb1a179f2e 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -43,6 +43,6 @@ export default class Clock extends React.Component { public render() { 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 - return {minutes}:{seconds}; + return { minutes }:{ seconds }; } } diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 81852b5944..15bc6c98a4 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback } from "../../../voice/Playback"; +import { Playback } from "../../../audio/Playback"; interface IProps { playback: Playback; diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index a9dbd3c52f..e7330efc1d 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index b9c5f80f05..9c33889884 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arrayFastResample } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index a4f1e770f2..de2822cc39 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import classNames from "classnames"; // omitted props are handled by render function diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 374d47c31d..affb025d86 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index ea1b846c01..96fd3f5ae2 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -18,7 +18,7 @@ import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; -import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; +import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback"; import { percentageOf } from "../../../utils/numbers"; interface IProps { diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index 7d9312f369..e3f612c9b6 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -14,61 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { ReactNode } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { TileShape } from "../rooms/EventTile"; import PlaybackWaveform from "./PlaybackWaveform"; - -interface IProps { - // Playback instance to render. Cannot change during component lifecycle: create - // an all-new component instead. - playback: Playback; - - tileShape?: TileShape; -} - -interface IState { - playbackPhase: PlaybackState; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.RecordingPlayback") -export default class RecordingPlayback extends React.PureComponent { - constructor(props: IProps) { - super(props); - - this.state = { - playbackPhase: PlaybackState.Decoding, // default assumption - }; - - // We don't need to de-register: the class handles this for us internally - this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); - - // Don't wait for the promise to complete - it will emit a progress update when it - // is done, and it's not meant to take long anyhow. - // noinspection JSIgnoredPromiseFromCall - this.props.playback.prepare(); - } - +export default class RecordingPlayback extends AudioPlayerBase { private get isWaveformable(): boolean { return this.props.tileShape !== TileShape.Notif && this.props.tileShape !== TileShape.FileGrid && this.props.tileShape !== TileShape.Pinned; } - private onPlaybackUpdate = (ev: PlaybackState) => { - this.setState({ playbackPhase: ev }); - }; - - public render(): ReactNode { + protected renderComponent(): ReactNode { const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : ''; - return
- - - { this.isWaveformable && } -
; + return ( +
+ + + { this.isWaveformable && } +
+ ); } } diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 5231a2fb79..f0c03bb032 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 3b7a881754..4e44abdf46 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -47,17 +47,21 @@ export default class Waveform extends React.PureComponent { public render() { return
- {this.props.relHeights.map((h, i) => { + { this.props.relHeights.map((h, i) => { const progress = this.props.progress; const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0; const classes = classNames({ 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; - })} + return ; + }) }
; } } diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.tsx similarity index 95% rename from src/components/views/auth/AuthBody.js rename to src/components/views/auth/AuthBody.tsx index abe7fd2fd3..3543a573d7 100644 --- a/src/components/views/auth/AuthBody.js +++ b/src/components/views/auth/AuthBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthBody") export default class AuthBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
{ this.props.children }
; diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.tsx similarity index 96% rename from src/components/views/auth/AuthFooter.js rename to src/components/views/auth/AuthFooter.tsx index e81d2cd969..00bced8c39 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.tsx @@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthFooter") export default class AuthFooter extends React.Component { - render() { + public render(): React.ReactNode { return (
{ _t("powered by Matrix") } diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.tsx similarity index 71% rename from src/components/views/auth/AuthHeader.js rename to src/components/views/auth/AuthHeader.tsx index d9bd81adcb..cab7da1468 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.tsx @@ -16,20 +16,17 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthHeaderLogo from "./AuthHeaderLogo"; +import LanguageSelector from "./LanguageSelector"; + +interface IProps { + disableLanguageSelector?: boolean; +} @replaceableComponent("views.auth.AuthHeader") -export default class AuthHeader extends React.Component { - static propTypes = { - disableLanguageSelector: PropTypes.bool, - }; - - render() { - const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); - const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); - +export default class AuthHeader extends React.Component { + public render(): React.ReactNode { return (
diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.tsx similarity index 95% rename from src/components/views/auth/AuthHeaderLogo.js rename to src/components/views/auth/AuthHeaderLogo.tsx index 0adf18dc1c..b6724793a5 100644 --- a/src/components/views/auth/AuthHeaderLogo.js +++ b/src/components/views/auth/AuthHeaderLogo.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthHeaderLogo") export default class AuthHeaderLogo extends React.PureComponent { - render() { + public render(): React.ReactNode { return
Matrix
; diff --git a/src/components/views/auth/AuthPage.js b/src/components/views/auth/AuthPage.tsx similarity index 86% rename from src/components/views/auth/AuthPage.js rename to src/components/views/auth/AuthPage.tsx index 6ba47e5288..c402d5b699 100644 --- a/src/components/views/auth/AuthPage.js +++ b/src/components/views/auth/AuthPage.tsx @@ -17,18 +17,16 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthFooter from "./AuthFooter"; @replaceableComponent("views.auth.AuthPage") export default class AuthPage extends React.PureComponent { - render() { - const AuthFooter = sdk.getComponent('auth.AuthFooter'); - + public render(): React.ReactNode { return (
- {this.props.children} + { this.props.children }
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.tsx similarity index 66% rename from src/components/views/auth/CaptchaForm.js rename to src/components/views/auth/CaptchaForm.tsx index bea4f89f53..b1c09f2b22 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.tsx @@ -15,66 +15,74 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; 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. */ @replaceableComponent("views.auth.CaptchaForm") -export default class CaptchaForm extends React.Component { - static propTypes = { - sitePublicKey: PropTypes.string, - - // called with the captcha response - onCaptchaResponse: PropTypes.func, - }; - +export default class CaptchaForm extends React.Component { static defaultProps = { onCaptchaResponse: () => {}, }; - constructor(props) { + private captchaWidgetId?: string; + private recaptchaContainer = createRef(); + + constructor(props: ICaptchaFormProps) { super(props); this.state = { - errorText: null, + errorText: undefined, }; - this._captchaWidgetId = null; - - this._recaptchaContainer = createRef(); - CountlyAnalytics.instance.track("onboarding_grecaptcha_begin"); } componentDidMount() { // Just putting a script tag into the returned jsx doesn't work, annoyingly, // so we do this instead. - if (global.grecaptcha) { + if (this.isRecaptchaReady()) { // already loaded - this._onCaptchaLoaded(); + this.onCaptchaLoaded(); } else { console.log("Loading recaptcha script..."); - window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; + window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); }; const scriptTag = document.createElement('script'); 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() { - this._resetRecaptcha(); + this.resetRecaptcha(); } - _renderRecaptcha(divId) { - if (!global.grecaptcha) { + // Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba + 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!"); 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!"); throw new Error( "This server has not supplied enough information for Recaptcha " - + "authentication"); + + "authentication"); } console.info("Rendering to %s", divId); - this._captchaWidgetId = global.grecaptcha.render(divId, { + this.captchaWidgetId = global.grecaptcha.render(divId, { sitekey: publicKey, callback: this.props.onCaptchaResponse, }); } - _resetRecaptcha() { - if (this._captchaWidgetId !== null) { - global.grecaptcha.reset(this._captchaWidgetId); + private resetRecaptcha() { + if (this.captchaWidgetId !== null) { + global.grecaptcha.reset(this.captchaWidgetId); } } - _onCaptchaLoaded() { + private onCaptchaLoaded() { console.log("Loaded recaptcha script."); try { - this._renderRecaptcha(DIV_ID); + this.renderRecaptcha(DIV_ID); // clear error if re-rendered this.setState({ errorText: null, @@ -128,10 +136,10 @@ export default class CaptchaForm extends React.Component { } return ( -
-

{_t( +

+

{ _t( "This homeserver would like to make sure you are not a robot.", - )}

+ ) }

{ error }
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.tsx similarity index 95% rename from src/components/views/auth/CompleteSecurityBody.js rename to src/components/views/auth/CompleteSecurityBody.tsx index 745d7abbf2..8f6affb64e 100644 --- a/src/components/views/auth/CompleteSecurityBody.js +++ b/src/components/views/auth/CompleteSecurityBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.CompleteSecurityBody") export default class CompleteSecurityBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
{ this.props.children }
; diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.tsx similarity index 75% rename from src/components/views/auth/CountryDropdown.js rename to src/components/views/auth/CountryDropdown.tsx index cbc19e0f8d..eb5b27be9d 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.tsx @@ -15,21 +15,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; - -import { COUNTRIES, getEmojiFlag } from '../../../phonenumber'; +import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Dropdown from "../elements/Dropdown"; const COUNTRIES_BY_ISO2 = {}; for (const c of COUNTRIES) { 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) if (query[0] === '+') { query = query.slice(1); @@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) { return false; } -@replaceableComponent("views.auth.CountryDropdown") -export default class CountryDropdown extends React.Component { - constructor(props) { - super(props); - this._onSearchChange = this._onSearchChange.bind(this); - this._onOptionChange = this._onOptionChange.bind(this); - this._getShortOption = this._getShortOption.bind(this); +interface IProps { + value?: string; + onOptionChange: (country: PhoneNumberCountryDefinition) => void; + isSmall: boolean; // if isSmall, show +44 in the selected value + showPrefix: boolean; + className?: string; + disabled?: boolean; +} - let defaultCountry = COUNTRIES[0]; +interface IState { + searchQuery: string; + defaultCountry: PhoneNumberCountryDefinition; +} + +@replaceableComponent("views.auth.CountryDropdown") +export default class CountryDropdown extends React.Component { + constructor(props: IProps) { + super(props); + + let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0]; const defaultCountryCode = SdkConfig.get()["defaultCountryCode"]; if (defaultCountryCode) { 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 no value is given, we start with the default // 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({ searchQuery: search, }); - } + }; - _onOptionChange(iso2) { + private onOptionChange = (iso2: string): void => { this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); - } + }; - _flagImgForIso2(iso2) { + private flagImgForIso2(iso2: string): React.ReactNode { return
{ getEmojiFlag(iso2) }
; } - _getShortOption(iso2) { + private getShortOption = (iso2: string): React.ReactNode => { if (!this.props.isSmall) { return undefined; } @@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component { countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; } return - { this._flagImgForIso2(iso2) } + { this.flagImgForIso2(iso2) } { countryPrefix } ; - } - - render() { - const Dropdown = sdk.getComponent('elements.Dropdown'); + }; + public render(): React.ReactNode { let displayedCountries; if (this.state.searchQuery) { displayedCountries = COUNTRIES.filter( @@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
- { this._flagImgForIso2(country.iso2) } + { this.flagImgForIso2(country.iso2) } { _t(country.name) } (+{ country.prefix })
; }); @@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component { return ; } } - -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, -}; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index d9af2c2b77..d9db140645 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -416,13 +416,15 @@ export class TermsAuthEntry extends React.Component{_t("Accept")}; + submitButton = ; } return (
-

{_t("Please review and accept the policies of this homeserver:")}

+

{ _t("Please review and accept the policies of this homeserver:") }

{ checkboxes } { errorSection } { submitButton } @@ -613,15 +615,17 @@ export class MsisdnAuthEntry extends React.Component
- - {errorSection} + { errorSection }
); @@ -717,21 +721,21 @@ export class SSOAuthEntry extends React.Component{_t("Cancel")} + >{ _t("Cancel") } ); if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) { continueButton = ( {this.props.continueText || _t("Single Sign On")} + >{ this.props.continueText || _t("Single Sign On") } ); } else { continueButton = ( {this.props.continueText || _t("Confirm")} + >{ this.props.continueText || _t("Confirm") } ); } @@ -753,8 +757,8 @@ export class SSOAuthEntry extends React.Component { errorSection }
- {cancelButton} - {continueButton} + { cancelButton } + { continueButton }
; } @@ -825,7 +829,7 @@ export class FallbackAuthEntry extends React.Component { { _t("Start authentication") } - {errorSection} + { errorSection }
); } diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.tsx similarity index 85% rename from src/components/views/auth/LanguageSelector.js rename to src/components/views/auth/LanguageSelector.tsx index 88293310e7..c26b4797f3 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.tsx @@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig"; import { getCurrentLanguage } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; -import * as sdk from '../../../index'; import React from 'react'; import { SettingLevel } from "../../../settings/SettingLevel"; +import LanguageDropdown from "../elements/LanguageDropdown"; -function onChange(newLang) { +function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } } -export default function LanguageSelector({ disabled }) { - if (SdkConfig.get()['disable_login_language_selector']) return
; +interface IProps { + 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
; return { kind="link" onClick={this.onForgotPasswordClick} > - {_t("Forgot password?")} + { _t("Forgot password?") } ; } @@ -441,16 +441,16 @@ export default class PasswordLogin extends React.PureComponent { disabled={this.props.disableSubmit} >
@@ -460,8 +460,8 @@ export default class PasswordLogin extends React.PureComponent { return (
- {loginType} - {loginField} + { loginType } + { loginField } { onValidate={this.onPasswordValidate} ref={field => this[LoginField.Password] = field} /> - {forgotPasswordJsx} + { forgotPasswordJsx } { !this.props.busy &&
- {this.renderUsername()} + { this.renderUsername() }
- {this.renderPassword()} - {this.renderPasswordConfirm()} + { this.renderPassword() } + { this.renderPasswordConfirm() }
- {this.renderEmail()} - {this.renderPhoneNumber()} + { this.renderEmail() } + { this.renderPhoneNumber() }
{ emailHelperText } { registerButton } diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.tsx similarity index 83% rename from src/components/views/auth/Welcome.js rename to src/components/views/auth/Welcome.tsx index e3f7a601f2..0e12025fbd 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import classNames from "classnames"; -import * as sdk from '../../../index'; +import * as sdk from "../../../index"; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; import { _td } from "../../../languageHandler"; @@ -25,21 +25,26 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import LanguageSelector from "./LanguageSelector"; // translatable strings for Welcome pages _td("Sign in with SSO"); +interface IProps { + +} + @replaceableComponent("views.auth.Welcome") -export default class Welcome extends React.PureComponent { - constructor(props) { +export default class Welcome extends React.PureComponent { + constructor(props: IProps) { super(props); CountlyAnalytics.instance.track("onboarding_welcome"); } - render() { - const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); - const LanguageSelector = sdk.getComponent('auth.LanguageSelector'); + public render(): React.ReactNode { + // FIXME: Using an import will result in wrench-element-tests failures + const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage"); const pagesConfig = SdkConfig.get().embeddedPages; let pageUrl = null; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 87cdbe7512..6aaef29854 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt={_t("Avatar")} + title={title} + alt={_t("Avatar")} inputRef={inputRef} {...otherProps} /> ); @@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt="" + title={title} + alt="" ref={inputRef} {...otherProps} /> ); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5e6bf45f07..99f2b70efc 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -205,8 +205,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent - {icon} - {badge} + { icon } + { badge }
; } } diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 61155e3880..11c24a5981 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component { } return ( - + ); } } diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index b8b23dc33e..82b7b8e400 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -145,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component { isExpanded={this.state.menuDisplayed} label={_t("User Status")} > - {avatar} + { avatar } { contextMenu } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index a07990c3bb..f285222f7b 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + import React, { ComponentProps } from 'react'; import { Room } from 'matrix-js-sdk/src/models/room'; import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; +import classNames from "classnames"; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -32,11 +34,14 @@ interface IProps extends Omit, "name" | "idNam // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData; + oobData?: IOOBData & { + roomId?: string; + }; width?: number; height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + className?: string; onClick?(): void; } @@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component { }; public render() { - const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; + const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; const roomName = room ? room.name : oobData.name; // If the room is a DM, we use the other user's ID for the color hash // in order to match the room avatar with their avatar - const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null; + const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId; return ( - { let transferItem; if (this.props.call.opponentCanBeTransferred()) { transferItem = - {_t("Transfer")} + { _t("Transfer") } ; } return - {holdUnholdCaption} + { holdUnholdCaption } - {transferItem} + { transferItem } ; } } diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 39dfd50795..aead3a266e 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -60,8 +60,10 @@ export default class DialpadContextMenu extends React.Component
-
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index a9c75bf3ba..1d822fd246 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -64,8 +64,8 @@ export const IconizedContextMenuRadio: React.FC = ({ label={label} > - {label} - {active && } + { label } + { active && } ; }; @@ -85,15 +85,15 @@ export const IconizedContextMenuCheckbox: React.FC = ({ label={label} > - {label} - {active && } + { label } + { active && } ; }; export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, ...props }) => { return { iconClassName && } - {label} + { label } ; }; @@ -104,7 +104,7 @@ export const IconizedContextMenuOptionList: React.FC = ({ firs }); return
- {children} + { children }
; }; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 999e98f4ad..8f5d3baa17 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -interface IEventTileOps { +export interface IEventTileOps { isWidgetHidden(): boolean; unhideWidget(): void; } +export interface IOperableEventTile { + getEventTileOps(): IEventTileOps; +} + interface IProps { /* the MatrixEvent associated with the context menu */ mxEvent: MatrixEvent; @@ -268,7 +272,7 @@ export default class MessageContextMenu extends React.Component resendReactionsButton = ( ); @@ -298,7 +302,7 @@ export default class MessageContextMenu extends React.Component pinButton = ( ); @@ -333,7 +337,7 @@ export default class MessageContextMenu extends React.Component - {_t("Clear status")} + { _t("Clear status") } ; } else { actionButton = - {_t("Update status")} + { _t("Update status") } ; } } else { - actionButton = - {_t("Set status")} + { _t("Set status") } ; } @@ -121,17 +123,24 @@ export default class StatusMessageContextMenu extends React.Component { spinner = ; } - const form = -
- {actionButton} - {spinner} + { actionButton } + { spinner }
; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index b21efdceb9..26d7b640a4 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC = ({ onFinished(); }; streamAudioStreamButton = ; } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index baebe44ec3..7f4b365a8b 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { sleep } from "matrix-js-sdk/src/utils"; import { _t } from '../../../languageHandler'; @@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; interface IProps extends IDialogProps { - matrixClient: MatrixClient; space: Room; - onCreateRoomClick(cli: MatrixClient, space: Room): void; + onCreateRoomClick(space: Room): void; } export const Entry = ({ room, checked, onChange }) => { @@ -211,17 +209,23 @@ export const AddExistingToSpace: React.FC = ({ function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } return
= ({
; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished }) => { const [selectedSpace, setSelectedSpace] = useState(space); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); @@ -344,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onFinished={onFinished} fixedWidth={false} > - +
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(cli, space)} kind="link"> + onCreateRoomClick(space)} kind="link"> { _t("Create a new room") } } diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.tsx similarity index 77% rename from src/components/views/dialogs/AddressPickerDialog.js rename to src/components/views/dialogs/AddressPickerDialog.tsx index 09714e24e3..6b239ee570 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.tsx @@ -18,14 +18,12 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { sleep } from "matrix-js-sdk/src/utils"; import { _t, _td } from '../../../languageHandler'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import { addressTypes, getAddressType } from '../../../UserAddress'; +import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; @@ -34,6 +32,10 @@ import { abbreviateUrl } from '../../../utils/UrlUtils'; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AddressSelector from '../elements/AddressSelector'; +import AddressTile from '../elements/AddressTile'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -44,29 +46,64 @@ const addressTypeName = { 'email': _td("email address"), }; -@replaceableComponent("views.dialogs.AddressPickerDialog") -export default class AddressPickerDialog extends React.Component { - static propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.node, - // Extra node inserted after picker input, dropdown and errors - extraNode: PropTypes.node, - value: PropTypes.string, - placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - roomId: PropTypes.string, - button: PropTypes.string, - focus: PropTypes.bool, - validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)), - onFinished: PropTypes.func.isRequired, - groupId: PropTypes.string, - // The type of entity to search for. Default: 'user'. - pickerType: PropTypes.oneOf(['user', 'room']), - // Whether the current user should be included in the addresses returned. Only - // applicable when pickerType is `user`. Default: false. - includeSelf: PropTypes.bool, - }; +interface IResult { + user_id: string; // eslint-disable-line camelcase + room_id?: string; // eslint-disable-line camelcase + name?: string; + display_name?: string; // eslint-disable-line camelcase + avatar_url?: string;// eslint-disable-line camelcase +} - static defaultProps = { +interface IProps { + title: string; + description?: JSX.Element; + // Extra node inserted after picker input, dropdown and errors + extraNode?: JSX.Element; + value?: string; + placeholder?: ((validAddressTypes: any) => string) | string; + roomId?: string; + button?: string; + focus?: boolean; + validAddressTypes?: AddressType[]; + onFinished: (success: boolean, list?: IUserAddress[]) => void; + groupId?: string; + // The type of entity to search for. Default: 'user'. + pickerType?: 'user' | 'room'; + // Whether the current user should be included in the addresses returned. Only + // applicable when pickerType is `user`. Default: false. + includeSelf?: boolean; +} + +interface IState { + // Whether to show an error message because of an invalid address + invalidAddressError: boolean; + // List of UserAddressType objects representing + // the list of addresses we're going to invite + selectedList: IUserAddress[]; + // Whether a search is ongoing + busy: boolean; + // An error message generated during the user directory search + searchError: string; + // Whether the server supports the user_directory API + serverSupportsUserDirectory: boolean; + // The query being searched for + query: string; + // List of UserAddressType objects representing the set of + // auto-completion results for the current search query. + suggestedList: IUserAddress[]; + // List of address types initialised from props, but may change while the + // dialog is open and represents the supported list of address types at this time. + validAddressTypes: AddressType[]; +} + +@replaceableComponent("views.dialogs.AddressPickerDialog") +export default class AddressPickerDialog extends React.Component { + private textinput = createRef(); + private addressSelector = createRef(); + private queryChangedDebouncer: number; + private cancelThreepidLookup: () => void; + + static defaultProps: Partial = { value: "", focus: true, validAddressTypes: addressTypes, @@ -74,36 +111,23 @@ export default class AddressPickerDialog extends React.Component { includeSelf: false, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._textinput = createRef(); - let validAddressTypes = this.props.validAddressTypes; // Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user - if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) { - validAddressTypes = validAddressTypes.filter(type => type !== "email"); + if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) { + validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email); } this.state = { - // Whether to show an error message because of an invalid address invalidAddressError: false, - // List of UserAddressType objects representing - // the list of addresses we're going to invite selectedList: [], - // Whether a search is ongoing busy: false, - // An error message generated during the user directory search searchError: null, - // Whether the server supports the user_directory API serverSupportsUserDirectory: true, - // The query being searched for query: "", - // List of UserAddressType objects representing the set of - // auto-completion results for the current search query. suggestedList: [], - // List of address types initialised from props, but may change while the - // dialog is open and represents the supported list of address types at this time. validAddressTypes, }; } @@ -111,11 +135,11 @@ export default class AddressPickerDialog extends React.Component { componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input - this._textinput.current.value = this.props.value; + this.textinput.current.value = this.props.value; } } - getPlaceholder() { + private getPlaceholder(): string { const { placeholder } = this.props; if (typeof placeholder === "string") { return placeholder; @@ -124,23 +148,23 @@ export default class AddressPickerDialog extends React.Component { return placeholder(this.state.validAddressTypes); } - onButtonClick = () => { + private onButtonClick = (): void => { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local selectedList - if (this._textinput.current.value !== '') { - selectedList = this._addAddressesToList([this._textinput.current.value]); + if (this.textinput.current.value !== '') { + selectedList = this.addAddressesToList([this.textinput.current.value]); if (selectedList === null) return; } this.props.onFinished(true, selectedList); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - onKeyDown = e => { - const textInput = this._textinput.current ? this._textinput.current.value : undefined; + private onKeyDown = (e: React.KeyboardEvent): void => { + const textInput = this.textinput.current ? this.textinput.current.value : undefined; if (e.key === Key.ESCAPE) { e.stopPropagation(); @@ -149,15 +173,15 @@ export default class AddressPickerDialog extends React.Component { } else if (e.key === Key.ARROW_UP) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionUp(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp(); } else if (e.key === Key.ARROW_DOWN) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionDown(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown(); } else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.chooseSelection(); + if (this.addressSelector.current) this.addressSelector.current.chooseSelection(); } else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) { e.stopPropagation(); e.preventDefault(); @@ -169,17 +193,17 @@ export default class AddressPickerDialog extends React.Component { // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } } else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) { e.stopPropagation(); e.preventDefault(); - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } }; - onQueryChanged = ev => { - const query = ev.target.value; + private onQueryChanged = (ev: React.ChangeEvent): void => { + const query = (ev.target as HTMLTextAreaElement).value; if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); } @@ -188,17 +212,17 @@ export default class AddressPickerDialog extends React.Component { this.queryChangedDebouncer = setTimeout(() => { if (this.props.pickerType === 'user') { if (this.props.groupId) { - this._doNaiveGroupSearch(query); + this.doNaiveGroupSearch(query); } else if (this.state.serverSupportsUserDirectory) { - this._doUserDirectorySearch(query); + this.doUserDirectorySearch(query); } else { - this._doLocalSearch(query); + this.doLocalSearch(query); } } else if (this.props.pickerType === 'room') { if (this.props.groupId) { - this._doNaiveGroupRoomSearch(query); + this.doNaiveGroupRoomSearch(query); } else { - this._doRoomSearch(query); + this.doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -213,7 +237,7 @@ export default class AddressPickerDialog extends React.Component { } }; - onDismissed = index => () => { + private onDismissed = (index: number) => () => { const selectedList = this.state.selectedList.slice(); selectedList.splice(index, 1); this.setState({ @@ -221,25 +245,21 @@ export default class AddressPickerDialog extends React.Component { suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - onClick = index => () => { - this.onSelected(index); - }; - - onSelected = index => { + private onSelected = (index: number): void => { const selectedList = this.state.selectedList.slice(); - selectedList.push(this._getFilteredSuggestions()[index]); + selectedList.push(this.getFilteredSuggestions()[index]); this.setState({ selectedList, suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - _doNaiveGroupSearch(query) { + private doNaiveGroupSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); this.setState({ busy: true, @@ -260,7 +280,7 @@ export default class AddressPickerDialog extends React.Component { display_name: u.displayname, }); }); - this._processResults(results, query); + this.processResults(results, query); }).catch((err) => { console.error('Error whilst searching group rooms: ', err); this.setState({ @@ -273,7 +293,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doNaiveGroupRoomSearch(query) { + private doNaiveGroupRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const results = []; GroupStore.getGroupRooms(this.props.groupId).forEach((r) => { @@ -289,13 +309,13 @@ export default class AddressPickerDialog extends React.Component { name: r.name || r.canonical_alias, }); }); - this._processResults(results, query); + this.processResults(results, query); this.setState({ busy: false, }); } - _doRoomSearch(query) { + private doRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const rooms = MatrixClientPeg.get().getRooms(); const results = []; @@ -346,13 +366,13 @@ export default class AddressPickerDialog extends React.Component { return a.rank - b.rank; }); - this._processResults(sortedResults, query); + this.processResults(sortedResults, query); this.setState({ busy: false, }); } - _doUserDirectorySearch(query) { + private doUserDirectorySearch(query: string): void { this.setState({ busy: true, query, @@ -366,7 +386,7 @@ export default class AddressPickerDialog extends React.Component { if (this.state.query !== query) { return; } - this._processResults(resp.results, query); + this.processResults(resp.results, query); }).catch((err) => { console.error('Error whilst searching user directory: ', err); this.setState({ @@ -377,7 +397,7 @@ export default class AddressPickerDialog extends React.Component { serverSupportsUserDirectory: false, }); // Do a local search immediately - this._doLocalSearch(query); + this.doLocalSearch(query); } }).then(() => { this.setState({ @@ -386,7 +406,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doLocalSearch(query) { + private doLocalSearch(query: string): void { this.setState({ query, searchError: null, @@ -407,10 +427,10 @@ export default class AddressPickerDialog extends React.Component { avatar_url: user.avatarUrl, }); }); - this._processResults(results, query); + this.processResults(results, query); } - _processResults(results, query) { + private processResults(results: IResult[], query: string): void { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -465,27 +485,27 @@ export default class AddressPickerDialog extends React.Component { address: query, isKnown: false, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); if (addrType === 'email') { - this._lookupThreepid(addrType, query); + this.lookupThreepid(addrType, query); } } this.setState({ suggestedList, invalidAddressError: false, }, () => { - if (this.addressSelector) this.addressSelector.moveSelectionTop(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop(); }); } - _addAddressesToList(addressTexts) { + private addAddressesToList(addressTexts: string[]): IUserAddress[] { const selectedList = this.state.selectedList.slice(); let hasError = false; addressTexts.forEach((addressText) => { addressText = addressText.trim(); const addrType = getAddressType(addressText); - const addrObj = { + const addrObj: IUserAddress = { addressType: addrType, address: addressText, isKnown: false, @@ -504,7 +524,6 @@ export default class AddressPickerDialog extends React.Component { const room = MatrixClientPeg.get().getRoom(addrObj.address); if (room) { addrObj.displayName = room.name; - addrObj.avatarMxc = room.avatarUrl; addrObj.isKnown = true; } } @@ -518,17 +537,17 @@ export default class AddressPickerDialog extends React.Component { query: "", invalidAddressError: hasError ? true : this.state.invalidAddressError, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); return hasError ? null : selectedList; } - async _lookupThreepid(medium, address) { + private async lookupThreepid(medium: AddressType, address: string): Promise { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just // leave it: it's replacing the old one each time so it's // not like they leak. - this._cancelThreepidLookup = function() { + this.cancelThreepidLookup = function() { cancelled = true; }; @@ -570,7 +589,7 @@ export default class AddressPickerDialog extends React.Component { } } - _getFilteredSuggestions() { + private getFilteredSuggestions(): IUserAddress[] { // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({ address, addressType }) => { @@ -584,15 +603,15 @@ export default class AddressPickerDialog extends React.Component { }); } - _onPaste = e => { + private onPaste = (e: React.ClipboardEvent): void => { // Prevent the text being pasted into the textarea e.preventDefault(); const text = e.clipboardData.getData("text"); // Process it as a list of addresses to add instead - this._addAddressesToList(text.split(/[\s,]+/)); + this.addAddressesToList(text.split(/[\s,]+/)); }; - onUseDefaultIdentityServerClick = e => { + private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -601,33 +620,27 @@ export default class AddressPickerDialog extends React.Component { // Add email as a valid address type. const { validAddressTypes } = this.state; - validAddressTypes.push('email'); + validAddressTypes.push(AddressType.Email); this.setState({ validAddressTypes }); }; - onManageSettingsClick = e => { + private onManageSettingsClick = (e: React.MouseEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AddressSelector = sdk.getComponent("elements.AddressSelector"); - this.scrollElement = null; - let inputLabel; if (this.props.description) { inputLabel =
- +
; } const query = []; // create the invite list if (this.state.selectedList.length > 0) { - const AddressTile = sdk.getComponent("elements.AddressTile"); for (let i = 0; i < this.state.selectedList.length; i++) { query.push( - , + autoFocus={this.props.focus} + />, ); - const filteredSuggestedList = this._getFilteredSuggestions(); + const filteredSuggestedList = this.getFilteredSuggestions(); let error; let addressSelector; @@ -675,7 +688,7 @@ export default class AddressPickerDialog extends React.Component { error =
{ _t("No results") }
; } else { addressSelector = ( - {this.addressSelector = ref;}} + {_t( + identityServer =
{ _t( "Use an identity server to invite by email. " + "Use the default (%(defaultIdentityServerName)s) " + "or manage in Settings.", @@ -698,25 +711,29 @@ export default class AddressPickerDialog extends React.Component { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, { - default: sub => {sub}, - settings: sub => {sub}, + default: sub => { sub }, + settings: sub => { sub }, }, - )}
; + ) }
; } else { - identityServer =
{_t( + identityServer =
{ _t( "Use an identity server to invite by email. " + "Manage in Settings.", {}, { - settings: sub => {sub}, + settings: sub => { sub }, }, - )}
; + ) }
; } } return ( - - {inputLabel} + + { inputLabel }
{ query }
{ error } diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx index 26fad0c724..3ae82f1026 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -51,7 +51,7 @@ export default class AskInviteAnywayDialog extends React.Component { public render() { const errorList = this.props.unknownProfileUsers - .map(address =>
  • {address.userId}: {address.errorText}
  • ); + .map(address =>
  • { address.userId }: { address.errorText }
  • ); return ( { contentId='mx_Dialog_content' >
    - {/* eslint-disable-next-line */} -

    {_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}

    +

    { _t("Unable to find profiles for the Matrix IDs listed below - " + + "would you like to invite them anyway?") }

      { errorList }
    diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e92bd6315e..42b21ec743 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -118,9 +118,7 @@ export default class BaseDialog extends React.Component { let headerImage; if (this.props.headerImage) { - headerImage = ; + headerImage = ; } return ( @@ -149,7 +147,7 @@ export default class BaseDialog extends React.Component { 'mx_Dialog_headerWithCancel': !!cancelButton, })}>
    - {headerImage} + { headerImage } { this.props.title }
    { this.props.headerButton } diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 5a2f16f169..34218a3399 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -69,15 +69,18 @@ const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => {
    { _t(info.feedbackSubheading) }   - { _t("Your platform and username will be noted to help us use your feedback as much as we can.")} + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } - { - onFinished(false); - defaultDispatcher.dispatch({ + { + onFinished(false); + defaultDispatcher.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Labs, - }); - }}> + }); + }} + > { _t("To leave the beta, visit your settings.") }
    diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 6baf24f797..3df05dac6e 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -166,7 +166,7 @@ export default class BugReportDialog extends React.Component { let error = null; if (this.state.err) { error =
    - {this.state.err} + { this.state.err }
    ; } @@ -175,7 +175,7 @@ export default class BugReportDialog extends React.Component { progress = (
    - {this.state.progress} ... + { this.state.progress } ...
    ); } @@ -188,7 +188,9 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -221,7 +223,7 @@ export default class BugReportDialog extends React.Component { { _t("Download logs") } - {this.state.downloadProgress && {this.state.downloadProgress} ...} + { this.state.downloadProgress && { this.state.downloadProgress } ... }
    { "please include those things here.", )} /> - {progress} - {error} + { progress } + { error }
    { return (
  • - {commit.commit.message.split('\n')[0]} + { commit.commit.message.split('\n')[0] }
  • ); @@ -79,15 +79,15 @@ export default class ChangelogDialog extends React.Component { } return (
    -

    {repo}

    -
      {content}
    +

    { repo }

    +
      { content }
    ); }); const content = (
    - {this.props.version == null || this.props.newVersion == null ?

    {_t("Unavailable")}

    : logs} + { this.props.version == null || this.props.newVersion == null ?

    { _t("Unavailable") }

    : logs }
    ); diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index 7627489deb..6a8773ce45 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -156,8 +156,8 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< height={avatarSize} />
    - {person.user.name} - {person.userId} + { person.user.name } + { person.userId }
    this.setPersonToggle(person, e.target.checked)} />
    @@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< emailAddresses.push(( this.onAddressChange(e, emailAddresses.length)} label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} @@ -205,18 +205,21 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< people.push(( {_t("Show more")} + > + { _t("Show more") } + )); } } if (this.state.people.length > 0) { peopleIntro = (
    - {_t("People you know on %(brand)s", { brand: SdkConfig.get().brand })} + { _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) } - {this.state.showPeople ? _t("Hide") : _t("Show")} + { this.state.showPeople ? _t("Hide") : _t("Show") }
    ); @@ -236,14 +239,17 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< >
    - {emailAddresses} - {peopleIntro} - {people} + { emailAddresses } + { peopleIntro } + { people } {buttonText} + > + { buttonText } +
    diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index a2f2b10144..b346d2d44c 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -37,8 +37,8 @@ export default class ConfirmRedactDialog extends React.Component { "Note that if you delete a room name or topic change, it could undo the change.")} placeholder={_t("Reason (optional)")} focus - button={_t("Remove")}> - + button={_t("Remove")} + /> ); } } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index cbef474c69..7099556ac6 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -104,7 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component { } return ( - diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 544d0df1c9..2577d5456d 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -44,10 +44,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component { >

    - {_t( + { _t( "Clearing all data from this session is permanent. Encrypted messages will be lost " + "unless their keys have been backed up.", - )} + ) }

    - {_t("Community ID: +:%(domain)s", { + { _t("Community ID: +:%(domain)s", { domain: MatrixClientPeg.getHomeserverName(), }, { - localpart: () => {this.state.localpart}, - })} + localpart: () => { this.state.localpart }, + }) } - {_t("You can change this later if needed.")} + { _t("You can change this later if needed.") } ); if (this.state.error) { const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error"; helpText = ( - {this.state.error} + { this.state.error } ); } @@ -193,31 +193,33 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent< placeholder={_t("Enter name")} label={_t("Enter name")} /> - {helpText} + { helpText } - {/*nbsp is to reserve the height of this element when there's nothing*/} -  {communityId} + { /*nbsp is to reserve the height of this element when there's nothing*/ } +  { communityId } - {_t("Create")} + { _t("Create") }
    - {preview} + { preview }
    - {_t("Add image (optional)")} + { _t("Add image (optional)") } - {_t("An image will help people identify your community.")} + { _t("An image will help people identify your community.") }
    diff --git a/src/components/views/dialogs/CreateGroupDialog.tsx b/src/components/views/dialogs/CreateGroupDialog.tsx index d6bb582079..b1ea75d367 100644 --- a/src/components/views/dialogs/CreateGroupDialog.tsx +++ b/src/components/views/dialogs/CreateGroupDialog.tsx @@ -102,7 +102,7 @@ export default class CreateGroupDialog extends React.Component { }); }; - _onCancel = () => { + private onCancel = () => { this.props.onFinished(false); }; @@ -123,7 +123,9 @@ export default class CreateGroupDialog extends React.Component { } return ( -
    @@ -133,8 +135,11 @@ export default class CreateGroupDialog extends React.Component {
    - {
    -
    diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index b5c0096771..a06f508908 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; import SdkConfig from '../../../SdkConfig'; import withValidation, { IFieldState } from '../elements/Validation'; @@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; +import Dropdown from "../elements/Dropdown"; +import SpaceStore from "../../../stores/SpaceStore"; interface IProps { defaultPublic?: boolean; @@ -41,7 +43,7 @@ interface IProps { } interface IState { - isPublic: boolean; + joinRule: JoinRule; isEncrypted: boolean; name: string; topic: string; @@ -54,15 +56,25 @@ interface IState { @replaceableComponent("views.dialogs.CreateRoomDialog") export default class CreateRoomDialog extends React.Component { + private readonly supportsRestricted: boolean; private nameField = createRef(); private aliasField = createRef(); constructor(props) { super(props); + this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + let joinRule = JoinRule.Invite; + if (this.props.defaultPublic) { + joinRule = JoinRule.Public; + } else if (this.supportsRestricted) { + joinRule = JoinRule.Restricted; + } + const config = SdkConfig.get(); this.state = { - isPublic: this.props.defaultPublic || false, + joinRule, isEncrypted: privateShouldBeEncrypted(), name: this.props.defaultName || "", topic: "", @@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component { const opts: IOpts = {}; const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; - if (this.state.isPublic) { + + if (this.state.joinRule === JoinRule.Public) { createOpts.visibility = Visibility.Public; createOpts.preset = Preset.PublicChat; opts.guestAccess = false; const { alias } = this.state; createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); + } else { + // If we cannot change encryption we pass `true` for safety, the server should automatically do this for us. + opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true; } + if (this.state.topic) { createOpts.topic = this.state.topic; } @@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component { createOpts.creation_content = { 'm.federate': false }; } - if (!this.state.isPublic) { - if (this.state.canChangeEncryption) { - opts.encryption = this.state.isEncrypted; - } else { - // the server should automatically do this for us, but for safety - // we'll demand it too. - opts.encryption = true; - } - } - if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); } - if (this.props.parentSpace) { + if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) { opts.parentSpace = this.props.parentSpace; + opts.joinRule = JoinRule.Restricted; } return opts; @@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component { this.setState({ topic: ev.target.value }); }; - private onPublicChange = (isPublic: boolean) => { - this.setState({ isPublic }); + private onJoinRuleChange = (joinRule: JoinRule) => { + this.setState({ joinRule }); }; private onEncryptedChange = (isEncrypted: boolean) => { @@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component { render() { let aliasField; - if (this.state.isPublic) { + if (this.state.joinRule === JoinRule.Public) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
    @@ -224,19 +232,46 @@ export default class CreateRoomDialog extends React.Component { ); } - let publicPrivateLabel =

    {_t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone.", - )}

    ; + let publicPrivateLabel: JSX.Element; if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { - publicPrivateLabel =

    {_t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone in this community.", - )}

    ; + publicPrivateLabel =

    + { _t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + ) } +

    ; + } else if (this.state.joinRule === JoinRule.Restricted) { + publicPrivateLabel =

    + { _t( + "Everyone in will be able to find and join this room.", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

    ; + } else if (this.state.joinRule === JoinRule.Public) { + publicPrivateLabel =

    + { _t( + "Anyone will be able to find and join this room, not just members of .", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

    ; + } else if (this.state.joinRule === JoinRule.Invite) { + publicPrivateLabel =

    + { _t( + "Only people invited will be able to find and join this room.", + ) } +   + { _t("You can change this at any time from room settings.") } +

    ; } let e2eeSection; - if (!this.state.isPublic) { + if (this.state.joinRule !== JoinRule.Public) { let microcopy; if (privateShouldBeEncrypted()) { if (this.state.canChangeEncryption) { @@ -250,7 +285,7 @@ export default class CreateRoomDialog extends React.Component { } e2eeSection = { ); } - let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let title = _t("Create a room"); if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", { communityName: name }); + } else if (!this.props.parentSpace) { + title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); } + + const options = [ +
    + { _t("Private room (invite only)") } +
    , +
    + { _t("Public room") } +
    , + ]; + + if (this.supportsRestricted) { + options.unshift(
    + { _t("Visible to space members") } +
    ); + } + return ( - +
    { value={this.state.topic} className="mx_CreateRoomDialog_topic" /> - + + + { options } + + { publicPrivateLabel } { e2eeSection } { aliasField } @@ -318,7 +376,7 @@ export default class CreateRoomDialog extends React.Component { onChange={this.onNoFederateChange} value={this.state.noFederate} /> -

    {federateLabel}

    +

    { federateLabel }

    diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 134c4ab79e..d03b668cd9 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -72,7 +72,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { hasCancel={false} onPrimaryButtonClick={props.onFinished} > - diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index b2ac849314..7221df222f 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -172,11 +172,11 @@ export default class DeactivateAccountDialog extends React.Component; } - let auth =
    {_t("Loading...")}
    ; + let auth =
    { _t("Loading...") }
    ; if (this.state.authData && this.state.authEnabled) { auth = (
    - {this.state.bodyText} + { this.state.bodyText } - {_t( + { _t( "Please forget all messages I have sent when my account is deactivated " + "(Warning: this will cause future users to see an incomplete view " + "of conversations)", {}, { b: (sub) => { sub } }, - )} + ) }

    - {error} - {auth} + { error } + { auth }
    diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 86b8f93d7b..8ae9d0654f 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -182,14 +182,23 @@ export class SendCustomEvent extends GenericEditor - +
    { !this.state.message && } { showTglFlip &&
    - - +
    { !this.state.message && } { !this.state.message &&
    - - + key={this.props.children[0] ? this.props.children[0].key : ''} + /> ; + }} + />; } return
    @@ -494,7 +525,7 @@ class RoomStateExplorer extends React.PureComponent - {eventType} + { eventType } ; }) } @@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent; + }} + forceMode={true} + />; } return
    @@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent
    -
    Transaction
    -
    {txnId}
    +
    { txnId }
    Phase
    -
    {PHASE_MAP[request.phase] || request.phase}
    +
    { PHASE_MAP[request.phase] || request.phase }
    Timeout
    -
    {Math.floor(timeout / 1000)}
    +
    { Math.floor(timeout / 1000) }
    Methods
    -
    {request.methods && request.methods.join(", ")}
    +
    { request.methods && request.methods.join(", ") }
    requestingUserId
    -
    {request.requestingUserId}
    +
    { request.requestingUserId }
    observeOnly
    -
    {JSON.stringify(request.observeOnly)}
    +
    { JSON.stringify(request.observeOnly) }
    ); }; @@ -771,12 +806,12 @@ class VerificationExplorer extends React.PureComponent { return (
    - {Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => + { Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => , - )} + ) }
    - +
    ); } @@ -844,9 +879,9 @@ class WidgetExplorer extends React.Component ev.getId() === editWidget.eventId); if (!stateEv) { // "should never happen" return
    - {_t("There was an error finding this widget.")} + { _t("There was an error finding this widget.") }
    - +
    ; } @@ -865,17 +900,17 @@ class WidgetExplorer extends React.Component
    - {widgets.map(w => { + { widgets.map(w => { return ; - })} + >{ w.url }; + }) }
    - +
    ); } @@ -1007,7 +1042,7 @@ class SettingsExplorer extends React.PureComponent{canEdit.toString()}; + return
    ; } render() { @@ -1021,46 +1056,53 @@ class SettingsExplorer extends React.PureComponent
    {_t( + { _t( customVariables[row[0]].expl, customVariables[row[0]].getTextVariables ? customVariables[row[0]].getTextVariables() : null, - )}{ row[1] }
    { canEdit.toString() }
    - - - + + + - {allSettings.map(i => ( + { allSettings.map(i => ( - ))} + )) }
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}{ _t("Setting ID") }{ _t("Value") }{ _t("Value in this room") }
    this.onViewClick(e, i)}> - {i} + { i } - this.onEditClick(e, i)} + this.onEditClick(e, i)} className='mx_DevTools_SettingsExplorer_edit' > ✏ - {this.renderSettingValue(SettingsStore.getValue(i))} + { this.renderSettingValue(SettingsStore.getValue(i)) } - {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + { this.renderSettingValue(SettingsStore.getValue(i, room.roomId)) }
    - +
    ); @@ -1068,62 +1110,70 @@ class SettingsExplorer extends React.PureComponent
    -

    {_t("Setting:")} {this.state.editSetting}

    +

    { _t("Setting:") } { this.state.editSetting }

    - {_t("Caution:")} {_t( + { _t("Caution:") } { _t( "This UI does NOT check the types of the values. Use at your own risk.", - )} + ) }
    - {_t("Setting definition:")} -
    {JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}
    + { _t("Setting definition:") } +
    { JSON.stringify(SETTINGS[this.state.editSetting], null, 4) }
    - - - + + + - {LEVEL_ORDER.map(lvl => ( + { LEVEL_ORDER.map(lvl => ( - - {this.renderCanEditLevel(null, lvl)} - {this.renderCanEditLevel(room.roomId, lvl)} + + { this.renderCanEditLevel(null, lvl) } + { this.renderCanEditLevel(room.roomId, lvl) } - ))} + )) }
    {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}{ _t("Level") }{ _t("Settable at global") }{ _t("Settable at room") }
    {lvl}{ lvl }
    - - + +
    ); @@ -1131,39 +1181,39 @@ class SettingsExplorer extends React.PureComponent
    -

    {_t("Setting:")} {this.state.viewSetting}

    +

    { _t("Setting:") } { this.state.viewSetting }

    - {_t("Setting definition:")} -
    {JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}
    + { _t("Setting definition:") } +
    { JSON.stringify(SETTINGS[this.state.viewSetting], null, 4) }
    - {_t("Value:")}  - {this.renderSettingValue( + { _t("Value:") }  + { this.renderSettingValue( SettingsStore.getValue(this.state.viewSetting), - )} + ) }
    - {_t("Value in this room:")}  - {this.renderSettingValue( + { _t("Value in this room:") }  + { this.renderSettingValue( SettingsStore.getValue(this.state.viewSetting, room.roomId), - )} + ) }
    - {_t("Values at explicit levels:")} -
    {this.renderExplicitSettingValues(
    +                            { _t("Values at explicit levels:") }
    +                            
    { this.renderExplicitSettingValues(
                                     this.state.viewSetting, null,
    -                            )}
    + ) }
    - {_t("Values at explicit levels in this room:")} -
    {this.renderExplicitSettingValues(
    +                            { _t("Values at explicit levels in this room:") }
    +                            
    { this.renderExplicitSettingValues(
                                     this.state.viewSetting, room.roomId,
    -                            )}
    + ) }
    @@ -1171,7 +1221,7 @@ class SettingsExplorer extends React.PureComponent this.onEditClick(e, this.state.viewSetting)}>{ _t("Edit Values") } - + ); @@ -1232,12 +1282,12 @@ export default class DevtoolsDialog extends React.PureComponent if (this.state.mode) { body = - {(cli) => + { (cli) =>
    { this.state.mode.getLabel() }
    Room ID: { this.props.roomId }
    - } + } ; } else { const classes = "mx_DevTools_RoomStateExplorer_button"; diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 217e4f2d37..a0e6046d71 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -144,23 +144,25 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent
    {preview} + >{ preview }
    - {_t("Add image (optional)")} + { _t("Add image (optional)") } - {_t("An image will help people identify your community.")} + { _t("An image will help people identify your community.") }
    - {_t("Save")} + { _t("Save") }
    diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js index 88a57cf8cb..85171c9bf6 100644 --- a/src/components/views/dialogs/FeedbackDialog.js +++ b/src/components/views/dialogs/FeedbackDialog.js @@ -58,10 +58,10 @@ export default (props) => { countlyFeedbackSection =
    -

    {_t("Rate %(brand)s", { brand })}

    +

    { _t("Rate %(brand)s", { brand }) }

    -

    {_t("Tell us below how you feel about %(brand)s so far.", { brand })}

    -

    {_t("Please go into as much detail as you like, so we can track down the problem.")}

    +

    { _t("Tell us below how you feel about %(brand)s so far.", { brand }) }

    +

    { _t("Please go into as much detail as you like, so we can track down the problem.") }

    { let subheading; if (hasFeedback) { subheading = ( -

    {_t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand })}

    +

    { _t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand }) }

    ); } @@ -106,7 +106,7 @@ export default (props) => { _t("PRO TIP: If you start a bug, please submit debug logs " + "to help us track down the problem.", {}, { debugLogsLink: sub => ( - {sub} + { sub } ), }) }

    @@ -121,7 +121,7 @@ export default (props) => { { subheading }
    -

    {_t("Report a bug")}

    +

    { _t("Report a bug") }

    { _t("Please view existing bugs on Github first. " + "No match? Start a new one.", {}, { @@ -133,7 +133,7 @@ export default (props) => { }, }) }

    - {bugReports} + { bugReports }
    { countlyFeedbackSection } } diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 839ca6da2f..77e2b6ae0c 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -106,12 +106,12 @@ const Entry: React.FC = ({ room, event, matrixClient: cli, onFinish className = "mx_ForwardList_sending"; disabled = true; title = _t("Sending"); - icon =
    ; + icon =
    ; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("Sent"); - icon =
    ; + icon =
    ; } else { className = "mx_ForwardList_sendFailed"; disabled = true; @@ -204,10 +204,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx index 64c080bf01..4b8b7f32f0 100644 --- a/src/components/views/dialogs/HostSignupDialog.tsx +++ b/src/components/views/dialogs/HostSignupDialog.tsx @@ -177,32 +177,32 @@ export default class HostSignupDialog extends React.PureComponent

    - {_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " + + { _t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " + "account to fetch verified email addresses. This data is not stored.", { hostSignupBrand: this.config.brand, - })} + }) }

    - {_t("Learn more in our , and .", + { _t("Learn more in our , and .", {}, { cookiePolicyLink: () => ( - {_t("Cookie Policy")} + { _t("Cookie Policy") } ), privacyPolicyLink: () => ( - {_t("Privacy Policy")} + { _t("Privacy Policy") } ), termsOfServiceLink: () => ( - {_t("Terms of Service")} + { _t("Terms of Service") } ), }, - )} + ) }

    ); @@ -241,12 +241,12 @@ export default class HostSignupDialog extends React.PureComponent - {this.state.minimized && + { this.state.minimized &&
    - {_t("%(hostSignupBrand)s Setup", { + { _t("%(hostSignupBrand)s Setup", { hostSignupBrand: this.config.brand, - })} + }) }
    } - {!this.state.minimized && + { !this.state.minimized &&
    } - {this.state.error && + { this.state.error &&
    - {this.state.error} + { this.state.error }
    } - {!this.state.error && + { !this.state.error &&