diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b35b7c59..73b383d76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,152 @@ +Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0) + + * Fix 'User' type import + [\#6376](https://github.com/matrix-org/matrix-react-sdk/pull/6376) + +Changes in [3.26.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0-rc.1) (2021-07-14) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0...v3.26.0-rc.1) + + * Fix voice messages in right panels + [\#6370](https://github.com/matrix-org/matrix-react-sdk/pull/6370) + * Use TileShape enum more universally + [\#6369](https://github.com/matrix-org/matrix-react-sdk/pull/6369) + * Translations update from Weblate + [\#6373](https://github.com/matrix-org/matrix-react-sdk/pull/6373) + * Hide world readable history option in encrypted rooms + [\#5947](https://github.com/matrix-org/matrix-react-sdk/pull/5947) + * Make the Image View buttons easier to hit + [\#6372](https://github.com/matrix-org/matrix-react-sdk/pull/6372) + * Reorder buttons in the Image View + [\#6368](https://github.com/matrix-org/matrix-react-sdk/pull/6368) + * Add VS Code to gitignore + [\#6367](https://github.com/matrix-org/matrix-react-sdk/pull/6367) + * Fix inviter exploding due to member being null + [\#6362](https://github.com/matrix-org/matrix-react-sdk/pull/6362) + * Increase sample count in voice message thumbnail + [\#6359](https://github.com/matrix-org/matrix-react-sdk/pull/6359) + * Improve arraySeed utility + [\#6360](https://github.com/matrix-org/matrix-react-sdk/pull/6360) + * Convert FontManager to TS and stub it out for tests + [\#6358](https://github.com/matrix-org/matrix-react-sdk/pull/6358) + * Adjust recording waveform behaviour for voice messages + [\#6357](https://github.com/matrix-org/matrix-react-sdk/pull/6357) + * Do not honor string power levels + [\#6245](https://github.com/matrix-org/matrix-react-sdk/pull/6245) + * Add alias and directory customisation points + [\#6343](https://github.com/matrix-org/matrix-react-sdk/pull/6343) + * Fix multiinviter user already in room and clean up code + [\#6354](https://github.com/matrix-org/matrix-react-sdk/pull/6354) + * Fix right panel not closing user info when changing rooms + [\#6341](https://github.com/matrix-org/matrix-react-sdk/pull/6341) + * Quit sticker picker on m.sticker + [\#5679](https://github.com/matrix-org/matrix-react-sdk/pull/5679) + * Don't autodetect language in inline code blocks + [\#6350](https://github.com/matrix-org/matrix-react-sdk/pull/6350) + * Make ghost button background transparent + [\#6331](https://github.com/matrix-org/matrix-react-sdk/pull/6331) + * only consider valid & loaded url previews for show N more prompt + [\#6346](https://github.com/matrix-org/matrix-react-sdk/pull/6346) + * Extract MXCs from _matrix/media/r0/ URLs for inline images in messages + [\#6335](https://github.com/matrix-org/matrix-react-sdk/pull/6335) + * Fix small visual regression with the site name on url previews + [\#6342](https://github.com/matrix-org/matrix-react-sdk/pull/6342) + * Make PIP CallView draggable/movable + [\#5952](https://github.com/matrix-org/matrix-react-sdk/pull/5952) + * Convert VoiceUserSettingsTab to TS + [\#6340](https://github.com/matrix-org/matrix-react-sdk/pull/6340) + * Simplify typescript definition for Modernizr + [\#6339](https://github.com/matrix-org/matrix-react-sdk/pull/6339) + * Remember the last used server for room directory searches + [\#6322](https://github.com/matrix-org/matrix-react-sdk/pull/6322) + * Focus composer after reacting + [\#6332](https://github.com/matrix-org/matrix-react-sdk/pull/6332) + * Fix bug which prevented more than one event getting pinned + [\#6336](https://github.com/matrix-org/matrix-react-sdk/pull/6336) + * Make DeviceListener also update on megolm key in SSSS + [\#6337](https://github.com/matrix-org/matrix-react-sdk/pull/6337) + * Improve URL previews + [\#6326](https://github.com/matrix-org/matrix-react-sdk/pull/6326) + * Don't close settings dialog when opening spaces feedback prompt + [\#6334](https://github.com/matrix-org/matrix-react-sdk/pull/6334) + * Update import location for types + [\#6330](https://github.com/matrix-org/matrix-react-sdk/pull/6330) + * Improve blurhash rendering performance + [\#6329](https://github.com/matrix-org/matrix-react-sdk/pull/6329) + * Use a proper color scheme for codeblocks + [\#6320](https://github.com/matrix-org/matrix-react-sdk/pull/6320) + * Burn `sdk.getComponent()` with 🔥 + [\#6308](https://github.com/matrix-org/matrix-react-sdk/pull/6308) + * Fix instances of the Edit Message Composer's save button being wrongly + disabled + [\#6307](https://github.com/matrix-org/matrix-react-sdk/pull/6307) + * Do not generate a lockfile when running in CI + [\#6327](https://github.com/matrix-org/matrix-react-sdk/pull/6327) + * Update lockfile with correct dependencies + [\#6324](https://github.com/matrix-org/matrix-react-sdk/pull/6324) + * Clarify the keys we use when submitting rageshakes + [\#6321](https://github.com/matrix-org/matrix-react-sdk/pull/6321) + * Fix ImageView context menu + [\#6318](https://github.com/matrix-org/matrix-react-sdk/pull/6318) + * TypeScript migration + [\#6315](https://github.com/matrix-org/matrix-react-sdk/pull/6315) + * Move animation to compositor + [\#6310](https://github.com/matrix-org/matrix-react-sdk/pull/6310) + * Reorganize preferences + [\#5742](https://github.com/matrix-org/matrix-react-sdk/pull/5742) + * Fix being able to un-rotate images + [\#6313](https://github.com/matrix-org/matrix-react-sdk/pull/6313) + * Fix icon size in passphrase prompt + [\#6312](https://github.com/matrix-org/matrix-react-sdk/pull/6312) + * Use sleep & defer from js-sdk instead of duplicating it + [\#6305](https://github.com/matrix-org/matrix-react-sdk/pull/6305) + * Convert EventTimeline, EventTimelineSet and TimelineWindow to TS + [\#6295](https://github.com/matrix-org/matrix-react-sdk/pull/6295) + * Comply with new member-delimiter-style rule + [\#6306](https://github.com/matrix-org/matrix-react-sdk/pull/6306) + * Fix Test Linting + [\#6304](https://github.com/matrix-org/matrix-react-sdk/pull/6304) + * Convert Markdown to TypeScript + [\#6303](https://github.com/matrix-org/matrix-react-sdk/pull/6303) + * Convert RoomHeader to TS + [\#6302](https://github.com/matrix-org/matrix-react-sdk/pull/6302) + * Prevent RoomDirectory from exploding when filterString is wrongly nulled + [\#6296](https://github.com/matrix-org/matrix-react-sdk/pull/6296) + * Add support for blurhash (MSC2448) + [\#5099](https://github.com/matrix-org/matrix-react-sdk/pull/5099) + * Remove rateLimitedFunc + [\#6300](https://github.com/matrix-org/matrix-react-sdk/pull/6300) + * Convert some Key Verification classes to TypeScript + [\#6299](https://github.com/matrix-org/matrix-react-sdk/pull/6299) + * Typescript conversion of Composer components and more + [\#6292](https://github.com/matrix-org/matrix-react-sdk/pull/6292) + * Upgrade browserlist target versions + [\#6298](https://github.com/matrix-org/matrix-react-sdk/pull/6298) + * Fix browser crashing when searching for a malformed HTML tag + [\#6297](https://github.com/matrix-org/matrix-react-sdk/pull/6297) + * Add custom audio player + [\#6264](https://github.com/matrix-org/matrix-react-sdk/pull/6264) + * Lint MXC APIs to centralise access + [\#6293](https://github.com/matrix-org/matrix-react-sdk/pull/6293) + * Remove reminescent references to the tinter + [\#6290](https://github.com/matrix-org/matrix-react-sdk/pull/6290) + * More js-sdk type consolidation + [\#6263](https://github.com/matrix-org/matrix-react-sdk/pull/6263) + * Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript + [\#6243](https://github.com/matrix-org/matrix-react-sdk/pull/6243) + * Migrate to `eslint-plugin-matrix-org` + [\#6285](https://github.com/matrix-org/matrix-react-sdk/pull/6285) + * Avoid cyclic dependencies by moving watchers out of constructor + [\#6287](https://github.com/matrix-org/matrix-react-sdk/pull/6287) + * Add spacing between toast buttons with cross browser support in mind + [\#6284](https://github.com/matrix-org/matrix-react-sdk/pull/6284) + * Deprecate Tinter and TintableSVG + [\#6279](https://github.com/matrix-org/matrix-react-sdk/pull/6279) + * Migrate FilePanel to TypeScript + [\#6283](https://github.com/matrix-org/matrix-react-sdk/pull/6283) + Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0) diff --git a/package.json b/package.json index e80ed8dd5a..b73462d188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.25.0", + "version": "3.26.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -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", @@ -79,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.1", + "matrix-js-sdk": "12.1.0", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/res/css/_components.scss b/res/css/_components.scss index 1146a100b5..bc7d4fc85f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -162,6 +162,7 @@ @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; +@import "./views/messages/_CallEvent.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; @@ -201,6 +202,7 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_EventBubbleTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 7b975110e1..1a02c0d5ac 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -118,10 +118,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/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index cbddd97e18..65e4493f19 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -27,6 +27,7 @@ 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/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/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss new file mode 100644 index 0000000000..54c7df3e0b --- /dev/null +++ b/res/css/views/messages/_CallEvent.scss @@ -0,0 +1,154 @@ +/* +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_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/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 878a4154cd..f5d8131e6e 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -18,17 +18,16 @@ $timelineImageBorderRadius: 4px; .mx_MImageBody { display: block; - margin-right: 34px; } .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 { border-radius: $timelineImageBorderRadius; } @@ -43,17 +42,6 @@ $timelineImageBorderRadius: 4px; position: relative; } -.mx_MImageBody_thumbnail_spinner { - position: absolute; - left: 50%; - top: 50%; -} - -// Inner img should be centered around 0, 0 -.mx_MImageBody_thumbnail_spinner > * { - transform: translate(-50%, -50%); -} - .mx_MImageBody_gifLabel { position: absolute; display: block; 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/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index e05065eb02..b2bca6dfb3 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -26,6 +26,7 @@ limitations under the License. height: 24px; vertical-align: middle; margin-left: 4px; + margin-right: 4px; &::before { content: ''; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss new file mode 100644 index 0000000000..c66f635ffe --- /dev/null +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -0,0 +1,323 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EventTile[data-layout=bubble], +.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { + --avatarSize: 32px; + --gutterSize: 11px; + --cornerRadius: 12px; + --maxWidth: 70%; +} + +.mx_EventTile[data-layout=bubble] { + + position: relative; + margin-top: var(--gutterSize); + margin-left: 50px; + margin-right: 100px; + + &.mx_EventTile_continuation { + margin-top: 2px; + } + + /* For replies */ + .mx_EventTile { + padding-top: 0; + } + + &:hover { + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -60px; + z-index: -1; + background: $eventbubble-bg-hover; + border-radius: 4px; + } + + .mx_EventTile_avatar { + img { + box-shadow: 0 0 0 3px $eventbubble-bg-hover; + } + } + } + + .mx_SenderProfile, + .mx_EventTile_line { + width: fit-content; + max-width: 70%; + } + + .mx_SenderProfile { + position: relative; + top: -2px; + left: 2px; + } + + &[data-self=false] { + .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } + .mx_EventTile_avatar { + left: -34px; + } + + .mx_MessageActionBar { + right: 0; + transform: translate3d(50%, 50%, 0); + } + + --backgroundColor: $eventbubble-others-bg; + } + &[data-self=true] { + .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + float: right; + > a { + left: auto; + right: -48px; + } + } + .mx_SenderProfile { + display: none; + } + .mx_ReactionsRow { + float: right; + clear: right; + display: flex; + + /* Moving the "add reaction button" before the reactions */ + > :last-child { + order: -1; + } + } + .mx_EventTile_avatar { + top: -19px; // height of the sender block + right: -35px; + } + + --backgroundColor: $eventbubble-self-bg; + } + + .mx_EventTile_line { + position: relative; + padding: var(--gutterSize); + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + background: var(--backgroundColor); + display: flex; + gap: 5px; + margin: 0 -12px 0 -9px; + > a { + position: absolute; + left: -48px; + } + } + + &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { + border-top-left-radius: 0; + } + &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + } + + &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { + border-top-right-radius: 0; + } + &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } + + .mx_EventTile_avatar { + position: absolute; + top: 0; + line-height: 1; + img { + box-shadow: 0 0 0 3px $eventbubble-avatar-outline; + border-radius: 50%; + } + } + + &[data-has-reply=true] { + > .mx_EventTile_line { + flex-direction: column; + } + + .mx_ReplyThread_show { + order: 99999; + } + + .mx_ReplyThread { + margin: 0 calc(-1 * var(--gutterSize)); + + .mx_EventTile_reply { + max-width: 90%; + padding: 0; + > a { + display: none !important; + } + } + + .mx_EventTile { + display: flex; + gap: var(--gutterSize); + .mx_EventTile_avatar { + position: static; + } + .mx_SenderProfile { + display: none; + } + } + } + } + + .mx_EditMessageComposer_buttons { + position: static; + padding: 0; + margin: 0; + background: transparent; + } + + .mx_ReactionsRow { + margin-right: -18px; + margin-left: -9px; + } + + .mx_ReplyThread { + border-left-width: 2px; + border-left-color: $eventbubble-reply-color; + } + + &.mx_EventTile_bubbleContainer, + &.mx_EventTile_info, + & ~ .mx_EventListSummary[data-expanded=false] { + --backgroundColor: transparent; + --gutterSize: 0; + + display: flex; + align-items: center; + justify-content: center; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } + } + + & ~ .mx_EventListSummary { + --maxWidth: 80%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: calc(var(--gutterSize) + var(--avatarSize)); + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + margin-left: 5px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(50%, 0, 0); + } + } + + & ~ .mx_EventListSummary[data-expanded=false] { + padding: 0 34px; + } + + /* events that do not require bubble layout */ + & ~ .mx_EventListSummary, + &.mx_EventTile_bad { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } + } + + & + .mx_EventListSummary { + .mx_EventTile { + margin-top: 0; + padding: 0; + } + } + + .mx_EventListSummary_toggle { + margin-right: 55px; + } + + /* Special layout scenario for "Unable To Decrypt (UTD)" events */ + &.mx_EventTile_bad > .mx_EventTile_line { + display: grid; + grid-template: + "reply reply" auto + "shield body" auto + "shield link" auto + / auto 1fr; + .mx_EventTile_e2eIcon { + grid-area: shield; + } + .mx_UnknownBody { + grid-area: body; + } + .mx_EventTile_keyRequestInfo { + grid-area: link; + } + .mx_ReplyThread_wrapper { + grid-area: reply; + } + } + + + .mx_EventTile_readAvatars { + position: absolute; + right: -110px; + bottom: 0; + top: auto; + } + + .mx_MTextBody { + max-width: 100%; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 55f73c0315..1e3f51e580 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,102 +18,307 @@ limitations under the License. $left-gutter: 64px; $hover-select-border: 4px; -.mx_EventTile { +.mx_EventTile:not([data-layout=bubble]) { max-width: 100%; clear: both; padding-top: 18px; font-size: $font-14px; position: relative; -} -.mx_EventTile.mx_EventTile_info { - padding-top: 1px; -} + &.mx_EventTile_info { + padding-top: 1px; + } -.mx_EventTile_avatar { - top: 14px; - left: 8px; - cursor: pointer; - user-select: none; -} + .mx_EventTile_avatar { + top: 14px; + left: 8px; + cursor: pointer; + user-select: none; + } -.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: $font-6px; - left: $left-gutter; -} + &.mx_EventTile_info .mx_EventTile_avatar { + top: $font-6px; + left: $left-gutter; + } -.mx_EventTile_continuation { - padding-top: 0px !important; + &.mx_EventTile_continuation { + padding-top: 0px !important; + + &.mx_EventTile_isEditing { + padding-top: 5px !important; + margin-top: -5px; + } + } &.mx_EventTile_isEditing { - padding-top: 5px !important; - margin-top: -5px; + background-color: $header-panel-bg-color; } -} -.mx_EventTile_isEditing { - background-color: $header-panel-bg-color; -} + .mx_SenderProfile { + color: $primary-fg-color; + font-size: $font-14px; + display: inline-block; /* anti-zalgo, with overflow hidden */ + overflow: hidden; + cursor: pointer; + padding-bottom: 0px; + padding-top: 0px; + margin: 0px; + /* the next three lines, along with overflow hidden, truncate long display names */ + white-space: nowrap; + text-overflow: ellipsis; + max-width: calc(100% - $left-gutter); + } -.mx_EventTile .mx_SenderProfile { - color: $primary-fg-color; - font-size: $font-14px; - display: inline-block; /* anti-zalgo, with overflow hidden */ - overflow: hidden; - cursor: pointer; - padding-bottom: 0px; - padding-top: 0px; - margin: 0px; - /* the next three lines, along with overflow hidden, truncate long display names */ - white-space: nowrap; - text-overflow: ellipsis; - max-width: calc(100% - $left-gutter); -} + .mx_SenderProfile .mx_Flair { + opacity: 0.7; + margin-left: 5px; + display: inline-block; + vertical-align: top; + overflow: hidden; + user-select: none; -.mx_EventTile .mx_SenderProfile .mx_Flair { - opacity: 0.7; - margin-left: 5px; - display: inline-block; - vertical-align: top; - overflow: hidden; - user-select: none; + img { + vertical-align: -2px; + margin-right: 2px; + border-radius: 8px; + } + } - img { - vertical-align: -2px; - margin-right: 2px; + &.mx_EventTile_isEditing .mx_MessageTimestamp { + visibility: hidden; + } + + .mx_MessageTimestamp { + display: block; + white-space: nowrap; + left: 0px; + text-align: center; + user-select: none; + } + + &.mx_EventTile_continuation .mx_EventTile_line { + clear: both; + } + + .mx_EventTile_line, .mx_EventTile_reply { + position: relative; + padding-left: $left-gutter; border-radius: 8px; } -} -.mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden; -} - -.mx_EventTile .mx_MessageTimestamp { - display: block; - white-space: nowrap; - left: 0px; - text-align: center; - user-select: none; -} - -.mx_EventTile_continuation .mx_EventTile_line { - clear: both; -} - -.mx_EventTile_line, .mx_EventTile_reply { - position: relative; - padding-left: $left-gutter; - border-radius: 8px; -} - -.mx_RoomView_timeline_rr_enabled, -// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter -.mx_EventListSummary { - .mx_EventTile_line { - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; + .mx_EventTile_reply { + margin-right: 10px; } + + &.mx_EventTile_selected > div > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } + + /* this is used for the tile for the event which is selected via the URL. + * TODO: ultimately we probably want some transition on here. + */ + &.mx_EventTile_selected > .mx_EventTile_line { + border-left: $accent-color 4px solid; + padding-left: calc($left-gutter - $hover-select-border); + background-color: $event-selected-color; + } + + &.mx_EventTile_highlight, + &.mx_EventTile_highlight .markdown-body { + color: $event-highlight-fg-color; + + .mx_EventTile_line { + background-color: $event-highlight-bg-color; + } + } + + &.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } + + & ~ .mx_EventListSummary .mx_EventTile_line { + padding-left: calc($left-gutter); + } + + &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + &.mx_EventTile:hover .mx_EventTile_line, + &.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, + &.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { + background-color: $event-selected-color; + } + + .mx_EventTile_searchHighlight { + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 5px; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; + } + + .mx_EventTile_searchHighlight a { + background-color: $accent-color; + color: $accent-fg-color; + } + + .mx_EventTile_receiptSent, + .mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } + } + .mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + .mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } + + &.mx_EventTile_contextual { + opacity: 0.4; + } + + .mx_EventTile_msgOption { + float: right; + text-align: right; + position: relative; + width: 90px; + + /* Hack to stop the height of this pushing the messages apart. + Replaces margin-top: -6px. This interacts better with a read + marker being in between. Content overflows. */ + height: 1px; + + margin-right: 10px; + } + + .mx_EventTile_msgOption a { + text-decoration: none; + } + + /* De-zalgoing */ + .mx_EventTile_body { + overflow-y: hidden; + } + + &:hover.mx_EventTile_verified .mx_EventTile_line, + &:hover.mx_EventTile_unverified .mx_EventTile_line, + &:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: calc($left-gutter - $hover-select-border); + } + + &:hover.mx_EventTile_verified .mx_EventTile_line { + border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; + } + + &:hover.mx_EventTile_unverified .mx_EventTile_line { + border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; + } + + &:hover.mx_EventTile_unknown .mx_EventTile_line { + border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; + } + + &:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, + &:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, + &:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + /* End to end encryption stuff */ + &:hover .mx_EventTile_e2eIcon { + opacity: 1; + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + &:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, + &:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, + &:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + &:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, + &:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, + &:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { + display: block; + left: 41px; + } + + .mx_MImageBody { + margin-right: 34px; + } + + .mx_EventTile_e2eIcon { + position: absolute; + top: 6px; + left: 44px; + bottom: 0; + right: 0; + } + + .mx_ReactionsRow { + margin: 0; + padding: 6px 60px; + } +} + +/* 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]) { + .mx_EventTile_line { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } + } + + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } .mx_EventTile_bubbleContainer { @@ -130,123 +335,15 @@ $hover-select-border: 4px; .mx_EventTile_msgOption { grid-column: 2; } -} -.mx_EventTile_reply { - margin-right: 10px; -} - -/* HACK to override line-height which is already marked important elsewhere */ -.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { - font-size: 48px !important; - line-height: 57px !important; -} - -.mx_EventTile_selected > div > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -.mx_EventTile:hover .mx_MessageActionBar, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, -[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, -.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { - visibility: visible; -} - -/* this is used for the tile for the event which is selected via the URL. - * TODO: ultimately we probably want some transition on here. - */ -.mx_EventTile_selected > .mx_EventTile_line { - border-left: $accent-color 4px solid; - padding-left: calc($left-gutter - $hover-select-border); - background-color: $event-selected-color; -} - -.mx_EventTile_highlight, -.mx_EventTile_highlight .markdown-body { - color: $event-highlight-fg-color; - - .mx_EventTile_line { - background-color: $event-highlight-bg-color; + &:hover { + .mx_EventTile_line { + // To avoid bubble events being highlighted + background-color: inherit !important; + } } } -.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); -} - -.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -.mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, -.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { - background-color: $event-selected-color; -} - -.mx_EventTile_searchHighlight { - background-color: $accent-color; - color: $accent-fg-color; - border-radius: 5px; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; -} - -.mx_EventTile_searchHighlight a { - background-color: $accent-color; - color: $accent-fg-color; -} - -.mx_EventTile_receiptSent, -.mx_EventTile_receiptSending { - // We don't use `position: relative` on the element because then it won't line - // up with the other read receipts - - &::before { - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - width: 14px; - height: 14px; - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - } -} -.mx_EventTile_receiptSent::before { - mask-image: url('$(res)/img/element-icons/circle-sent.svg'); -} -.mx_EventTile_receiptSending::before { - mask-image: url('$(res)/img/element-icons/circle-sending.svg'); -} - -.mx_EventTile_contextual { - opacity: 0.4; -} - -.mx_EventTile_msgOption { - float: right; - text-align: right; - position: relative; - width: 90px; - - /* Hack to stop the height of this pushing the messages apart. - Replaces margin-top: -6px. This interacts better with a read - marker being in between. Content overflows. */ - height: 1px; - - margin-right: 10px; -} - -.mx_EventTile_msgOption a { - text-decoration: none; -} - .mx_EventTile_readAvatars { position: relative; display: inline-block; @@ -277,52 +374,27 @@ $hover-select-border: 4px; position: absolute; } -/* 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; +/* HACK to override line-height which is already marked important elsewhere */ +.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { + font-size: 48px !important; + line-height: 57px !important; } -/* De-zalgoing */ -.mx_EventTile_body { - overflow-y: hidden; -} - -/* Spoiler stuff */ -.mx_EventTile_spoiler { +.mx_EventTile_content .mx_EventTile_edited { + user-select: none; + font-size: $font-12px; + color: $roomtopic-color; + display: inline-block; + margin-left: 9px; cursor: pointer; } -.mx_EventTile_spoiler_reason { - color: $event-timestamp-color; - font-size: $font-11px; -} - -.mx_EventTile_spoiler_content { - filter: blur(5px) saturate(0.1) sepia(1); - transition-duration: 0.5s; -} - -.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - filter: none; -} .mx_EventTile_e2eIcon { - position: absolute; - top: 6px; - left: 44px; + position: relative; width: 14px; height: 14px; display: block; - bottom: 0; - right: 0; opacity: 0.2; background-repeat: no-repeat; background-size: contain; @@ -381,87 +453,6 @@ $hover-select-border: 4px; opacity: 1; } -.mx_EventTile_keyRequestInfo { - font-size: $font-12px; -} - -.mx_EventTile_keyRequestInfo_text { - opacity: 0.5; -} - -.mx_EventTile_keyRequestInfo_text a { - color: $primary-fg-color; - text-decoration: underline; - cursor: pointer; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p { - text-align: auto; - margin-left: 3px; - margin-right: 3px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { - margin-top: 0px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { - margin-bottom: 0px; -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: calc($left-gutter - $hover-select-border); -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -/* End to end encryption stuff */ -.mx_EventTile:hover .mx_EventTile_e2eIcon { - opacity: 1; -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { - display: block; - left: 41px; -} - -.mx_EventTile_content .mx_EventTile_edited { - user-select: none; - font-size: $font-12px; - color: $roomtopic-color; - display: inline-block; - margin-left: 9px; - cursor: pointer; -} - /* Various markdown overrides */ .mx_EventTile_body pre { @@ -595,6 +586,35 @@ $hover-select-border: 4px; /* end of overrides */ + +.mx_EventTile_keyRequestInfo { + font-size: $font-12px; +} + +.mx_EventTile_keyRequestInfo_text { + opacity: 0.5; +} + +.mx_EventTile_keyRequestInfo_text a { + color: $primary-fg-color; + text-decoration: underline; + cursor: pointer; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p { + text-align: auto; + margin-left: 3px; + margin-right: 3px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { + margin-top: 0px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { + margin-bottom: 0px; +} + .mx_EventTile_tileError { color: red; text-align: center; @@ -615,6 +635,13 @@ $hover-select-border: 4px; } } +.mx_EventTile:hover .mx_MessageActionBar, +.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, +[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, +.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { + visibility: visible; +} + @media only screen and (max-width: 480px) { .mx_EventTile_line, .mx_EventTile_reply { padding-left: 0; diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 94983a60bf..9bc583ea93 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -45,6 +45,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; 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/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 74b33fbd02..2a4ebff034 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); +// Bubble tiles +$eventbubble-self-bg: #143A34; +$eventbubble-others-bg: #394049; +$eventbubble-bg-hover: #433C23; +$eventbubble-avatar-outline: $bg-color; +$eventbubble-reply-color: #C1C6CD; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index c7debcdabe..f349a804a8 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color; $composer-shadow-color: tranparent; +// Bubble tiles +$eventbubble-self-bg: #F8FDFC; +$eventbubble-others-bg: #F7F8F9; +$eventbubble-bg-hover: rgb(242, 242, 242); +$eventbubble-avatar-outline: #fff; +$eventbubble-reply-color: #C1C6CD; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 7e958c2af6..ef5f4d8c86 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px; $composer-shadow-color: rgba(0, 0, 0, 0.04); +// Bubble tiles +$eventbubble-self-bg: #F8FDFC; +$eventbubble-others-bg: #F7F8F9; +$eventbubble-bg-hover: #FEFCF5; +$eventbubble-avatar-outline: $primary-bg-color; +$eventbubble-reply-color: #C1C6CD; + // ***** Mixins! ***** @define-mixin mx_DialogButton { 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/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/Avatar.ts b/src/Avatar.ts index 198d4162a0..c0ecb19eaf 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -18,6 +18,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { User } from "matrix-js-sdk/src/models/user"; import { Room } from "matrix-js-sdk/src/models/room"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; +import { split } from "lodash"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; @@ -122,27 +123,13 @@ export function getInitialLetter(name: string): string { return undefined; } - let idx = 0; const initial = name[0]; if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { - idx++; + name = name.substring(1); } - // string.codePointAt(0) would do this, but that isn't supported by - // some browsers (notably PhantomJS). - let chars = 1; - const first = name.charCodeAt(idx); - - // check if it’s the start of a surrogate pair - if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { - const second = name.charCodeAt(idx+1); - if (second >= 0xDC00 && second <= 0xDFFF) { - chars++; - } - } - - const firstChar = name.substring(idx, idx+chars); - return firstChar.toUpperCase(); + // rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis + return split(name, "", 1)[0].toUpperCase(); } export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index f90854ee64..e7ba1aa9fb 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); @@ -615,23 +656,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 +690,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..b125ddeeb5 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"; @@ -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/SlashCommands.tsx b/src/SlashCommands.tsx index 7753ff6f75..9f5ac83a56 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -480,14 +480,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 +522,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 +1069,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/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("Advanced") } - {_t("Set up with a Security Key")} + { _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..aa78d68830 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -475,9 +475,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { >
- {_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.") }
); } @@ -494,9 +494,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { >
- {_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,35 @@ 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 +732,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 +787,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/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js index 4a0aa37da0..263d25c98c 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js @@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const title = - {_t("New Recovery Method")} + { _t("New Recovery Method") } ; - const newMethodDetected =

{_t( + const newMethodDetected =

{ _t( "A new Security Phrase and key for Secure Messages have been detected.", - )}

; + ) }

; - const hackWarning =

{_t( + const hackWarning =

{ _t( "If you didn't set the new recovery method, an " + "attacker may be trying to access your account. " + "Change your account password and set a new recovery " + "method immediately in Settings.", - )}

; + ) }

; let content; if (MatrixClientPeg.get().getKeyBackupEnabled()) { content =
- {newMethodDetected} -

{_t( + { newMethodDetected } +

{ _t( "This session is encrypting history using the new recovery method.", - )}

- {hackWarning} + ) }

+ { hackWarning } ; } else { content =
- {newMethodDetected} - {hackWarning} + { newMethodDetected } + { hackWarning } - {content} + { content } ); } diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js index f0f8a5273b..f586c9430a 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js @@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const title = - {_t("Recovery Method Removed")} + { _t("Recovery Method Removed") } ; return ( @@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { title={title} >
-

{_t( +

{ _t( "This session has detected that your Security Phrase and key " + "for Secure Messages have been removed.", - )}

-

{_t( + ) }

+

{ _t( "If you did this accidentally, you can setup Secure Messages on " + "this session which will re-encrypt this session's message " + "history with a new recovery method.", - )}

-

{_t( + ) }

+

{ _t( "If you didn't remove the recovery method, an " + "attacker may be trying to access your account. " + "Change your account password and set a new recovery " + "method immediately in Settings.", - )}

+ ) }

{ +const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => { if (a.group === b.group) { return a.order - b.order; } return a.group - b.group; }).map((emoji, index) => ({ emoji, - shortname: `:${emoji.shortcodes[0]}:`, // Include the index so that we can preserve the original order _orderBy: index, })); @@ -64,20 +61,18 @@ function score(query, space) { } export default class EmojiProvider extends AutocompleteProvider { - matcher: QueryMatcher; - nameMatcher: QueryMatcher; + matcher: QueryMatcher; + nameMatcher: QueryMatcher; constructor() { super(EMOJI_REGEX); - this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { - keys: ['emoji.emoticon', 'shortname'], - funcs: [ - (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases - ], + this.matcher = new QueryMatcher(SORTED_EMOJI, { + keys: ['emoji.emoticon'], + funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); - this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { + this.nameMatcher = new QueryMatcher(SORTED_EMOJI, { keys: ['emoji.annotation'], // For removing punctuation shouldMatchWordsOnly: true, @@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider { const sorters = []; // make sure that emoticons come first - sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); + sorters.push(c => score(matchedString, c.emoji.emoticon || "")); - // then sort by score (Infinity if matchedString not in shortname) - sorters.push((c) => score(matchedString, c.shortname)); + // then sort by score (Infinity if matchedString not in shortcode) + sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); // then sort by max score of all shortcodes, trim off the `:` - sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)))); - // If the matchedString is not empty, sort by length of shortname. Example: + sorters.push(c => Math.min( + ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)), + )); + // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] if (matchedString.length > 1) { - sorters.push((c) => c.shortname.length); + sorters.push(c => c.emoji.shortcodes[0].length); } // Finally, sort by original ordering - sorters.push((c) => c._orderBy); + sorters.push(c => c._orderBy); completions = sortBy(uniq(completions), sorters); - completions = completions.map(({ shortname }) => { - const unicode = shortcodeToUnicode(shortname); - return { - completion: unicode, - component: ( - - { unicode } - - ), - range, - }; - }).slice(0, LIMIT); + completions = completions.map(c => ({ + completion: c.emoji.unicode, + component: ( + + { c.emoji.unicode } + + ), + range, + })).slice(0, LIMIT); } return completions; } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index d8f17c54d0..182743abb3 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider { limit = -1, ): Promise { // lazy-load user list into matcher - if (!this.users) this._makeUsers(); + if (!this.users) this.makeUsers(); let completions = []; const { command, range } = this.getCurrentCommand(rawQuery, selection, force); @@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider { return _t('Users'); } - _makeUsers() { + private makeUsers() { const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts new file mode 100644 index 0000000000..384f20cd4e --- /dev/null +++ b/src/components/structures/CallEventGrouper.ts @@ -0,0 +1,145 @@ +/* +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. +*/ + +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import CallHandler, { CallHandlerEvent } from '../../CallHandler'; +import { EventEmitter } from 'events'; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import defaultDispatcher from "../../dispatcher/dispatcher"; + +export enum CallEventGrouperEvent { + StateChanged = "state_changed", + SilencedChanged = "silenced_changed", +} + +const SUPPORTED_STATES = [ + CallState.Connected, + CallState.Connecting, + CallState.Ringing, +]; + +export enum CustomCallState { + Missed = "missed", +} + +export default class CallEventGrouper extends EventEmitter { + private events: Set = new Set(); + private call: MatrixCall; + public state: CallState | CustomCallState; + + constructor() { + super(); + + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall); + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + } + + private get invite(): MatrixEvent { + return [...this.events].find((event) => event.getType() === EventType.CallInvite); + } + + private get hangup(): MatrixEvent { + return [...this.events].find((event) => event.getType() === EventType.CallHangup); + } + + private get reject(): MatrixEvent { + return [...this.events].find((event) => event.getType() === EventType.CallReject); + } + + public get isVoice(): boolean { + const invite = this.invite; + if (!invite) return; + + // FIXME: Find a better way to determine this from the event? + if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false; + return true; + } + + public get hangupReason(): string | null { + return this.hangup?.getContent()?.reason; + } + + /** + * Returns true if there are only events from the other side - we missed the call + */ + private get callWasMissed(): boolean { + return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); + } + + private get callId(): string { + return [...this.events][0].getContent().call_id; + } + + private onSilencedCallsChanged = () => { + const newState = CallHandler.sharedInstance().isCallSilenced(this.callId); + this.emit(CallEventGrouperEvent.SilencedChanged, newState); + }; + + public answerCall = () => { + this.call?.answer(); + }; + + public rejectCall = () => { + this.call?.reject(); + }; + + public callBack = () => { + defaultDispatcher.dispatch({ + action: 'place_call', + type: this.isVoice ? CallType.Voice : CallType.Video, + room_id: [...this.events][0]?.getRoomId(), + }); + }; + + public toggleSilenced = () => { + const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId); + silenced ? + CallHandler.sharedInstance().unSilenceCall(this.callId) : + CallHandler.sharedInstance().silenceCall(this.callId); + }; + + private setCallListeners() { + if (!this.call) return; + this.call.addListener(CallEvent.State, this.setState); + } + + private setState = () => { + if (SUPPORTED_STATES.includes(this.call?.state)) { + this.state = this.call.state; + } else { + if (this.callWasMissed) this.state = CustomCallState.Missed; + else if (this.reject) this.state = CallState.Ended; + else if (this.hangup) this.state = CallState.Ended; + else if (this.invite && this.call) this.state = CallState.Connecting; + } + this.emit(CallEventGrouperEvent.StateChanged, this.state); + }; + + private setCall = () => { + if (this.call) return; + + this.call = CallHandler.sharedInstance().getCallById(this.callId); + this.setCallListeners(); + this.setState(); + }; + + public add(event: MatrixEvent) { + this.events.add(event); + this.setCall(); + } +} diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index 037d7c251c..5e31048207 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component { return (
- {tags} + { tags }
); } @@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component { "mx_TagTile_badge": true, "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions, }); - badgeElement = (
{FormattingUtils.formatCount(badgeNotifState.count)}
); + badgeElement = (
{ FormattingUtils.formatCount(badgeNotifState.count) }
); } return ( diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 628c16f322..472a43e142 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -125,11 +125,11 @@ export default class EmbeddedPage extends React.PureComponent { if (this.props.scrollbar) { return - {content} + { content } ; } else { return
- {content} + { content }
; } } diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 36f774a130..c6d72d04bb 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -241,8 +241,8 @@ class FilePanel extends React.Component { // wrap a TimelinePanel with the jump-to-event bits turned off. const emptyState = (
-

{_t('No files visible in this room')}

-

{_t('Attach files from chat or just drag and drop them anywhere in a room.')}

+

{ _t('No files visible in this room') }

+

{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }

); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); @@ -262,7 +262,7 @@ class FilePanel extends React.Component { manageReadReceipts={false} manageReadMarkers={false} timelineSet={this.state.timelineSet} - showUrlPreview = {false} + showUrlPreview={false} onPaginationRequest={this.onPaginationRequest} tileShape={TileShape.FileGrid} resizeNotifier={this.props.resizeNotifier} diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js index c9ed4ae622..017d365273 100644 --- a/src/components/structures/GenericErrorPage.js +++ b/src/components/structures/GenericErrorPage.js @@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent { render() { return
-

{this.props.title}

-

{this.props.message}

+

{ this.props.title }

+

{ this.props.message }

; } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index f31f302b29..9d69fce801 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -819,12 +819,12 @@ export default class GroupView extends React.Component { let hostingSignup = null; if (hostingSignupLink && this.state.isUserPrivileged) { hostingSignup =
- {_t( + { _t( "Want more than a community? Get your own server", {}, { - a: sub => {sub}, + a: sub => { sub }, }, - )} + ) } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 3d5e386b00..3bd2c68c6c 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component { onSelectRoom={this.selectRoom} /> - {dialPadButton} + { dialPadButton } { leftLeftPanel = (
- {SettingsStore.getValue("feature_custom_tags") ? : null} + { SettingsStore.getValue("feature_custom_tags") ? : null }
); } @@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component { return (
- {leftLeftPanel} + { leftLeftPanel }
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 6c086ed17c..d496c4ad21 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -17,8 +17,8 @@ limitations under the License. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { Key } from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -79,6 +79,8 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; + // Called with the credentials of a registered user (if they were a ROU that + // transitioned to PWLU) onRegistered: (credentials: IMatrixClientCreds) => Promise; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; @@ -140,18 +142,6 @@ interface IState { class LoggedInView extends React.Component { static displayName = 'LoggedInView'; - static propTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - page_type: PropTypes.string.isRequired, - onRoomCreated: PropTypes.func, - - // Called with the credentials of a registered user (if they were a ROU that - // transitioned to PWLU) - onRegistered: PropTypes.func, - - // and lots and lots of other stuff. - }; - protected readonly _matrixClient: MatrixClient; protected readonly _roomView: React.RefObject; protected readonly _resizeContainer: React.RefObject; @@ -181,10 +171,10 @@ class LoggedInView extends React.Component { } componentDidMount() { - document.addEventListener('keydown', this._onNativeKeyDown, false); + document.addEventListener('keydown', this.onNativeKeyDown, false); CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); - this._updateServerNoticeEvents(); + this.updateServerNoticeEvents(); this._matrixClient.on("accountData", this.onAccountData); this._matrixClient.on("sync", this.onSync); @@ -200,13 +190,13 @@ class LoggedInView extends React.Component { "useCompactLayout", null, this.onCompactLayoutChanged, ); - this.resizer = this._createResizer(); + this.resizer = this.createResizer(); this.resizer.attach(); - this._loadResizerPreferences(); + this.loadResizerPreferences(); } componentWillUnmount() { - document.removeEventListener('keydown', this._onNativeKeyDown, false); + document.removeEventListener('keydown', this.onNativeKeyDown, false); CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); @@ -221,37 +211,37 @@ class LoggedInView extends React.Component { }); }; - canResetTimelineInRoom = (roomId) => { + public canResetTimelineInRoom = (roomId: string) => { if (!this._roomView.current) { return true; } return this._roomView.current.canResetTimeline(); }; - _createResizer() { - let size; - let collapsed; + private createResizer() { + let panelSize; + let panelCollapsed; const collapseConfig: ICollapseConfig = { // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel toggleSize: 206 - 50, - onCollapsed: (_collapsed) => { - collapsed = _collapsed; - if (_collapsed) { + onCollapsed: (collapsed) => { + panelCollapsed = collapsed; + if (collapsed) { dis.dispatch({ action: "hide_left_panel" }); window.localStorage.setItem("mx_lhs_size", '0'); } else { dis.dispatch({ action: "show_left_panel" }); } }, - onResized: (_size) => { - size = _size; + onResized: (size) => { + panelSize = size; this.props.resizeNotifier.notifyLeftHandleResized(); }, onResizeStart: () => { this.props.resizeNotifier.startResizing(); }, onResizeStop: () => { - if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size); + if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize); this.props.resizeNotifier.stopResizing(); }, isItemCollapsed: domNode => { @@ -267,7 +257,7 @@ class LoggedInView extends React.Component { return resizer; } - _loadResizerPreferences() { + private loadResizerPreferences() { let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); if (isNaN(lhsSize)) { lhsSize = 350; @@ -275,7 +265,7 @@ class LoggedInView extends React.Component { this.resizer.forHandleAt(0).resize(lhsSize); } - onAccountData = (event) => { + private onAccountData = (event: MatrixEvent) => { if (event.getType() === "m.ignored_user_list") { dis.dispatch({ action: "ignore_state_changed" }); } @@ -307,16 +297,16 @@ class LoggedInView extends React.Component { } if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { - this._updateServerNoticeEvents(); + this.updateServerNoticeEvents(); } else { - this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); + this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); } }; onRoomStateEvents = (ev, state) => { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { - this._updateServerNoticeEvents(); + this.updateServerNoticeEvents(); } }; @@ -326,7 +316,7 @@ class LoggedInView extends React.Component { }); }; - _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { usageLimitEventContent = syncError.error.data; @@ -346,7 +336,7 @@ class LoggedInView extends React.Component { } } - _updateServerNoticeEvents = async () => { + private updateServerNoticeEvents = async () => { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; if (!serverNoticeList) return []; @@ -378,7 +368,7 @@ class LoggedInView extends React.Component { ); }); const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); - this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); + this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); this.setState({ usageLimitEventContent, usageLimitEventTs: pinnedEventTs, @@ -387,7 +377,7 @@ class LoggedInView extends React.Component { }); }; - _onPaste = (ev) => { + private onPaste = (ev) => { let canReceiveInput = false; let element = ev.target; // test for all parents because the target can be a child of a contenteditable element @@ -425,22 +415,22 @@ class LoggedInView extends React.Component { We also listen with a native listener on the document to get keydown events when no element is focused. Bubbling is irrelevant here as the target is the body element. */ - _onReactKeyDown = (ev) => { + private onReactKeyDown = (ev) => { // events caught while bubbling up on the root element // of this component, so something must be focused. - this._onKeyDown(ev); + this.onKeyDown(ev); }; - _onNativeKeyDown = (ev) => { + private onNativeKeyDown = (ev) => { // only pass this if there is no focused element. - // if there is, _onKeyDown will be called by the + // if there is, onKeyDown will be called by the // react keydown handler that respects the react bubbling order. if (ev.target === document.body) { - this._onKeyDown(ev); + this.onKeyDown(ev); } }; - _onKeyDown = (ev) => { + private onKeyDown = (ev) => { let handled = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); @@ -450,7 +440,7 @@ class LoggedInView extends React.Component { case RoomAction.JumpToFirstMessage: case RoomAction.JumpToLatestMessage: // pass the event down to the scroll panel - this._onScrollKeyPressed(ev); + this.onScrollKeyPressed(ev); handled = true; break; case RoomAction.FocusSearch: @@ -565,7 +555,7 @@ class LoggedInView extends React.Component { * dispatch a page-up/page-down/etc to the appropriate component * @param {Object} ev The key event */ - _onScrollKeyPressed = (ev) => { + private onScrollKeyPressed = (ev) => { if (this._roomView.current) { this._roomView.current.handleScrollKey(ev); } @@ -625,8 +615,8 @@ class LoggedInView extends React.Component { return (
@@ -644,7 +634,7 @@ class LoggedInView extends React.Component { - {audioFeedArraysForCalls} + { audioFeedArraysForCalls } ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 785838ffca..8cfe35c4cf 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillUpdate(props, state) { if (this.shouldTrackPageChange(this.state, state)) { this.startPageChangeTimer(); @@ -1112,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent { if (memberCount === 1) { warnings.push(( - {' '/* Whitespace, otherwise the sentences get smashed together */ } + { ' '/* Whitespace, otherwise the sentences get smashed together */ } { _t("You are the only person here. " + "If you leave, no one will be able to join in the future, including you.") } @@ -1127,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent { if (rule !== "public") { warnings.push(( - {' '/* Whitespace, otherwise the sentences get smashed together */ } + { ' '/* Whitespace, otherwise the sentences get smashed together */ } { isSpace ? _t("This space is not public. You will not be able to rejoin without an invite.") : _t("This room is not public. You will not be able to rejoin without an invite.") } @@ -1155,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent { : _t( "Are you sure you want to leave the room '%(roomName)s'?", { roomName: roomToLeave.name }, - )} + ) } { warnings } ), @@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: 'timeline_resize' }); } - onRoomCreated(roomId: string) { - dis.dispatch({ - action: "view_room", - room_id: roomId, - }); - } - onRegisterClick = () => { this.showScreen("register"); }; @@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent { {...this.state} ref={this.loggedInView} matrixClient={MatrixClientPeg.get()} - onRoomCreated={this.onRoomCreated} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} /> @@ -2053,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent { let errorBox; if (this.state.syncError && !isStoreError) { errorBox =
- {messageForSyncError(this.state.syncError)} + { messageForSyncError(this.state.syncError) }
; } view = (
- {errorBox} + { errorBox } - {_t('Logout')} + { _t('Logout') }
); @@ -2124,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent { } return - {view} + { view } ; } } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 8977549697..5bfe759508 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { replaceableComponent } from "../../utils/replaceableComponent"; import defaultDispatcher from '../../dispatcher/dispatcher'; +import CallEventGrouper from "./CallEventGrouper"; import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; import ScrollPanel, { IScrollState } from "./ScrollPanel"; import EventListSummary from '../views/elements/EventListSummary'; @@ -232,6 +233,9 @@ export default class MessagePanel extends React.Component { private readonly showTypingNotificationsWatcherRef: string; private eventNodes: Record; + // A map of + private callEventGroupers = new Map(); + constructor(props, context) { super(props, context); @@ -576,6 +580,20 @@ export default class MessagePanel extends React.Component { const last = (mxEv === lastShownEvent); const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); + if ( + mxEv.getType().indexOf("m.call.") === 0 || + mxEv.getType().indexOf("org.matrix.call.") === 0 + ) { + const callId = mxEv.getContent().call_id; + if (this.callEventGroupers.has(callId)) { + this.callEventGroupers.get(callId).add(mxEv); + } else { + const callEventGrouper = new CallEventGrouper(); + callEventGrouper.add(mxEv); + this.callEventGroupers.set(callId, callEventGrouper); + } + } + if (grouper) { if (grouper.shouldGroup(mxEv)) { grouper.add(mxEv, this.showHiddenEvents); @@ -653,8 +671,10 @@ export default class MessagePanel extends React.Component { } let willWantDateSeparator = false; + let lastInSection = true; if (nextEvent) { willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); + lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender(); } // is this a continuation of the previous message? @@ -690,6 +710,8 @@ export default class MessagePanel extends React.Component { // it's successful: we received it. isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); + const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id); + // use txnId as key if available so that we don't remount during sending ret.push( @@ -712,7 +734,7 @@ export default class MessagePanel extends React.Component { isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} last={last} - lastInSection={willWantDateSeparator} + lastInSection={lastInSection} lastSuccessful={isLastSuccessful} isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} @@ -720,6 +742,8 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} + callEventGrouper={callEventGrouper} + hideSender={this.props.room?.getMembers().length <= 2 && this.props.layout === Layout.Bubble} /> , ); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 87447b6aba..fca5613ede 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -121,7 +121,7 @@ export default class MyGroups extends React.Component { ) }
- {/*
+ { /*
@@ -137,7 +137,7 @@ export default class MyGroups extends React.Component { { 'i': (sub) => { sub } }) }
-
*/} +
*/ }
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index a2d419b4ba..6e914c40fb 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent { return (
- {React.createElement(t, {})} + { React.createElement(t, {}) }
); }); return (
- {toasts} + { toasts }
); } diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 8c8fab7ece..8abc161bab 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -35,8 +35,8 @@ interface IProps { export default class NotificationPanel extends React.PureComponent { render() { const emptyState = (
-

{_t('You’re all caught up')}

-

{_t('You have no visible notifications.')}

+

{ _t('You’re all caught up') }

+

{ _t('You have no visible notifications.') }

); let content; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 2a3448b017..95d70e913a 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { User } from "matrix-js-sdk/src/models/user"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -152,7 +153,7 @@ export default class RightPanel extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase + UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line if (newProps.groupId !== this.props.groupId) { this.unregisterGroupStore(); this.initGroupStore(newProps.groupId); @@ -174,7 +175,7 @@ export default class RightPanel extends React.Component { }); }; - private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => { + private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index aa5baaf8c2..84e8de8221 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -589,7 +589,7 @@ export default class RoomDirectory extends React.Component { // We use onMouseDown instead of onClick, so that we can avoid text getting selected return [
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomAvatar" > @@ -603,7 +603,7 @@ export default class RoomDirectory extends React.Component { />
,
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomDescription" > @@ -626,14 +626,14 @@ export default class RoomDirectory extends React.Component {
,
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomMemberCount" > { room.num_joined_members }
,
this.onRoomClicked(room, ev)} // cancel onMouseDown otherwise shift-clicking highlights text className="mx_RoomDirectory_preview" @@ -641,7 +641,7 @@ export default class RoomDirectory extends React.Component { { previewButton }
,
this.onRoomClicked(room, ev)} className="mx_RoomDirectory_join" > @@ -796,7 +796,7 @@ export default class RoomDirectory extends React.Component { showJoinButton={showJoinButton} initialText={this.props.initialText} /> - {dropdown} + { dropdown }
; } const explanation = @@ -814,16 +814,16 @@ export default class RoomDirectory extends React.Component { }) : _t("Explore rooms"); return (
- {explanation} + { explanation }
- {listHeader} - {content} + { listHeader } + { content }
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index e8080b4f7b..9acfb7bb8e 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -209,9 +209,9 @@ export default class RoomSearch extends React.PureComponent { return (
- {icon} - {input} - {clearButton} + { icon } + { input } + { clearButton }
); } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 80ea26c3f2..ac4d197346 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -222,17 +222,17 @@ export default class RoomStatusBar extends React.PureComponent { let buttonRow = <> - {_t("Delete all")} + { _t("Delete all") } - {_t("Retry all")} + { _t("Retry all") } ; if (this.state.isResending) { buttonRow = <> - {/* span for css */} - {_t("Sending")} + { /* span for css */ } + { _t("Sending") } ; } @@ -253,7 +253,7 @@ export default class RoomStatusBar extends React.PureComponent {
- {buttonRow} + { buttonRow }
@@ -270,10 +270,10 @@ export default class RoomStatusBar extends React.PureComponent { height="24" title="/!\ " alt="/!\ " />
- {_t('Connectivity to the server has been lost.')} + { _t('Connectivity to the server has been lost.') }
- {_t('Sent messages will be stored until your connection has returned.')} + { _t('Sent messages will be stored until your connection has returned.') }
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0c10a2aeca..7860e65362 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1892,10 +1892,10 @@ export default class RoomView extends React.Component { className="mx_RoomView_auxPanel_hiddenHighlights" onClick={this.onHiddenHighlightsClick} > - {_t( + { _t( "You have %(count)s unread notifications in a prior version of this room.", { count: hiddenHighlightCount }, - )} + ) } ); } @@ -2007,7 +2007,7 @@ export default class RoomView extends React.Component { onScroll={this.onMessageListScroll} onUserScroll={this.onUserScroll} onReadMarkerUpdated={this.updateTopUnreadMessagesBar} - showUrlPreview = {this.state.showUrlPreview} + showUrlPreview={this.state.showUrlPreview} className={messagePanelClassNames} membersLoaded={this.state.membersLoaded} permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} @@ -2057,7 +2057,7 @@ export default class RoomView extends React.Component { return (
- {showChatEffects && this.roomView.current && + { showChatEffects && this.roomView.current && } @@ -2076,22 +2076,22 @@ export default class RoomView extends React.Component { />
- {auxPanel} + { auxPanel }
- {fileDropTarget} - {topUnreadMessagesBar} - {jumpToBottom} - {messagePanel} - {searchResultsPanel} + { fileDropTarget } + { topUnreadMessagesBar } + { jumpToBottom } + { messagePanel } + { searchResultsPanel }
- {statusBar} + { statusBar }
- {previewBar} - {messageComposer} + { previewBar } + { messageComposer }
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 5c966d2d3a..3cf4b9b593 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -136,7 +136,7 @@ export default class SearchBox extends React.Component { key="button" tabIndex={-1} className="mx_SearchBox_closeButton" - onClick={ () => {this._clearSearch("button"); } }> + onClick={() => {this._clearSearch("button"); }}> ) : undefined; // show a shorter placeholder when blurred, if requested @@ -153,12 +153,12 @@ export default class SearchBox extends React.Component { type="text" ref={this._search} className={"mx_textinput_icon mx_textinput_search " + className} - value={ this.state.searchTerm } - onFocus={ this._onFocus } - onChange={ this.onChange } - onKeyDown={ this._onKeyDown } + value={this.state.searchTerm} + onFocus={this._onFocus} + onChange={this.onChange} + onKeyDown={this._onKeyDown} onBlur={this._onBlur} - placeholder={ placeholder } + placeholder={placeholder} autoComplete="off" autoFocus={this.props.autoFocus} /> diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 27539a5c3c..038c1df514 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -404,7 +404,7 @@ export const SpaceHierarchy: React.FC = ({ const [saving, setSaving] = useState(false); if (summaryError) { - return

{_t("Your server does not support showing space hierarchies.")}

; + return

{ _t("Your server does not support showing space hierarchies.") }

; } let content; @@ -569,7 +569,7 @@ export const SpaceHierarchy: React.FC = ({ return <> = ({ space, onFinished, initialText } { _t("If you can't find the room you're looking for, ask for an invite or create a new room.", null, { a: sub => { - return {sub}; + return { sub }; } }, ) } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0ee68a9578..06b2f4a629 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -146,7 +146,7 @@ const SpaceInfo = ({ space }) => { return
{ visibilitySection } { joinRule === "public" && - {(count) => count > 0 ? ( + { (count) => count > 0 ? ( { @@ -159,7 +159,7 @@ const SpaceInfo = ({ space }) => { > { _t("%(count)s members", { count }) } - ) : null} + ) : null } }
; }; @@ -292,7 +292,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => - {(topic, ref) => + { (topic, ref) =>
{ topic }
@@ -419,12 +419,12 @@ const SpaceLanding = ({ space }) => {
- {(name) => { + { (name) => { const tags = { name: () =>

{ name }

}; return _t("Welcome to ", {}, tags) as JSX.Element; - }} + } }
@@ -434,11 +434,11 @@ const SpaceLanding = ({ space }) => { { settingsButton }
- {(topic, ref) => ( + { (topic, ref) => (
{ topic }
- )} + ) }

@@ -458,7 +458,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const numFields = 3; const placeholders = [_t("General"), _t("Random"), _t("Support")]; const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]); - const fields = new Array(numFields).fill(0).map((_, i) => { + const fields = new Array(numFields).fill(0).map((x, i) => { const name = "roomName" + i; return { const numFields = 3; const fieldRefs: RefObject[] = [useRef(), useRef(), useRef()]; const [emailAddresses, setEmailAddress] = useStateArray(numFields, ""); - const fields = new Array(numFields).fill(0).map((_, i) => { + const fields = new Array(numFields).fill(0).map((x, i) => { const name = "emailAddress" + i; return { tabLocation: TabLocation.LEFT, }; - private _getActiveTabIndex() { + private getActiveTabIndex() { if (!this.state || !this.state.activeTabIndex) return 0; return this.state.activeTabIndex; } @@ -84,7 +84,7 @@ export default class TabbedView extends React.Component { * @param {Tab} tab the tab to show * @private */ - private _setActiveTab(tab: Tab) { + private setActiveTab(tab: Tab) { const idx = this.props.tabs.indexOf(tab); if (idx !== -1) { if (this.props.onChange) this.props.onChange(tab.id); @@ -94,23 +94,23 @@ export default class TabbedView extends React.Component { } } - private _renderTabLabel(tab: Tab) { + private renderTabLabel(tab: Tab) { let classes = "mx_TabbedView_tabLabel "; const idx = this.props.tabs.indexOf(tab); - if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active"; + if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active"; let tabIcon = null; if (tab.icon) { tabIcon = ; } - const onClickHandler = () => this._setActiveTab(tab); + const onClickHandler = () => this.setActiveTab(tab); const label = _t(tab.label); return ( - {tabIcon} + { tabIcon } { label } @@ -118,19 +118,19 @@ export default class TabbedView extends React.Component { ); } - private _renderTabPanel(tab: Tab): React.ReactNode { + private renderTabPanel(tab: Tab): React.ReactNode { return (
- {tab.body} + { tab.body }
); } public render(): React.ReactNode { - const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); - const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); + const labels = this.props.tabs.map(tab => this.renderTabLabel(tab)); + const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]); const tabbedViewClasses = classNames({ 'mx_TabbedView': true, @@ -141,9 +141,9 @@ export default class TabbedView extends React.Component { return (
- {labels} + { labels }
- {panel} + { panel }
); } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 59930bf41d..c4210c68a8 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -277,7 +277,7 @@ class TimelinePanel extends React.Component { } // TODO: [REACT-WARNING] Move into constructor - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillMount() { if (this.props.manageReadReceipts) { this.updateReadReceiptOnUserActivity(); @@ -290,7 +290,7 @@ class TimelinePanel extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase + // eslint-disable-next-line UNSAFE_componentWillReceiveProps(newProps) { if (newProps.timelineSet !== this.props.timelineSet) { // throw new Error("changing timelineSet on a TimelinePanel is not supported"); @@ -1448,7 +1448,7 @@ class TimelinePanel extends React.Component { if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) { return (
-
{this.props.empty}
+
{ this.props.empty }
); } diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 79a73735f4..b7b0b7c652 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -37,14 +37,14 @@ export default class ToastContainer extends React.Component<{}, IState> { // toasts may dismiss themselves in their didMount if they find // they're already irrelevant by the time they're mounted, and // our own componentDidMount is too late. - ToastStore.sharedInstance().on('update', this._onToastStoreUpdate); + ToastStore.sharedInstance().on('update', this.onToastStoreUpdate); } componentWillUnmount() { - ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate); + ToastStore.sharedInstance().removeListener('update', this.onToastStoreUpdate); } - _onToastStoreUpdate = () => { + private onToastStoreUpdate = () => { this.setState({ toasts: ToastStore.sharedInstance().getToasts(), countSeen: ToastStore.sharedInstance().getCountSeen(), @@ -75,10 +75,10 @@ export default class ToastContainer extends React.Component<{}, IState> { }); toast = (
-

{title}

- {countIndicator} +

{ title }

+ { countIndicator }
-
{React.createElement(component, toastProps)}
+
{ React.createElement(component, toastProps) }
); containerClasses = classNames("mx_ToastContainer", { @@ -88,7 +88,7 @@ export default class ToastContainer extends React.Component<{}, IState> { return toast ? (
- {toast} + { toast }
) : null; diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index c8e90a1c0a..6ee53da5d1 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -104,7 +104,7 @@ export default class UploadBar extends React.Component { const uploadSize = filesize(this.state.currentUpload.total); return (
-
{uploadText} ({uploadSize})
+
{ uploadText } ({ uploadSize })
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 34575ba582..0a30367e4b 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -342,20 +342,20 @@ export default class UserMenu extends React.Component { if (MatrixClientPeg.get().isGuest()) { topSection = (
- {_t("Got an account? Sign in", {}, { + { _t("Got an account? Sign in", {}, { a: sub => ( - {sub} + { sub } ), - })} - {_t("New here? Create an account", {}, { + }) } + { _t("New here? Create an account", {}, { a: sub => ( - {sub} + { sub } ), - })} + }) }
); } else if (hostSignupConfig) { @@ -394,17 +394,17 @@ export default class UserMenu extends React.Component { let primaryHeader = (
- {OwnProfileStore.instance.displayName} + { OwnProfileStore.instance.displayName } - {MatrixClientPeg.get().getUserId()} + { MatrixClientPeg.get().getUserId() }
); let primaryOptionList = ( - {homeButton} + { homeButton } { label={_t("All settings")} onClick={(e) => this.onSettingsOpen(e, null)} /> - {/* */} + /> */ } { feedbackButton } @@ -443,7 +443,7 @@ export default class UserMenu extends React.Component { primaryHeader = (
- {prototypeCommunityName} + { prototypeCommunityName }
); @@ -470,13 +470,13 @@ export default class UserMenu extends React.Component { } primaryOptionList = ( - {settingsOption} + { settingsOption } - {inviteOption} + { inviteOption } ); secondarySection = ( @@ -485,10 +485,10 @@ export default class UserMenu extends React.Component {
- {OwnProfileStore.instance.displayName} + { OwnProfileStore.instance.displayName } - {MatrixClientPeg.get().getUserId()} + { MatrixClientPeg.get().getUserId() }
@@ -540,7 +540,7 @@ export default class UserMenu extends React.Component { className={classes} >
- {primaryHeader} + { primaryHeader } { />
- {topSection} - {primaryOptionList} - {secondarySection} + { topSection } + { primaryOptionList } + { secondarySection } ; }; @@ -570,27 +570,27 @@ export default class UserMenu extends React.Component { let isPrototype = false; let menuName = _t("User menu"); - let name = {displayName}; + let name = { displayName }; let buttons = ( - {/* masked image in CSS */} + { /* masked image in CSS */ } ); let dnd; if (this.state.selectedSpace) { name = (
- {displayName} + { displayName } - {(roomName) => {roomName}} + { (roomName) => { roomName } }
); } else if (prototypeCommunityName) { name = (
- {prototypeCommunityName} - {displayName} + { prototypeCommunityName } + { displayName }
); menuName = _t("Community and user menu"); @@ -598,8 +598,8 @@ export default class UserMenu extends React.Component { } else if (SettingsStore.getValue("feature_communities_v2_prototypes")) { name = (
- {_t("Home")} - {displayName} + { _t("Home") } + { displayName }
); isPrototype = true; @@ -647,20 +647,20 @@ export default class UserMenu extends React.Component { className="mx_UserMenu_userAvatar" />
- {name} - {this.state.pendingRoomJoin.size > 0 && ( + { name } + { this.state.pendingRoomJoin.size > 0 && ( - )} - {dnd} - {buttons} + ) } + { dnd } + { buttons }
- {this.renderContextMenu()} + { this.renderContextMenu() } ); } diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index b69a92dd61..2bfa20e892 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -63,23 +63,23 @@ export default class ViewSource extends React.Component { <>
- {_t("Decrypted event source")} + { _t("Decrypted event source") } - {JSON.stringify(decryptedEventSource, null, 2)} + { JSON.stringify(decryptedEventSource, null, 2) }
- {_t("Original event source")} + { _t("Original event source") } - {JSON.stringify(originalEventSource, null, 2)} + { JSON.stringify(originalEventSource, null, 2) }
); } else { return ( <> -
{_t("Original event source")}
- {JSON.stringify(originalEventSource, null, 2)} +
{ _t("Original event source") }
+ { JSON.stringify(originalEventSource, null, 2) } ); } @@ -110,7 +110,7 @@ export default class ViewSource extends React.Component { if (isStateEvent) { return ( - {(cli) => ( + { (cli) => ( - )} + ) } ); } else { @@ -142,7 +142,7 @@ export default class ViewSource extends React.Component { }; return ( - {(cli) => ( + { (cli) => ( - )} + ) } ); } @@ -176,16 +176,16 @@ export default class ViewSource extends React.Component { return (
-
Room ID: {roomId}
-
Event ID: {eventId}
+
Room ID: { roomId }
+
Event ID: { eventId }
- {isEditing ? this.editSourceContent() : this.viewSourceContent()} + { isEditing ? this.editSourceContent() : this.viewSourceContent() }
- {!isEditing && canEdit && ( + { !isEditing && canEdit && (
- +
- )} + ) } ); } 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..3755505f3d 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,8 +312,8 @@ 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 }) }
@@ -322,12 +322,12 @@ export default class ForgotPassword extends React.Component { 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 +358,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..6a3d339681 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 }, - )} + ) }
); @@ -565,7 +565,7 @@ export default class LoginComponent extends React.PureComponent }); serverDeadSection = (
- {this.state.serverDeadError} + { this.state.serverDeadError }
); } @@ -578,15 +578,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 +596,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..549e47260f 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,43 @@ 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")} + { _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..748b1c9ffc 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -101,11 +101,11 @@ export default class AudioPlayer extends React.PureComponent { />
- {this.props.mediaName || _t("Unnamed audio")} + { this.props.mediaName || _t("Unnamed audio") }
-   {/* easiest way to introduce a gap between the components */} +   { /* easiest way to introduce a gap between the components */ } { this.renderFileSize() }
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/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 3b7a881754..8a4427fd01 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -47,7 +47,7 @@ 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({ @@ -57,7 +57,7 @@ export default class Waveform extends React.PureComponent { 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..763ce10cd9 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -417,12 +417,12 @@ export class TermsAuthEntry extends React.Component{_t("Accept")}; + onClick={this.trySubmit} disabled={!allChecked}>{ _t("Accept") }; } 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,7 +613,7 @@ export class MsisdnAuthEntry extends React.Component
- {errorSection} + { errorSection }
); @@ -717,21 +717,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 +753,8 @@ export class SSOAuthEntry extends React.Component { errorSection }
- {cancelButton} - {continueButton} + { cancelButton } + { continueButton }
; } @@ -825,7 +825,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/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/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/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx index 76e1670669..a61cdeedd3 100644 --- a/src/components/views/context_menus/CallContextMenu.tsx +++ b/src/components/views/context_menus/CallContextMenu.tsx @@ -65,15 +65,15 @@ export default class CallContextMenu extends React.Component { 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/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") } ; } @@ -130,8 +130,8 @@ export default class StatusMessageContextMenu extends React.Component { onChange={this._onStatusChange} />
- {actionButton} - {spinner} + { actionButton } + { spinner }
; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 5024b98def..0f78b971eb 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -221,7 +221,7 @@ export const AddExistingToSpace: React.FC = ({ return
- +
; } @@ -690,7 +690,7 @@ export default class AddressPickerDialog extends React.Component { && this.props.validAddressTypes.includes('email')) { const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); if (defaultIdentityServerUrl) { - identityServer =
{_t( + identityServer =
{ _t( "Use an identity server to invite by email. " + "Use the default (%(defaultIdentityServerName)s) " + "or manage in Settings.", @@ -698,25 +698,25 @@ 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..8ccc485d7c 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -149,7 +149,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..917004dbc7 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -69,7 +69,7 @@ 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); diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 6baf24f797..64e984fe20 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 } ...
    ); } @@ -221,7 +221,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..73fd4def25 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")} @@ -207,16 +207,16 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< onClick={this.onShowMorePeople} kind="link" key="more" className="mx_CommunityPrototypeInviteDialog_morePeople" - >{_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 +236,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< >
    - {emailAddresses} - {peopleIntro} - {people} + { emailAddresses } + { peopleIntro } + { people } {buttonText} + >{ buttonText }
    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,13 +193,13 @@ 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") }
    @@ -212,12 +212,12 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent< onClick={this.onChangeAvatar} className="mx_CreateCommunityPrototypeDialog_avatarContainer" > - {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..88ae801441 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); }; @@ -167,7 +167,7 @@ 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..6d75b94c70 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -224,15 +224,15 @@ export default class CreateRoomDialog extends React.Component { ); } - let publicPrivateLabel =

    {_t( + let publicPrivateLabel =

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

    ; + ) }

    ; if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { - publicPrivateLabel =

    {_t( + publicPrivateLabel =

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

    ; + ) }

    ; } let e2eeSection; @@ -250,7 +250,7 @@ export default class CreateRoomDialog extends React.Component { } e2eeSection = { onChange={this.onNoFederateChange} value={this.state.noFederate} /> -

    {federateLabel}

    +

    { federateLabel }

    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..61cda796ee 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -337,7 +337,7 @@ class FilteredList extends React.PureComponent - {eventType} + { eventType } ; }) } @@ -726,17 +726,17 @@ const VerificationRequestExplorer: React.FC<{ return (
    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 +771,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 +844,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 +865,17 @@ class WidgetExplorer extends React.Component
    - {widgets.map(w => { + { widgets.map(w => { return ; - })} + >{ w.url }; + }) }
    - +
    ); } @@ -1007,7 +1007,7 @@ class SettingsExplorer extends React.PureComponent{canEdit.toString()}; + return
    ; } render() { @@ -1028,17 +1028,17 @@ class SettingsExplorer extends React.PureComponent - - - + + + - {allSettings.map(i => ( + { allSettings.map(i => ( - ))} + )) }
    {_t( + { _t( customVariables[row[0]].expl, customVariables[row[0]].getTextVariables ? customVariables[row[0]].getTextVariables() : null, - )}{ row[1] }
    { canEdit.toString() }
    {_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)} className='mx_DevTools_SettingsExplorer_edit' @@ -1047,20 +1047,20 @@ class SettingsExplorer extends React.PureComponent - {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,36 +1068,36 @@ 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 }
    @@ -1122,8 +1122,8 @@ class SettingsExplorer extends React.PureComponent
    - - + +
    ); @@ -1131,39 +1131,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 +1171,7 @@ class SettingsExplorer extends React.PureComponent this.onEditClick(e, this.state.viewSetting)}>{ _t("Edit Values") } - + ); @@ -1232,12 +1232,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..1eabb68081 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -151,16 +151,16 @@ 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/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 &&