diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c9d11f02c8..e9ede862d2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,15 @@ - + - + + + diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 3c3807e33b..0ae59da09a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -1,5 +1,8 @@ name: Develop on: + # These tests won't work for non-develop branches at the moment as they + # won't pull in the right versions of other repos, so they're only enabled + # on develop. push: branches: [develop] pull_request: diff --git a/.gitignore b/.gitignore index 50aa10fbfd..102f4b5ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ package-lock.json .DS_Store *.tmp + +.vscode +.vscode/ 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/CONTRIBUTING.rst b/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.rst rename to CONTRIBUTING.md diff --git a/__mocks__/FontManager.js b/__mocks__/FontManager.js new file mode 100644 index 0000000000..41eab4bf94 --- /dev/null +++ b/__mocks__/FontManager.js @@ -0,0 +1,6 @@ +// Stub out FontManager for tests as it doesn't validate anything we don't already know given +// our fixed test environment and it requires the installation of node-canvas. + +module.exports = { + fixupColorFonts: () => Promise.resolve(), +}; diff --git a/__mocks__/workerMock.js b/__mocks__/workerMock.js new file mode 100644 index 0000000000..6ee585673e --- /dev/null +++ b/__mocks__/workerMock.js @@ -0,0 +1 @@ +module.exports = jest.fn(); diff --git a/package.json b/package.json index 610404ba0d..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", @@ -54,7 +55,6 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@types/commonmark": "^0.27.4", "await-lock": "^2.1.0", "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", @@ -65,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", @@ -80,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", @@ -92,6 +92,7 @@ "re-resizable": "^6.9.0", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.0", + "react-blurhash": "^0.1.3", "react-dom": "^17.0.2", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", @@ -124,7 +125,9 @@ "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", + "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", + "@types/css-font-loading-module": "^0.0.6", "@types/diff-match-patch": "^1.0.32", "@types/flux": "^3.1.9", "@types/jest": "^26.0.20", @@ -185,7 +188,8 @@ "\\$webapp/i18n/languages.json": "/__mocks__/languages.json", "decoderWorker\\.min\\.js": "/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", - "waveWorker\\.min\\.js": "/__mocks__/empty.js" + "waveWorker\\.min\\.js": "/__mocks__/empty.js", + "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js" }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" diff --git a/res/css/_components.scss b/res/css/_components.scss index 389be11c60..f9e3ab1160 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -86,6 +86,7 @@ @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; +@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @@ -120,6 +121,7 @@ @import "./views/elements/_AddressTile.scss"; @import "./views/elements/_DesktopBuildsNotice.scss"; @import "./views/elements/_DesktopCapturerSourcePicker.scss"; +@import "./views/elements/_DialPadBackspaceButton.scss"; @import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @@ -148,6 +150,7 @@ @import "./views/elements/_StyledCheckbox.scss"; @import "./views/elements/_StyledRadioButton.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TagComposer.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_Tooltip.scss"; @@ -157,18 +160,19 @@ @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; +@import "./views/messages/_CallEvent.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MImageReplyBody.scss"; @import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; -@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @@ -197,10 +201,12 @@ @import "./views/rooms/_E2EIcon.scss"; @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; +@import "./views/rooms/_EventBubbleTile.scss"; @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; +@import "./views/rooms/_LinkPreviewGroup.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberList.scss"; @@ -211,6 +217,7 @@ @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; +@import "./views/rooms/_ReplyTile.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @@ -259,6 +266,7 @@ @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; +@import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; @import "./views/voip/_DialPad.scss"; diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 7b975110e1..c180a8a02d 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -45,9 +45,14 @@ limitations under the License. /* Overrides for the attachment body tiles */ -.mx_FilePanel .mx_EventTile { +.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) { word-break: break-word; - margin-top: 32px; + margin-top: 10px; + padding-top: 0; + + .mx_EventTile_line { + padding-left: 0; + } } .mx_FilePanel .mx_EventTile .mx_MImageBody { @@ -118,10 +123,6 @@ limitations under the License. padding-left: 0px; } -.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line { - background-color: $primary-bg-color; -} - .mx_FilePanel_empty::before { mask-image: url('$(res)/img/element-icons/room/files.svg'); } diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index e54feca175..d271cd2bcc 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -84,7 +84,7 @@ limitations under the License. display: inline; } -.mx_NotificationPanel .mx_EventTile_senderDetails { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails { padding-left: 36px; // align with the room name position: relative; @@ -105,7 +105,7 @@ limitations under the License. padding-left: 5px; } -.mx_NotificationPanel .mx_EventTile_line { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line { margin-right: 0px; padding-left: 36px; // align with the room name padding-top: 0px; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 48b565be7f..e4832d9430 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_landing { + display: flex; + flex-direction: column; + > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { border-radius: 12px; @@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SearchBox { margin: 0 0 20px; + flex: 0; } .mx_SpaceFeedbackPrompt { @@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px; display: none; } } + + .mx_SpaceRoomDirectory_list { + // we don't want this container to get forced into the flexbox layout + display: contents; + } } .mx_SpaceRoomView_privateScope { diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 39a8ebed32..833450a25b 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd +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. @@ -20,7 +21,6 @@ limitations under the License. padding: 0 0 0 16px; display: flex; flex-direction: column; - position: absolute; top: 0; bottom: 0; left: 0; @@ -28,11 +28,93 @@ limitations under the License. margin-top: 8px; } +.mx_TabbedView_tabsOnLeft { + flex-direction: column; + position: absolute; + + .mx_TabbedView_tabLabels { + width: 170px; + max-width: 170px; + position: fixed; + } + + .mx_TabbedView_tabPanel { + margin-left: 240px; // 170px sidebar + 70px padding + flex-direction: column; + } + + .mx_TabbedView_tabLabel_active { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $tab-label-active-icon-bg-color; + } + + .mx_TabbedView_maskedIcon { + width: 16px; + height: 16px; + margin-left: 8px; + margin-right: 16px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 16px; + width: 16px; + height: 16px; + } +} + +.mx_TabbedView_tabsOnTop { + flex-direction: column; + + .mx_TabbedView_tabLabels { + display: flex; + margin-bottom: 8px; + } + + .mx_TabbedView_tabLabel { + padding-left: 0px; + padding-right: 52px; + + .mx_TabbedView_tabLabel_text { + font-size: 15px; + color: $tertiary-fg-color; + } + } + + .mx_TabbedView_tabPanel { + flex-direction: row; + } + + .mx_TabbedView_tabLabel_active { + color: $accent-color; + .mx_TabbedView_tabLabel_text { + color: $accent-color; + } + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $accent-color; + } + + .mx_TabbedView_maskedIcon { + width: 22px; + height: 22px; + margin-left: 0px; + margin-right: 8px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 22px; + width: inherit; + height: inherit; + } +} + .mx_TabbedView_tabLabels { - width: 170px; - max-width: 170px; color: $tab-label-fg-color; - position: fixed; } .mx_TabbedView_tabLabel { @@ -46,43 +128,25 @@ limitations under the License. position: relative; } -.mx_TabbedView_tabLabel_active { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} - .mx_TabbedView_maskedIcon { - margin-left: 8px; - margin-right: 16px; - width: 16px; - height: 16px; display: inline-block; } .mx_TabbedView_maskedIcon::before { display: inline-block; - background-color: $tab-label-icon-bg-color; + background-color: $icon-button-color; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; mask-position: center; content: ''; } -.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: $tab-label-active-icon-bg-color; -} - .mx_TabbedView_tabLabel_text { vertical-align: middle; } .mx_TabbedView_tabPanel { - margin-left: 240px; // 170px sidebar + 70px padding flex-grow: 1; display: flex; - flex-direction: column; min-height: 0; // firefox } diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 9a65ad008f..77dcebbb9a 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AudioPlayer_container { +.mx_MediaBody.mx_AudioPlayer_container { padding: 16px 12px 12px 12px; - max-width: 267px; // use max to make the control fit in the files/pinned panels .mx_AudioPlayer_primaryContainer { display: flex; diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index fd01864bba..773fc50fb9 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -18,10 +18,10 @@ limitations under the License. // are shared amongst multiple voice message components. // Container for live recording and playback controls -.mx_VoiceMessagePrimaryContainer { - // 7px top and bottom for visual design. 12px left & right, but the waveform (right) - // has a 1px padding on it that we want to account for. - padding: 7px 12px 7px 11px; +.mx_MediaBody.mx_VoiceMessagePrimaryContainer { + // The waveform (right) has a 1px padding on it that we want to account for, otherwise + // inherit from mx_MediaBody + padding-right: 11px; // Cheat at alignment a bit display: flex; @@ -49,4 +49,8 @@ limitations under the License. padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. padding-left: 8px; // isolate from recording circle / play control } + + &.mx_VoiceMessagePrimaryContainer_noWaveform { + max-width: 162px; // with all the padding this results in 185px wide + } } diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 2678f7b4ad..5321d8ff69 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -65,7 +65,7 @@ limitations under the License. .mx_CreateRoomDialog_aliasContainer { display: flex; // put margin on container so it can collapse with siblings - margin: 10px 0; + margin: 24px 0 10px; .mx_RoomAliasField { margin: 0; @@ -101,10 +101,6 @@ limitations under the License. margin-left: 30px; } - .mx_CreateRoomDialog_topic { - margin-bottom: 36px; - } - .mx_Dialog_content > .mx_SettingsFlag { margin-top: 24px; } @@ -113,5 +109,56 @@ limitations under the License. margin: 0 85px 0 0; font-size: $font-12px; } + + .mx_Dropdown { + margin-bottom: 8px; + font-weight: normal; + font-family: $font-family; + font-size: $font-14px; + color: $primary-fg-color; + + .mx_Dropdown_input { + border: 1px solid $input-border-color; + } + + .mx_Dropdown_option { + font-size: $font-14px; + line-height: $font-32px; + height: 32px; + min-height: 32px; + + > div { + padding-left: 30px; + position: relative; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 6px; + top: 8px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $secondary-fg-color; + } + } + } + + .mx_CreateRoomDialog_dropdown_invite::before { + mask-image: url('$(res)/img/element-icons/lock.svg'); + mask-size: contain; + } + + .mx_CreateRoomDialog_dropdown_public::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 12px; + } + + .mx_CreateRoomDialog_dropdown_restricted::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-size: contain; + } + } } diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 95d7ce74c4..e018f60172 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -36,6 +36,10 @@ limitations under the License. flex-shrink: 0; overflow-y: auto; + .mx_EventTile[data-layout=bubble] { + margin-top: 20px; + } + div { pointer-events: none; } diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index c01b43c1c4..9fc4b7a15c 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InviteDialog_transferWrapper .mx_Dialog { + padding-bottom: 16px; +} + .mx_InviteDialog_addressBar { display: flex; flex-direction: row; @@ -286,16 +290,41 @@ limitations under the License. } } -.mx_InviteDialog { +.mx_InviteDialog_other { // Prevent the dialog from jumping around randomly when elements change. height: 600px; padding-left: 20px; // the design wants some padding on the left - display: flex; + + .mx_InviteDialog_userSections { + height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements + } +} + +.mx_InviteDialog_content { + height: calc(100% - 36px); // full height minus the size of the header + overflow: hidden; +} + +.mx_InviteDialog_transfer { + width: 496px; + height: 466px; flex-direction: column; .mx_InviteDialog_content { - overflow: hidden; - height: 100%; + flex-direction: column; + + .mx_TabbedView { + height: calc(100% - 60px); + } + overflow: visible; + } + + .mx_InviteDialog_addressBar { + margin-top: 8px; + } + + input[type="checkbox"] { + margin-right: 8px; } } @@ -303,7 +332,6 @@ limitations under the License. margin-top: 4px; overflow-y: auto; padding: 0 45px 4px 0; - height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements } .mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { @@ -318,6 +346,74 @@ limitations under the License. padding: 0; } +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField { + border-top: 0; + border-left: 0; + border-right: 0; + border-radius: 0; + margin-top: 0; + border-color: $quaternary-fg-color; + + input { + font-size: 18px; + font-weight: 600; + padding-top: 0; + } +} + +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within { + border-color: $accent-color; +} + +.mx_InviteDialog_dialPadField .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; +} + +.mx_InviteDialog_dialPad { + width: 224px; + margin-top: 16px; + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_dialPad .mx_DialPad { + row-gap: 16px; + column-gap: 48px; + + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_transferConsultConnect { + padding-top: 16px; + /* This wants a drop shadow the full width of the dialog, so relative-position it + * and make it wider, then compensate with padding + */ + position: relative; + width: 496px; + left: -24px; + padding-left: 24px; + padding-right: 24px; + border-top: 1px solid $message-body-panel-bg-color; + + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_InviteDialog_transferConsultConnect_pushRight { + margin-left: auto; +} + +.mx_InviteDialog_userDirectoryIcon::before { + mask-image: url('$(res)/img/voip/tab-userdirectory.svg'); +} + +.mx_InviteDialog_dialPadIcon::before { + mask-image: url('$(res)/img/voip/tab-dialpad.svg'); +} + .mx_InviteDialog_multiInviterError { > h4 { font-size: $font-15px; diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss new file mode 100644 index 0000000000..91df76675a --- /dev/null +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -0,0 +1,150 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_ManageRestrictedJoinRuleDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_ManageRestrictedJoinRuleDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 60vh; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ManageRestrictedJoinRuleDialog_content { + flex-grow: 1; + } + + .mx_ManageRestrictedJoinRuleDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_ManageRestrictedJoinRuleDialog_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry { + display: flex; + margin-top: 12px; + + > div { + flex-grow: 1; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 4px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_name { + margin: 0 8px; + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_description { + margin-top: 8px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_info { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_ManageRestrictedJoinRuleDialog_footer { + margin-top: 20px; + + .mx_ManageRestrictedJoinRuleDialog_footer_buttons { + display: flex; + width: max-content; + margin-left: auto; + + .mx_AccessibleButton { + display: inline-block; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 2997c83cfd..7bc47a3c98 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -72,7 +72,7 @@ limitations under the License. .mx_AccessibleButton_kind_danger_outline { color: $button-danger-bg-color; - background-color: $button-secondary-bg-color; + background-color: transparent; border: 1px solid $button-danger-bg-color; } diff --git a/res/css/views/elements/_DialPadBackspaceButton.scss b/res/css/views/elements/_DialPadBackspaceButton.scss new file mode 100644 index 0000000000..40e4af7025 --- /dev/null +++ b/res/css/views/elements/_DialPadBackspaceButton.scss @@ -0,0 +1,40 @@ +/* +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_DialPadBackspaceButton { + position: relative; + height: 28px; + width: 28px; + + &::before { + /* force this element to appear on the DOM */ + content: ""; + + background-color: #8D97A5; + width: inherit; + height: inherit; + top: 0px; + left: 0px; + position: absolute; + display: inline-block; + vertical-align: middle; + + mask-image: url('$(res)/img/element-icons/call/delete.svg'); + mask-position: 8px; + mask-size: 20px; + mask-repeat: no-repeat; + } +} diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 2a2508c17c..3b67e0191e 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -27,7 +27,7 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 3px; + border-radius: 4px; border: 1px solid $strong-input-border-color; font-size: $font-12px; user-select: none; @@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus { z-index: 2; margin: 0; padding: 0px; - border-radius: 3px; + border-radius: 4px; border: 1px solid $input-focused-border-color; background-color: $primary-bg-color; max-height: 200px; diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index da23957b36..cf92ffec64 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +$button-size: 32px; +$icon-size: 22px; +$button-gap: 24px; + .mx_ImageView { display: flex; width: 100%; @@ -66,16 +70,17 @@ limitations under the License. pointer-events: initial; display: flex; align-items: center; + gap: calc($button-gap - ($button-size - $icon-size)); } .mx_ImageView_button { - margin-left: 24px; + padding: calc(($button-size - $icon-size) / 2); display: block; &::before { content: ''; - height: 22px; - width: 22px; + height: $icon-size; + width: $icon-size; mask-repeat: no-repeat; mask-size: contain; mask-position: center; @@ -109,11 +114,12 @@ limitations under the License. } .mx_ImageView_button_close { + padding: calc($button-size - $button-size); border-radius: 100%; background: #21262c; // same on all themes &::before { - width: 32px; - height: 32px; + width: $button-size; + height: $button-size; mask-image: url('$(res)/img/image-view/close.svg'); mask-size: 40%; } diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss index 5858a60629..5329e7f1f8 100644 --- a/res/css/views/elements/_InfoTooltip.scss +++ b/res/css/views/elements/_InfoTooltip.scss @@ -30,5 +30,12 @@ limitations under the License. mask-position: center; content: ''; vertical-align: middle; +} + +.mx_InfoTooltip_icon_info::before { mask-image: url('$(res)/img/element-icons/info.svg'); } + +.mx_InfoTooltip_icon_warning::before { + mask-image: url('$(res)/img/element-icons/warning.svg'); +} diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index bf44a11728..032cb49359 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -16,22 +16,46 @@ limitations under the License. .mx_ReplyThread { margin-top: 0; -} - -.mx_ReplyThread .mx_DateSeparator { - font-size: 1em !important; - margin-top: 0; - margin-bottom: 0; - padding-bottom: 1px; - bottom: -5px; -} - -.mx_ReplyThread_show { - cursor: pointer; -} - -blockquote.mx_ReplyThread { margin-left: 0; - padding-left: 10px; - border-left: 4px solid $blockquote-bar-color; + margin-right: 0; + margin-bottom: 8px; + padding: 0 10px; + border-left: 2px solid $button-bg-color; + border-radius: 2px; + + .mx_ReplyThread_show { + cursor: pointer; + } + + &.mx_ReplyThread_color1 { + border-left-color: $username-variant1-color; + } + + &.mx_ReplyThread_color2 { + border-left-color: $username-variant2-color; + } + + &.mx_ReplyThread_color3 { + border-left-color: $username-variant3-color; + } + + &.mx_ReplyThread_color4 { + border-left-color: $username-variant4-color; + } + + &.mx_ReplyThread_color5 { + border-left-color: $username-variant5-color; + } + + &.mx_ReplyThread_color6 { + border-left-color: $username-variant6-color; + } + + &.mx_ReplyThread_color7 { + border-left-color: $username-variant7-color; + } + + &.mx_ReplyThread_color8 { + border-left-color: $username-variant8-color; + } } diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss index 62fb5c5512..1ae787dfc2 100644 --- a/res/css/views/elements/_StyledRadioButton.scss +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -46,7 +46,7 @@ limitations under the License. width: $font-16px; } - > input[type=radio] { + input[type=radio] { // Remove the OS's representation margin: 0; padding: 0; @@ -112,6 +112,12 @@ limitations under the License. } } } + + .mx_RadioButton_innerLabel { + display: flex; + position: relative; + top: 4px; + } } .mx_RadioButton_outlined { diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss new file mode 100644 index 0000000000..2ffd601765 --- /dev/null +++ b/res/css/views/elements/_TagComposer.scss @@ -0,0 +1,77 @@ +/* +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_TagComposer { + .mx_TagComposer_input { + display: flex; + + .mx_Field { + flex: 1; + margin: 0; // override from field styles + } + + .mx_AccessibleButton { + min-width: 70px; + padding: 0; // override from button styles + margin-left: 16px; // distance from + } + + .mx_Field, .mx_Field input, .mx_AccessibleButton { + // So they look related to each other by feeling the same + border-radius: 8px; + } + } + + .mx_TagComposer_tags { + display: flex; + flex-wrap: wrap; + margin-top: 12px; // this plus 12px from the tags makes 24px from the input + + .mx_TagComposer_tag { + padding: 6px 8px 8px 12px; + position: relative; + margin-right: 12px; + margin-top: 12px; + + // Cheaty way to get an opacified variable colour background + &::before { + content: ''; + border-radius: 20px; + background-color: $tertiary-fg-color; + opacity: 0.15; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + // Pass through the pointer otherwise we have effectively put a whole div + // on top of the component, which makes it hard to interact with buttons. + pointer-events: none; + } + } + + .mx_AccessibleButton { + background-image: url('$(res)/img/subtract.svg'); + width: 16px; + height: 16px; + margin-left: 8px; + display: inline-block; + vertical-align: middle; + cursor: pointer; + } + } +} 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/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index c215d69ec2..403f671673 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -60,12 +60,6 @@ limitations under the License. } .mx_MFileBody_info { - background-color: $message-body-panel-bg-color; - border-radius: 12px; - width: 243px; // same width as a playable voice message, accounting for padding - padding: 6px 12px; - color: $message-body-panel-fg-color; - .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; border-radius: 20px; @@ -83,12 +77,12 @@ limitations under the License. mask-size: cover; mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); background-color: $message-body-panel-icon-fg-color; - width: 13px; + width: 15px; height: 15px; position: absolute; top: 8px; - left: 9px; + left: 8px; } } diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 878a4154cd..a748435cd8 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,20 +16,15 @@ limitations under the License. $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; - > canvas { + display: flex; + justify-content: center; + align-items: center; + + > div > canvas { border-radius: $timelineImageBorderRadius; } } @@ -43,17 +38,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/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss new file mode 100644 index 0000000000..70c53f8c9c --- /dev/null +++ b/res/css/views/messages/_MImageReplyBody.scss @@ -0,0 +1,37 @@ +/* +Copyright 2020 Tulir Asokan + +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_MImageReplyBody { + display: flex; + + .mx_MImageBody_thumbnail_container { + flex: 1; + margin-right: 4px; + } + + .mx_MImageReplyBody_info { + flex: 1; + + .mx_MImageReplyBody_sender { + grid-area: sender; + } + + .mx_MImageReplyBody_filename { + grid-area: filename; + } + } +} + diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss index 12e441750c..7f4bfd3fdc 100644 --- a/res/css/views/messages/_MediaBody.scss +++ b/res/css/views/messages/_MediaBody.scss @@ -20,9 +20,11 @@ limitations under the License. .mx_MediaBody { background-color: $message-body-panel-bg-color; border-radius: 12px; + max-width: 243px; // use max-width instead of width so it fits within right panels color: $message-body-panel-fg-color; font-size: $font-14px; line-height: $font-24px; -} + padding: 6px 12px; +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index e2fafe6c62..69f3c672b7 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -107,3 +107,12 @@ limitations under the License. .mx_MessageActionBar_cancelButton::after { mask-image: url('$(res)/img/element-icons/trashcan.svg'); } + +.mx_MessageActionBar_downloadButton::after { + mask-size: 14px; + mask-image: url('$(res)/img/download.svg'); +} + +.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after { + background-color: transparent; // hide the download icon mask +} diff --git a/res/css/views/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..8629682693 --- /dev/null +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -0,0 +1,336 @@ +/* +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, + &.mx_EventTile_selected { + &::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(90%, 50%, 0); + } + + --backgroundColor: $eventbubble-others-bg; + } + &[data-self=true] { + .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + float: right; + > a { + left: auto; + right: -68px; + } + } + .mx_SenderProfile { + display: none; + } + + .mx_ReplyTile .mx_SenderProfile { + display: block; + } + + .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; + padding: 10px 20px; + top: 0; + left: -68px; + } + } + + &.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; + img { + box-shadow: 0 0 0 3px $eventbubble-avatar-outline; + border-radius: 50%; + } + } + + .mx_BaseAvatar, + .mx_EventTile_avatar { + line-height: 1; + } + + &[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; + padding: 5px 0; + + .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(90%, 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: 2px 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 27a83e58f8..bda4a15f45 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,308 @@ 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, + & ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } + + & ~ .mx_EventListSummary .mx_EventTile_line { + padding-left: calc($left-gutter); + } + + &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + &.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 +336,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 +375,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 +454,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 { @@ -477,8 +469,11 @@ $hover-select-border: 4px; pre, code { font-family: $monospace-font-family !important; - // deliberate constants as we're behind an invert filter - color: #333; + background-color: $header-panel-bg-color; + } + + pre code > * { + display: inline-block; } pre { @@ -488,11 +483,6 @@ $hover-select-border: 4px; overflow-x: overlay; overflow-y: visible; } - - code { - // deliberate constants as we're behind an invert filter - background-color: #f8f8f8; - } } .mx_EventTile_lineNumbers { @@ -601,6 +591,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; @@ -621,6 +640,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/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index ddee81a914..ebb7f99e45 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -26,6 +26,7 @@ $left-gutter: 64px; > .mx_EventTile_avatar { position: absolute; + z-index: 9; } .mx_MessageTimestamp { diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 5e61c3b8a3..97190807ca 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -198,8 +198,9 @@ $irc-line-height: $font-18px; .mx_ReplyThread { margin: 0; .mx_SenderProfile { + order: unset; + max-width: unset; width: unset; - max-width: var(--name-width); background: transparent; } diff --git a/res/css/views/rooms/_LinkPreviewGroup.scss b/res/css/views/rooms/_LinkPreviewGroup.scss new file mode 100644 index 0000000000..ed341904fd --- /dev/null +++ b/res/css/views/rooms/_LinkPreviewGroup.scss @@ -0,0 +1,38 @@ +/* +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_LinkPreviewGroup { + .mx_LinkPreviewGroup_hide { + cursor: pointer; + width: 18px; + height: 18px; + + img { + flex: 0 0 40px; + visibility: hidden; + } + } + + &:hover .mx_LinkPreviewGroup_hide img, + .mx_LinkPreviewGroup_hide.focus-visible:focus img { + visibility: visible; + } + + > .mx_AccessibleButton { + color: $accent-color; + text-align: center; + } +} diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index 022cf3ed28..24900ee14b 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -19,7 +19,8 @@ limitations under the License. margin-right: 15px; margin-bottom: 15px; display: flex; - border-left: 4px solid $preview-widget-bar-color; + border-left: 2px solid $preview-widget-bar-color; + border-radius: 2px; color: $preview-widget-fg-color; } @@ -33,38 +34,29 @@ limitations under the License. .mx_LinkPreviewWidget_caption { margin-left: 15px; flex: 1 1 auto; + overflow: hidden; // cause it to wrap rather than clip } .mx_LinkPreviewWidget_title { - display: inline; font-weight: bold; white-space: normal; -} + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; -.mx_LinkPreviewWidget_siteName { - display: inline; + .mx_LinkPreviewWidget_siteName { + font-weight: normal; + } } .mx_LinkPreviewWidget_description { margin-top: 8px; white-space: normal; word-wrap: break-word; -} - -.mx_LinkPreviewWidget_cancel { - cursor: pointer; - width: 18px; - height: 18px; - - img { - flex: 0 0 40px; - visibility: hidden; - } -} - -.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img, -.mx_LinkPreviewWidget_cancel.focus-visible:focus img { - visibility: visible; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; } .mx_MatrixChat_useCompactLayout { diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 10f8e21e43..60feb39d11 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -22,28 +22,34 @@ limitations under the License. max-height: 50vh; overflow: auto; box-shadow: 0px -16px 32px $composer-shadow-color; + + .mx_ReplyPreview_section { + border-bottom: 1px solid $primary-hairline-color; + + .mx_ReplyPreview_header { + margin: 8px; + color: $primary-fg-color; + font-weight: 400; + opacity: 0.4; + } + + .mx_ReplyPreview_tile { + margin: 0 8px; + } + + .mx_ReplyPreview_title { + float: left; + } + + .mx_ReplyPreview_cancel { + float: right; + cursor: pointer; + display: flex; + } + + .mx_ReplyPreview_clear { + clear: both; + } + } } -.mx_ReplyPreview_section { - border-bottom: 1px solid $primary-hairline-color; -} - -.mx_ReplyPreview_header { - margin: 12px; - color: $primary-fg-color; - font-weight: 400; - opacity: 0.4; -} - -.mx_ReplyPreview_title { - float: left; -} - -.mx_ReplyPreview_cancel { - float: right; - cursor: pointer; -} - -.mx_ReplyPreview_clear { - clear: both; -} diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss new file mode 100644 index 0000000000..f3e204e415 --- /dev/null +++ b/res/css/views/rooms/_ReplyTile.scss @@ -0,0 +1,119 @@ +/* +Copyright 2020 Tulir Asokan + +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_ReplyTile { + position: relative; + padding: 2px 0; + font-size: $font-14px; + line-height: $font-16px; + + &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { + mask-image: url("$(res)/img/element-icons/speaker.svg"); + } + + &.mx_ReplyTile_video .mx_MFileBody_info_icon::before { + mask-image: url("$(res)/img/element-icons/call/video-call.svg"); + } + + .mx_MFileBody { + .mx_MFileBody_info { + margin: 5px 0; + } + + .mx_MFileBody_download { + display: none; + } + } + + > a { + display: flex; + flex-direction: column; + text-decoration: none; + color: $primary-fg-color; + } + + .mx_RedactedBody { + padding: 4px 0 2px 20px; + + &::before { + height: 13px; + width: 13px; + top: 5px; + } + } + + // We do reply size limiting with CSS to avoid duplicating the TextualBody component. + .mx_EventTile_content { + $reply-lines: 2; + $line-height: $font-22px; + + pointer-events: none; + + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: $reply-lines; + line-height: $line-height; + + .mx_EventTile_body.mx_EventTile_bigEmoji { + line-height: $line-height !important; + font-size: $font-14px !important; // Override the big emoji override + } + + // Hide line numbers + .mx_EventTile_lineNumbers { + display: none; + } + + // Hack to cut content in
 tags too
+        .mx_EventTile_pre_container > pre {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            display: -webkit-box;
+            -webkit-box-orient: vertical;
+            -webkit-line-clamp: $reply-lines;
+            padding: 4px;
+        }
+
+        .markdown-body blockquote,
+        .markdown-body dl,
+        .markdown-body ol,
+        .markdown-body p,
+        .markdown-body pre,
+        .markdown-body table,
+        .markdown-body ul {
+            margin-bottom: 4px;
+        }
+    }
+
+    &.mx_ReplyTile_info {
+        padding-top: 0;
+    }
+
+    .mx_SenderProfile {
+        font-size: $font-14px;
+        line-height: $font-17px;
+
+        display: inline-block; // anti-zalgo, with overflow hidden
+        padding: 0;
+        margin: 0;
+
+        // truncate long display names
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+    }
+}
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index 03146e0325..b8f4aeb6e7 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -193,6 +193,10 @@ limitations under the License.
         mask-image: url('$(res)/img/element-icons/settings.svg');
     }
 
+    .mx_RoomTile_iconCopyLink::before {
+        mask-image: url('$(res)/img/element-icons/link.svg');
+    }
+
     .mx_RoomTile_iconInvite::before {
         mask-image: url('$(res)/img/element-icons/room/invite.svg');
     }
diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss
index 9f6a8d52ce..4b7eb54188 100644
--- a/res/css/views/rooms/_SendMessageComposer.scss
+++ b/res/css/views/rooms/_SendMessageComposer.scss
@@ -29,8 +29,10 @@ limitations under the License.
         display: flex;
         flex-direction: column;
         // min-height at this level so the mx_BasicMessageComposer_input
-        // still stays vertically centered when less than 50px
-        min-height: 50px;
+        // still stays vertically centered when less than 55px.
+        // We also set this to ensure the voice message recording widget
+        // doesn't cause a jump.
+        min-height: 55px;
 
         .mx_BasicMessageComposer_input {
             padding: 3px 0;
diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss
index 77a7bc5b68..f93e0a53a8 100644
--- a/res/css/views/settings/_Notifications.scss
+++ b/res/css/views/settings/_Notifications.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015 - 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.
@@ -14,82 +14,79 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_UserNotifSettings_tableRow {
-    display: table-row;
-}
+.mx_UserNotifSettings {
+    color: $primary-fg-color; // override from default settings page styles
 
-.mx_UserNotifSettings_inputCell {
-    display: table-cell;
-    padding-bottom: 8px;
-    padding-right: 8px;
-    width: 16px;
-}
+    .mx_UserNotifSettings_pushRulesTable {
+        width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches
+        table-layout: fixed;
+        border-collapse: collapse;
+        border-spacing: 0;
+        margin-top: 40px;
 
-.mx_UserNotifSettings_labelCell {
-    padding-bottom: 8px;
-    width: 400px;
-    display: table-cell;
-}
+        tr > th {
+            font-weight: $font-semi-bold;
+        }
 
-.mx_UserNotifSettings_pushRulesTableWrapper {
-    padding-bottom: 8px;
-}
+        tr > th:first-child {
+            text-align: left;
+            font-size: $font-18px;
+        }
 
-.mx_UserNotifSettings_pushRulesTable {
-    width: 100%;
-    table-layout: fixed;
-}
+        tr > th:nth-child(n + 2) {
+            color: $secondary-fg-color;
+            font-size: $font-12px;
+            vertical-align: middle;
+            width: 66px;
+        }
 
-.mx_UserNotifSettings_pushRulesTable thead {
-    font-weight: bold;
-}
+        tr > td:nth-child(n + 2) {
+            text-align: center;
+        }
 
-.mx_UserNotifSettings_pushRulesTable tbody th {
-    font-weight: 400;
-}
+        tr > td {
+            padding-top: 8px;
+        }
 
-.mx_UserNotifSettings_pushRulesTable tbody th:first-child {
-    text-align: left;
-}
+        // Override StyledRadioButton default styles
+        .mx_RadioButton {
+            justify-content: center;
 
-.mx_UserNotifSettings_keywords {
-    cursor: pointer;
-    color: $accent-color;
-}
+            .mx_RadioButton_content {
+                display: none;
+            }
 
-.mx_UserNotifSettings_devicesTable td {
-    padding-left: 20px;
-    padding-right: 20px;
-}
+            .mx_RadioButton_spacer {
+                display: none;
+            }
+        }
+    }
 
-.mx_UserNotifSettings_notifTable {
-    display: table;
-    position: relative;
-}
+    .mx_UserNotifSettings_floatingSection {
+        margin-top: 40px;
 
-.mx_UserNotifSettings_notifTable .mx_Spinner {
-    position: absolute;
-}
+        & > div:first-child { // section header
+            font-size: $font-18px;
+            font-weight: $font-semi-bold;
+        }
 
-.mx_NotificationSound_soundUpload {
-    display: none;
-}
+        > table {
+            border-collapse: collapse;
+            border-spacing: 0;
+            margin-top: 8px;
 
-.mx_NotificationSound_browse {
-    color: $accent-color;
-    border: 1px solid $accent-color;
-    background-color: transparent;
-}
+            tr > td:first-child {
+                // Just for a bit of spacing
+                padding-right: 8px;
+            }
+        }
+    }
 
-.mx_NotificationSound_save {
-    margin-left: 5px;
-    color: white;
-    background-color: $accent-color;
-}
+    .mx_UserNotifSettings_clearNotifsButton {
+        margin-top: 8px;
+    }
 
-.mx_NotificationSound_resetSound {
-    margin-top: 5px;
-    color: white;
-    border: $warning-color;
-    background-color: $warning-color;
+    .mx_TagComposer {
+        margin-top: 35px; // lots of distance from the last line of the table
+    }
 }
diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss
index 892f5fe744..0d679af4e5 100644
--- a/res/css/views/settings/tabs/_SettingsTab.scss
+++ b/res/css/views/settings/tabs/_SettingsTab.scss
@@ -47,14 +47,14 @@ limitations under the License.
     color: $settings-subsection-fg-color;
     font-size: $font-14px;
     display: block;
-    margin: 10px 100px 10px 0; // Align with the rest of the view
+    margin: 10px 80px 10px 0; // Align with the rest of the view
 }
 
 .mx_SettingsTab_section {
     margin-bottom: 24px;
 
     .mx_SettingsFlag {
-        margin-right: 100px;
+        margin-right: 80px;
         margin-bottom: 10px;
     }
 
diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
index 23dcc532b2..2aab201352 100644
--- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
+++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
@@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+.mx_SecurityRoomSettingsTab {
+    .mx_SettingsTab_showAdvanced {
+        padding: 0;
+        margin-bottom: 16px;
+    }
+
+    .mx_SecurityRoomSettingsTab_spacesWithAccess {
+        > h4 {
+            color: $secondary-fg-color;
+            font-weight: $font-semi-bold;
+            font-size: $font-12px;
+            line-height: $font-15px;
+            text-transform: uppercase;
+        }
+
+        > span {
+            font-weight: 500;
+            font-size: $font-14px;
+            line-height: 32px; // matches height of avatar for v-align
+            color: $secondary-fg-color;
+            display: inline-block;
+
+            img.mx_RoomAvatar_isSpaceRoom,
+            .mx_RoomAvatar_isSpaceRoom img {
+                border-radius: 8px;
+            }
+
+            .mx_BaseAvatar {
+                margin-right: 8px;
+            }
+
+            & + span {
+                margin-left: 16px;
+            }
+        }
+    }
+}
+
 .mx_SecurityRoomSettingsTab_warning {
     display: block;
 
@@ -26,5 +64,51 @@ limitations under the License.
 }
 
 .mx_SecurityRoomSettingsTab_encryptionSection {
-    margin-bottom: 25px;
+    padding-bottom: 24px;
+    border-bottom: 1px solid $menu-border-color;
+    margin-bottom: 32px;
+}
+
+.mx_SecurityRoomSettingsTab_upgradeRequired {
+    margin-left: 16px;
+    padding: 4px 16px;
+    border: 1px solid $accent-color;
+    border-radius: 8px;
+    color: $accent-color;
+    font-size: $font-12px;
+    line-height: $font-15px;
+}
+
+.mx_SecurityRoomSettingsTab_joinRule {
+    .mx_RadioButton {
+        padding-top: 16px;
+        margin-bottom: 8px;
+
+        .mx_RadioButton_content {
+            margin-left: 14px;
+            font-weight: $font-semi-bold;
+            font-size: $font-15px;
+            line-height: $font-24px;
+            color: $primary-fg-color;
+            display: block;
+        }
+    }
+
+    > span {
+        display: inline-block;
+        margin-left: 34px;
+        margin-bottom: 16px;
+        font-size: $font-15px;
+        line-height: $font-24px;
+        color: $secondary-fg-color;
+
+        & + .mx_RadioButton {
+            border-top: 1px solid $menu-border-color;
+        }
+    }
+
+    .mx_AccessibleButton_kind_link {
+        padding: 0;
+        font-size: inherit;
+    }
 }
diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
index 94983a60bf..ca5a6f0a66 100644
--- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
@@ -15,8 +15,7 @@ limitations under the License.
 */
 
 .mx_AppearanceUserSettingsTab_fontSlider,
-.mx_AppearanceUserSettingsTab_fontSlider_preview,
-.mx_AppearanceUserSettingsTab_Layout {
+.mx_AppearanceUserSettingsTab_fontSlider_preview {
     @mixin mx_Settings_fullWidthField;
 }
 
@@ -45,6 +44,11 @@ limitations under the License.
     border-radius: 10px;
     padding: 0 16px 9px 16px;
     pointer-events: none;
+    display: flow-root;
+
+    .mx_EventTile[data-layout=bubble] {
+        margin-top: 30px;
+    }
 
     .mx_EventTile_msgOption {
         display: none;
@@ -154,13 +158,10 @@ limitations under the License.
 .mx_AppearanceUserSettingsTab_Layout_RadioButtons {
     display: flex;
     flex-direction: row;
+    gap: 24px;
 
     color: $primary-fg-color;
 
-    .mx_AppearanceUserSettingsTab_spacer {
-        width: 24px;
-    }
-
     > .mx_AppearanceUserSettingsTab_Layout_RadioButton {
         flex-grow: 0;
         flex-shrink: 1;
@@ -210,6 +211,21 @@ limitations under the License.
     .mx_RadioButton_checked {
         background-color: rgba($accent-color, 0.08);
     }
+
+    .mx_EventTile {
+        margin: 0;
+        &[data-layout=bubble] {
+            margin-right: 40px;
+        }
+        &[data-layout=irc] {
+            > a {
+                display: none;
+            }
+        }
+        .mx_EventTile_line {
+            max-width: 90%;
+        }
+    }
 }
 
 .mx_AppearanceUserSettingsTab_Advanced {
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 168a8bb74b..0c09070334 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -30,8 +30,8 @@ limitations under the License.
         pointer-events: initial; // restore pointer events so the user can leave/interact
         cursor: pointer;
 
-        .mx_CallView_video {
-            width: 350px;
+        .mx_VideoFeed_remote.mx_VideoFeed_voice {
+            min-height: 150px;
         }
 
         .mx_VideoFeed_local {
diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss
new file mode 100644
index 0000000000..92348fb465
--- /dev/null
+++ b/res/css/views/voip/_CallPreview.scss
@@ -0,0 +1,21 @@
+/*
+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_CallPreview {
+    position: fixed;
+    left: 0;
+    top: 0;
+}
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 0be75be28c..205d431752 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -39,7 +39,6 @@ limitations under the License.
 .mx_CallView_pip {
     width: 320px;
     padding-bottom: 8px;
-    margin-top: 10px;
     background-color: $voipcall-plinth-color;
     box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
     border-radius: 8px;
diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss
index 483b131bfe..eefd2e9ba5 100644
--- a/res/css/views/voip/_DialPad.scss
+++ b/res/css/views/voip/_DialPad.scss
@@ -16,11 +16,21 @@ limitations under the License.
 
 .mx_DialPad {
     display: grid;
+    row-gap: 16px;
+    column-gap: 0px;
+    margin-top: 24px;
+    margin-left: auto;
+    margin-right: auto;
+
+    /* squeeze the dial pad buttons together horizontally */
     grid-template-columns: repeat(3, 1fr);
-    gap: 16px;
 }
 
 .mx_DialPad_button {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
     width: 40px;
     height: 40px;
     background-color: $dialpad-button-bg-color;
@@ -29,10 +39,19 @@ limitations under the License.
     font-weight: 600;
     text-align: center;
     vertical-align: middle;
-    line-height: 40px;
+    margin-left: auto;
+    margin-right: auto;
 }
 
-.mx_DialPad_deleteButton, .mx_DialPad_dialButton {
+.mx_DialPad_button .mx_DialPad_buttonSubText {
+    font-size: 8px;
+}
+
+.mx_DialPad_dialButton {
+    /* Always show the dial button in the center grid column */
+    grid-column: 2;
+    background-color: $accent-color;
+
     &::before {
         content: '';
         display: inline-block;
@@ -42,21 +61,7 @@ limitations under the License.
         mask-repeat: no-repeat;
         mask-size: 20px;
         mask-position: center;
-        background-color: $primary-bg-color;
-    }
-}
-
-.mx_DialPad_deleteButton {
-    background-color: $notice-primary-color;
-    &::before {
-        mask-image: url('$(res)/img/element-icons/call/delete.svg');
-        mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
-    }
-}
-
-.mx_DialPad_dialButton {
-    background-color: $accent-color;
-    &::before {
+        background-color: #FFF; // on all themes
         mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
     }
 }
diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss
index 31327113cf..0019994e72 100644
--- a/res/css/views/voip/_DialPadContextMenu.scss
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -14,10 +14,40 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+.mx_DialPadContextMenu_dialPad .mx_DialPad {
+    row-gap: 16px;
+    column-gap: 32px;
+}
+
+.mx_DialPadContextMenuWrapper {
+    padding: 15px;
+}
+
 .mx_DialPadContextMenu_header {
-    margin-top: 12px;
-    margin-left: 12px;
-    margin-right: 12px;
+    border: none;
+    margin-top: 32px;
+    margin-left: 20px;
+    margin-right: 20px;
+
+    /* a separator between the input line and the dial buttons */
+    border-bottom: 1px solid $quaternary-fg-color;
+    transition: border-bottom 0.25s;
+}
+
+.mx_DialPadContextMenu_cancel {
+    float: right;
+    mask: url('$(res)/img/feather-customised/cancel.svg');
+    mask-repeat: no-repeat;
+    mask-position: center;
+    mask-size: cover;
+    width: 14px;
+    height: 14px;
+    background-color: $dialog-close-fg-color;
+    cursor: pointer;
+}
+
+.mx_DialPadContextMenu_header:focus-within {
+    border-bottom: 1px solid $accent-color;
 }
 
 .mx_DialPadContextMenu_title {
@@ -30,7 +60,6 @@ limitations under the License.
     height: 1.5em;
     font-size: 18px;
     font-weight: 600;
-    max-width: 150px;
     border: none;
     margin: 0px;
 }
@@ -38,7 +67,7 @@ limitations under the License.
     font-size: 18px;
     font-weight: 600;
     overflow: hidden;
-    max-width: 150px;
+    max-width: 185px;
     text-align: left;
     direction: rtl;
     padding: 8px 0px;
@@ -48,13 +77,3 @@ limitations under the License.
 .mx_DialPadContextMenu_dialPad {
     margin: 16px;
 }
-
-.mx_DialPadContextMenu_horizSep {
-    position: relative;
-    &::before {
-        content: '';
-        position: absolute;
-        width: 100%;
-        border-bottom: 1px solid $input-darker-bg-color;
-    }
-}
diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss
index f9d7673a38..b8042f77ae 100644
--- a/res/css/views/voip/_DialPadModal.scss
+++ b/res/css/views/voip/_DialPadModal.scss
@@ -19,14 +19,23 @@ limitations under the License.
 }
 
 .mx_DialPadModal {
-    width: 192px;
-    height: 368px;
+    width: 292px;
+    height: 370px;
+    padding: 16px 0px 0px 0px;
 }
 
 .mx_DialPadModal_header {
-    margin-top: 12px;
-    margin-left: 12px;
-    margin-right: 12px;
+    margin-top: 32px;
+    margin-left: 40px;
+    margin-right: 40px;
+
+    /* a separator between the input line and the dial buttons */
+    border-bottom: 1px solid $quaternary-fg-color;
+    transition: border-bottom 0.25s;
+}
+
+.mx_DialPadModal_header:focus-within {
+    border-bottom: 1px solid $accent-color;
 }
 
 .mx_DialPadModal_title {
@@ -45,11 +54,18 @@ limitations under the License.
     height: 14px;
     background-color: $dialog-close-fg-color;
     cursor: pointer;
+    margin-right: 16px;
 }
 
 .mx_DialPadModal_field {
     border: none;
     margin: 0px;
+    height: 30px;
+}
+
+.mx_DialPadModal_field .mx_Field_postfix {
+    /* Remove border separator between postfix and field content */
+    border-left: none;
 }
 
 .mx_DialPadModal_field input {
@@ -62,13 +78,3 @@ limitations under the License.
     margin-right: 16px;
     margin-top: 16px;
 }
-
-.mx_DialPadModal_horizSep {
-    position: relative;
-    &::before {
-        content: '';
-        position: absolute;
-        width: 100%;
-        border-bottom: 1px solid $input-darker-bg-color;
-    }
-}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 7d85ac264e..4a3fbdf597 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -15,8 +15,6 @@ limitations under the License.
 */
 
 .mx_VideoFeed_voice {
-    // We don't want to collide with the call controls that have 52px of height
-    padding-bottom: 52px;
     background-color: $inverted-bg-color;
 }
 
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/speaker.svg b/res/img/element-icons/speaker.svg
new file mode 100644
index 0000000000..fd811d2cda
--- /dev/null
+++ b/res/img/element-icons/speaker.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg
new file mode 100644
index 0000000000..eef5193140
--- /dev/null
+++ b/res/img/element-icons/warning.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/feather-customised/globe.svg b/res/img/feather-customised/globe.svg
deleted file mode 100644
index 8af7dc41dc..0000000000
--- a/res/img/feather-customised/globe.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-    
-        
-        
-        
-    
-
diff --git a/res/img/subtract.svg b/res/img/subtract.svg
new file mode 100644
index 0000000000..55e25831ef
--- /dev/null
+++ b/res/img/subtract.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/voip/tab-dialpad.svg b/res/img/voip/tab-dialpad.svg
new file mode 100644
index 0000000000..b7add0addb
--- /dev/null
+++ b/res/img/voip/tab-dialpad.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/voip/tab-userdirectory.svg b/res/img/voip/tab-userdirectory.svg
new file mode 100644
index 0000000000..792ded7be4
--- /dev/null
+++ b/res/img/voip/tab-userdirectory.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 81fd3c892a..655492661c 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -118,9 +118,7 @@ $voipcall-plinth-color: #394049;
 // ********************
 
 $theme-button-bg-color: #e3e8f0;
-$dialpad-button-bg-color: #6F7882;
-;
-
+$dialpad-button-bg-color: #394049;
 
 $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
 $roomlist-filter-active-bg-color: $bg-color;
@@ -211,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
 
 $message-body-panel-fg-color: $secondary-fg-color;
 $message-body-panel-bg-color: #394049; // "Dark Tile"
-$message-body-panel-icon-fg-color: #21262C; // "Separator"
-$message-body-panel-icon-bg-color: $tertiary-fg-color;
+$message-body-panel-icon-fg-color: $secondary-fg-color;
+$message-body-panel-icon-bg-color: #21262C; // "System Dark"
 
 $voice-record-stop-border-color: $quaternary-fg-color;
 $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
@@ -229,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 {
@@ -274,24 +279,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
 }
 
 // markdown overrides:
-.mx_EventTile_content .markdown-body pre:hover {
-    border-color: #808080 !important; // inverted due to rules below
-    scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below
-    // the code above works only in Firefox, this is for other browsers
-    // see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
-    &::-webkit-scrollbar-thumb {
-        background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below
-    }
-}
 .mx_EventTile_content .markdown-body {
-    pre, code {
-        filter: invert(1);
-    }
-
-    pre code {
-        filter: none;
-    }
-
     table {
         tr {
             background-color: #000000;
@@ -301,18 +289,17 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
             background-color: #080808;
         }
     }
-
-    blockquote {
-        color: #919191;
-    }
 }
 
-// diff highlight colors
-// intentionally swapped to avoid inversion
+// highlight.js overrides
+.hljs-tag {
+    color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
+}
+
 .hljs-addition {
-    background: #fdd;
+    background: #1a4b59;
 }
 
 .hljs-deletion {
-    background: #dfd;
+    background: #53232a;
 }
diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss
index f9695018e4..600cfd528a 100644
--- a/res/themes/dark/css/dark.scss
+++ b/res/themes/dark/css/dark.scss
@@ -9,3 +9,4 @@
 @import "_dark.scss";
 @import "../../light/css/_mods.scss";
 @import "../../../../res/css/_components.scss";
+@import url("highlight.js/styles/atom-one-dark.css");
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index df01efbe1e..0c0197cfb0 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -118,7 +118,7 @@ $voipcall-plinth-color: #394049;
 
 $theme-button-bg-color: #e3e8f0;
 $dialpad-button-bg-color: #6F7882;
-;
+
 
 $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
 $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
@@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
 
 $message-body-panel-fg-color: $secondary-fg-color;
 $message-body-panel-bg-color: #394049;
-$message-body-panel-icon-fg-color: $primary-bg-color;
-$message-body-panel-icon-bg-color: $secondary-fg-color;
+$message-body-panel-icon-fg-color: $secondary-fg-color;
+$message-body-panel-icon-bg-color: #21262C;
 
 // See non-legacy dark for variable information
 $voice-record-stop-border-color: #6F7882;
@@ -249,7 +249,7 @@ $composer-shadow-color: tranparent;
 @define-mixin mx_DialogButton_secondary {
     // flip colours for the secondary ones
     font-weight: 600;
-    border: 1px solid $accent-color ! important;
+    border: 1px solid $accent-color !important;
     color: $accent-color;
     background-color: $button-secondary-bg-color;
 }
@@ -267,18 +267,7 @@ $composer-shadow-color: tranparent;
 }
 
 // markdown overrides:
-.mx_EventTile_content .markdown-body pre:hover {
-    border-color: #808080 !important; // inverted due to rules below
-}
 .mx_EventTile_content .markdown-body {
-    pre, code {
-        filter: invert(1);
-    }
-
-    pre code {
-        filter: none;
-    }
-
     table {
         tr {
             background-color: #000000;
@@ -290,12 +279,7 @@ $composer-shadow-color: tranparent;
     }
 }
 
-// diff highlight colors
-// intentionally swapped to avoid inversion
-.hljs-addition {
-    background: #fdd;
-}
-
-.hljs-deletion {
-    background: #dfd;
+// highlight.js overrides:
+.hljs-tag {
+    color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
 }
diff --git a/res/themes/legacy-dark/css/legacy-dark.scss b/res/themes/legacy-dark/css/legacy-dark.scss
index 2a4d432d26..840794f7c0 100644
--- a/res/themes/legacy-dark/css/legacy-dark.scss
+++ b/res/themes/legacy-dark/css/legacy-dark.scss
@@ -4,3 +4,4 @@
 @import "../../legacy-light/css/_legacy-light.scss";
 @import "_legacy-dark.scss";
 @import "../../../../res/css/_components.scss";
+@import url("highlight.js/styles/atom-one-dark.css");
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index c7debcdabe..b7d45452ff 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
 $message-body-panel-fg-color: $secondary-fg-color;
 $message-body-panel-bg-color: #E3E8F0;
 $message-body-panel-icon-fg-color: $secondary-fg-color;
-$message-body-panel-icon-bg-color: $primary-bg-color;
+$message-body-panel-icon-bg-color: #F4F6FA;
 
 // See non-legacy _light for variable information
 $voice-record-stop-symbol-color: #ff4b55;
@@ -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/legacy-light/css/legacy-light.scss b/res/themes/legacy-light/css/legacy-light.scss
index e39a1765f3..347d240fc6 100644
--- a/res/themes/legacy-light/css/legacy-light.scss
+++ b/res/themes/legacy-light/css/legacy-light.scss
@@ -3,3 +3,4 @@
 @import "_fonts.scss";
 @import "_legacy-light.scss";
 @import "../../../../res/css/_components.scss";
+@import url("highlight.js/styles/atom-one-light.css");
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 7e958c2af6..32722515d8 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
 $message-body-panel-fg-color: $secondary-fg-color;
 $message-body-panel-bg-color: #E3E8F0; // "Separator"
 $message-body-panel-icon-fg-color: $secondary-fg-color;
-$message-body-panel-icon-bg-color: $primary-bg-color;
+$message-body-panel-icon-bg-color: #F4F6FA;
 
 // These two don't change between themes. They are the $warning-color, but we don't
 // want custom themes to affect them by accident.
@@ -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/res/themes/light/css/light.scss b/res/themes/light/css/light.scss
index f31ce5c139..4e912bc756 100644
--- a/res/themes/light/css/light.scss
+++ b/res/themes/light/css/light.scss
@@ -4,3 +4,4 @@
 @import "_light.scss";
 @import "_mods.scss";
 @import "../../../../res/css/_components.scss";
+@import url("highlight.js/styles/atom-one-light.css");
diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh
index bbda74ef9d..fcbf6b1198 100755
--- a/scripts/ci/install-deps.sh
+++ b/scripts/ci/install-deps.sh
@@ -6,8 +6,8 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
 
 pushd matrix-js-sdk
 yarn link
-yarn install $@
+yarn install --pure-lockfile $@
 popd
 
 yarn link matrix-js-sdk
-yarn install $@
+yarn install --pure-lockfile $@
diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh
index 039f90c7df..2e163456fe 100755
--- a/scripts/ci/layered.sh
+++ b/scripts/ci/layered.sh
@@ -13,13 +13,13 @@
 scripts/fetchdep.sh matrix-org matrix-js-sdk
 pushd matrix-js-sdk
 yarn link
-yarn install
+yarn install --pure-lockfile
 popd
 
 # Now set up the react-sdk
 yarn link matrix-js-sdk
 yarn link
-yarn install
+yarn install --pure-lockfile
 yarn reskindex
 
 # Finally, set up element-web
@@ -27,6 +27,6 @@ scripts/fetchdep.sh vector-im element-web
 pushd element-web
 yarn link matrix-js-sdk
 yarn link matrix-react-sdk
-yarn install
+yarn install --pure-lockfile
 yarn build:res
 popd
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 d257ee4c5c..9d6bc2c6fb 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -15,7 +15,9 @@ limitations under the License.
 */
 
 import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
-import * as ModernizrStatic from "modernizr";
+// Load types for the WG CSS Font Loading APIs https://github.com/Microsoft/TypeScript/issues/13569
+import "@types/css-font-loading-module";
+import "@types/modernizr";
 
 import ContentMessages from "../ContentMessages";
 import { IMatrixClientPeg } from "../MatrixClientPeg";
@@ -48,9 +50,10 @@ 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 {
-        Modernizr: ModernizrStatic;
         matrixChat: ReturnType;
         mxMatrixClientPeg: IMatrixClientPeg;
         Olm: {
@@ -89,6 +92,7 @@ declare global {
         mxUIStore: UIStore;
         mxSetupEncryptionStore?: SetupEncryptionStore;
         mxRoomScrollStateStore?: RoomScrollStateStore;
+        mxOnRecaptchaLoaded?: () => void;
     }
 
     interface Document {
@@ -113,7 +117,7 @@ declare global {
     }
 
     interface StorageEstimate {
-        usageDetails?: {[key: string]: number};
+        usageDetails?: { [key: string]: number };
     }
 
     interface HTMLAudioElement {
@@ -184,4 +188,21 @@ declare global {
             parameterDescriptors?: AudioParamDescriptor[];
         }
     );
+
+    // eslint-disable-next-line no-var
+    var grecaptcha:
+        | undefined
+        | {
+              reset: (id: string) => void;
+              render: (
+                  divId: string,
+                  options: {
+                      sitekey: string;
+                      callback: (response: string) => void;
+                  },
+              ) => string;
+              isReady: () => boolean;
+          };
 }
+
+/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts
new file mode 100644
index 0000000000..96f671c52f
--- /dev/null
+++ b/src/@types/svg.d.ts
@@ -0,0 +1,20 @@
+/*
+Copyright 2021 Šimon Brandner 
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+declare module "*.svg" {
+    const path: string;
+    export default path;
+}
diff --git a/res/css/views/messages/_MVoiceMessageBody.scss b/src/@types/worker-loader.d.ts
similarity index 81%
rename from res/css/views/messages/_MVoiceMessageBody.scss
rename to src/@types/worker-loader.d.ts
index 3dfb98f778..a8f5d8e9a4 100644
--- a/res/css/views/messages/_MVoiceMessageBody.scss
+++ b/src/@types/worker-loader.d.ts
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_MVoiceMessageBody {
-    display: inline-block; // makes the playback controls magically line up
+declare module "*.worker.ts" {
+    class WebpackWorker extends Worker {
+        constructor();
+    }
+
+    export default WebpackWorker;
 }
diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts
index 1126dc9496..0be49a24ea 100644
--- a/src/ActiveRoomObserver.ts
+++ b/src/ActiveRoomObserver.ts
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import RoomViewStore from './stores/RoomViewStore';
+import { EventSubscription } from 'fbemitter';
 
 type Listener = (isActive: boolean) => void;
 
@@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void;
 export class ActiveRoomObserver {
     private listeners: {[key: string]: Listener[]} = {};
     private _activeRoomId = RoomViewStore.getRoomId();
-    private readonly roomStoreToken: string;
+    private readonly roomStoreToken: EventSubscription;
 
     constructor() {
         // TODO: We could self-destruct when the last listener goes away, or at least stop listening.
diff --git a/src/AddThreepid.js b/src/AddThreepid.js
index eb822c6d75..ab291128a7 100644
--- a/src/AddThreepid.js
+++ b/src/AddThreepid.js
@@ -248,7 +248,7 @@ export default class AddThreepid {
 
     /**
      * Takes a phone number verification code as entered by the user and validates
-     * it with the ID server, then if successful, adds the phone number.
+     * it with the identity server, then if successful, adds the phone number.
      * @param {string} msisdnToken phone number verification code as entered by the user
      * @return {Promise} Resolves if the phone number was added. Rejects with an object
      * with a "message" property which contains a human-readable message detailing why
diff --git a/src/Analytics.tsx b/src/Analytics.tsx
index 8c82639b5f..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,
@@ -390,21 +390,22 @@ export class Analytics {
             { expl: _td('Your device resolution'), value: resolution },
         ];
 
+        // FIXME: Using an import will result in test failures
         const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
         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/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 3bbef71093..ef8924add8 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -77,6 +77,7 @@ export default class AsyncWrapper extends React.Component { const Component = this.state.component; return ; } else if (this.state.error) { + // FIXME: Using an import will result in test failures const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return diff --git a/src/Avatar.ts b/src/Avatar.ts index 4c4bd1c265..c0ecb19eaf 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -18,10 +18,11 @@ 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"; -import SettingsStore from "./settings/SettingsStore"; +import SpaceStore from "./stores/SpaceStore"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( @@ -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) { @@ -153,7 +140,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi } // space rooms cannot be DMs so skip the rest - if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null; + if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null; let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts new file mode 100644 index 0000000000..2aee370fe9 --- /dev/null +++ b/src/BlurhashEncoder.ts @@ -0,0 +1,60 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { defer, IDeferred } from "matrix-js-sdk/src/utils"; + +// @ts-ignore - `.ts` is needed here to make TS happy +import BlurhashWorker from "./workers/blurhash.worker.ts"; + +interface IBlurhashWorkerResponse { + seq: number; + blurhash: string; +} + +export class BlurhashEncoder { + private static internalInstance = new BlurhashEncoder(); + + public static get instance(): BlurhashEncoder { + return BlurhashEncoder.internalInstance; + } + + private readonly worker: Worker; + private seq = 0; + private pendingDeferredMap = new Map>(); + + constructor() { + this.worker = new BlurhashWorker(); + this.worker.onmessage = this.onMessage; + } + + private onMessage = (ev: MessageEvent) => { + const { seq, blurhash } = ev.data; + const deferred = this.pendingDeferredMap.get(seq); + if (deferred) { + this.pendingDeferredMap.delete(seq); + deferred.resolve(blurhash); + } + }; + + public getBlurhash(imageData: ImageData): Promise { + const seq = this.seq++; + const deferred = defer(); + this.pendingDeferredMap.set(seq, deferred); + this.worker.postMessage({ seq, imageData }); + return deferred.promise; + } +} + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 6e1e6ce83a..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 { @@ -154,7 +155,7 @@ export default class CallHandler extends EventEmitter { private supportsPstnProtocol = null; private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native - private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser + private pstnSupportCheckTimer: number; // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. private invitedRoomsAreVirtual = new Map(); private invitedRoomCheckInProgress = false; @@ -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; } @@ -394,7 +431,7 @@ export default class CallHandler extends EventEmitter { } private setCallListeners(call: MatrixCall) { - let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + let mappedRoomId = this.roomIdForCall(call); call.on(CallEvent.Error, (err: CallError) => { if (!this.matchesCallForThisRoom(call)) return; @@ -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") }
; } @@ -871,6 +912,12 @@ export default class CallHandler extends EventEmitter { case Action.DialNumber: this.dialNumber(payload.number); break; + case Action.TransferCallToMatrixID: + this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst); + break; + case Action.TransferCallToPhoneNumber: + this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst); + break; } }; @@ -905,6 +952,48 @@ export default class CallHandler extends EventEmitter { }); } + private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) { + const results = await this.pstnLookup(destination); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to transfer call"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + + await this.startTransferToMatrixID(call, results[0].userid, consultFirst); + } + + private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) { + if (consultFirst) { + const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination); + + dis.dispatch({ + action: 'place_call', + type: call.type, + room_id: dmRoomId, + transferee: call, + }); + dis.dispatch({ + action: 'view_room', + room_id: dmRoomId, + should_peek: false, + joining: false, + }); + } else { + try { + await call.transfer(destination); + } catch (e) { + console.log("Failed to transfer call", e); + Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, { + title: _t('Transfer Failed'), + description: _t('Failed to transfer call'), + }); + } + } + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 1b8a3be37e..c5bcb226ff 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from "react"; -import { encode } from "blurhash"; import { MatrixClient } from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; @@ -28,7 +27,6 @@ import RoomViewStore from './stores/RoomViewStore'; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; - import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; import { @@ -39,7 +37,8 @@ import { UploadStartedPayload, } from "./dispatcher/payloads/UploadPayload"; import { IUpload } from "./models/IUpload"; -import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; +import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; +import { BlurhashEncoder } from "./BlurhashEncoder"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -49,8 +48,6 @@ const MAX_HEIGHT = 600; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 -const BLURHASH_X_COMPONENTS = 6; -const BLURHASH_Y_COMPONENTS = 6; export class UploadCanceledError extends Error {} @@ -87,10 +84,6 @@ interface IThumbnail { thumbnail: Blob; } -interface IAbortablePromise extends Promise { - abort(): void; -} - /** * Create a thumbnail for a image DOM element. * The image will be smaller than MAX_WIDTH and MAX_HEIGHT. @@ -109,54 +102,62 @@ interface IAbortablePromise extends Promise { * @return {Promise} A promise that resolves with an object with an info key * and a thumbnail key. */ -function createThumbnail( +async function createThumbnail( element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string, ): Promise { - return new Promise((resolve) => { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - const canvas = document.createElement("canvas"); + let canvas: HTMLCanvasElement | OffscreenCanvas; + if (window.OffscreenCanvas) { + canvas = new window.OffscreenCanvas(targetWidth, targetHeight); + } else { + canvas = document.createElement("canvas"); canvas.width = targetWidth; canvas.height = targetHeight; - const context = canvas.getContext("2d"); - context.drawImage(element, 0, 0, targetWidth, targetHeight); - const imageData = context.getImageData(0, 0, targetWidth, targetHeight); - const blurhash = encode( - imageData.data, - imageData.width, - imageData.height, - BLURHASH_X_COMPONENTS, - BLURHASH_Y_COMPONENTS, - ); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, - [BLURHASH_FIELD]: blurhash, - }, - thumbnail, - }); - }, mimeType); - }); + } + + const context = canvas.getContext("2d"); + context.drawImage(element, 0, 0, targetWidth, targetHeight); + + let thumbnailPromise: Promise; + + if (window.OffscreenCanvas) { + thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType }); + } else { + thumbnailPromise = new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); + } + + const imageData = context.getImageData(0, 0, targetWidth, targetHeight); + // thumbnailPromise and blurhash promise are being awaited concurrently + const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData); + const thumbnail = await thumbnailPromise; + + return { + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, + [BLURHASH_FIELD]: blurhash, + }, + thumbnail, + }; } /** @@ -334,7 +335,7 @@ export function uploadFile( roomId: string, file: File | Blob, progressHandler?: any, // TODO: Types -): Promise<{url?: string, file?: any}> { // TODO: Types +): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. @@ -366,8 +367,8 @@ export function uploadFile( encryptInfo.mimetype = file.type; } return { "file": encryptInfo }; - }); - (prom as IAbortablePromise).abort = () => { + }) as IAbortablePromise<{ file: any }>; + prom.abort = () => { canceled = true; if (uploadPromise) matrixClient.cancelUpload(uploadPromise); }; @@ -380,8 +381,8 @@ export function uploadFile( if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. return { url }; - }); - (promise1 as any).abort = () => { + }) as IAbortablePromise<{ url: string }>; + promise1.abort = () => { canceled = true; matrixClient.cancelUpload(basePromise); }; @@ -419,14 +420,15 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { + // FIXME: Using an import will result in Element crashing const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); 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"), @@ -453,6 +455,7 @@ export default class ContentMessages { } if (tooBigFiles.length > 0) { + // FIXME: Using an import will result in Element crashing const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles, @@ -463,7 +466,6 @@ export default class ContentMessages { if (!shouldContinue) return; } - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); let uploadAll = false; // Promise to complete before sending next file into room, used for synchronisation of file-sending // to match the order the files were specified in @@ -471,6 +473,8 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { + // FIXME: Using an import will result in Element crashing + const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { file, @@ -549,10 +553,10 @@ export default class ContentMessages { content.msgtype = 'm.file'; resolve(); } - }); + }) as IAbortablePromise; // create temporary abort handler for before the actual upload gets passed off to js-sdk - (prom as IAbortablePromise).abort = () => { + prom.abort = () => { upload.canceled = true; }; @@ -567,7 +571,7 @@ export default class ContentMessages { dis.dispatch({ action: Action.UploadStarted, upload }); // Focus the composer view - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); function onProgress(ev) { upload.total = ev.total; @@ -581,9 +585,7 @@ export default class ContentMessages { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. - upload.promise = uploadFile( - matrixClient, roomId, file, onProgress, - ); + upload.promise = uploadFile(matrixClient, roomId, file, onProgress); return upload.promise.then(function(result) { content.file = result.file; content.url = result.url; @@ -606,6 +608,7 @@ export default class ContentMessages { { fileName: upload.fileName }, ); } + // FIXME: Using an import will result in Element crashing const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { title: _t('Upload Failed'), diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index a75c578536..72b0462bcd 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -364,8 +364,8 @@ export default class CountlyAnalytics { private initTime = CountlyAnalytics.getTimestamp(); private firstPage = true; - private heartbeatIntervalId: NodeJS.Timeout; - private activityIntervalId: NodeJS.Timeout; + private heartbeatIntervalId: number; + private activityIntervalId: number; private trackTime = true; private lastBeat: number; private storedDuration = 0; diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index d40574a6db..df306a54f5 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -46,8 +46,8 @@ export class DecryptionFailureTracker { }; // Set to an interval ID when `start` is called - public checkInterval: NodeJS.Timeout = null; - public trackInterval: NodeJS.Timeout = null; + public checkInterval: number = null; + public trackInterval: number = null; // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. static TRACK_INTERVAL_MS = 60000; diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index d70585e5ec..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 @@ -160,34 +161,35 @@ export default class DeviceListener { // which result in account data changes affecting checks below. if ( ev.getType().startsWith('m.secret_storage.') || - ev.getType().startsWith('m.cross_signing.') + 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(); @@ -205,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; @@ -234,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); @@ -255,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 016b557477..af5d2b3019 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -25,7 +25,6 @@ import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; -import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; import { IContent } from 'matrix-js-sdk/src/models/event'; @@ -34,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"; @@ -58,7 +57,9 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; + +const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; /* * Return true if the given string contains emoji @@ -78,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 { @@ -151,10 +140,8 @@ export function getHtmlText(insaneHtml: string): string { */ export function isUrlPermitted(inputUrl: string): boolean { try { - const parsed = url.parse(inputUrl); - if (!parsed.protocol) return false; // URL parser protocol includes the trailing colon - return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); + return PERMITTED_URL_SCHEMES.includes(new URL(inputUrl).protocol.slice(0, -1)); } catch (e) { return false; } @@ -176,18 +163,31 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to return { tagName, attribs }; }, 'img': function(tagName: string, attribs: sanitizeHtml.Attributes) { + let src = attribs.src; // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. // We also drop inline images (as if they were not present at all) when the "show // images" preference is disabled. Future work might expose some UI to reveal them // like standalone image events have. - if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { + if (!src || !SettingsStore.getValue("showImages")) { return { tagName, attribs: {} }; } + + if (!src.startsWith("mxc://")) { + const match = MEDIA_API_MXC_REGEX.exec(src); + if (match) { + src = `mxc://${match[1]}/${match[2]}`; + } + } + + if (!src.startsWith("mxc://")) { + return { tagName, attribs: {} }; + } + const width = Number(attribs.width) || 800; const height = Number(attribs.height) || 600; - attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); + attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height); return { tagName, attribs }; }, 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 31a5021317..e91e1d72cf 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -127,7 +127,7 @@ export default class IdentityAuthClient { await this._matrixClient.getIdentityAccount(token); } catch (e) { if (e.errcode === "M_TERMS_NOT_SIGNED") { - console.log("Identity Server requires new terms to be agreed to"); + console.log("Identity server requires new terms to be agreed to"); await startTermsFlow([new Service( SERVICE_TYPES.IS, identityServerUrl, @@ -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/Lifecycle.ts b/src/Lifecycle.ts index 76dee5ab55..410124a637 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -21,6 +21,7 @@ import { createClient } from 'matrix-js-sdk/src/matrix'; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; +import { QueryDict } from 'matrix-js-sdk/src/utils'; import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg'; import SecurityCustomisations from "./customisations/Security"; @@ -33,7 +34,6 @@ import Presence from './Presence'; import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; -import * as sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; @@ -52,6 +52,10 @@ import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import { _t } from "./languageHandler"; +import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog"; +import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog"; +import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; +import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -62,7 +66,7 @@ interface ILoadSessionOpts { guestIsUrl?: string; ignoreGuest?: boolean; defaultDeviceDisplayName?: string; - fragmentQueryParams?: Record; + fragmentQueryParams?: QueryDict; } /** @@ -115,8 +119,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise ) { console.log("Using guest access credentials"); return doSetLoggedIn({ - userId: fragmentQueryParams.guest_user_id, - accessToken: fragmentQueryParams.guest_access_token, + userId: fragmentQueryParams.guest_user_id as string, + accessToken: fragmentQueryParams.guest_access_token as string, homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, @@ -170,7 +174,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> { * login, else false */ export function attemptTokenLogin( - queryParams: Record, + queryParams: QueryDict, defaultDeviceDisplayName?: string, fragmentAfterLogin?: string, ): Promise { @@ -195,7 +199,7 @@ export function attemptTokenLogin( homeserver, identityServer, "m.login.token", { - token: queryParams.loginToken, + token: queryParams.loginToken as string, initial_device_display_name: defaultDeviceDisplayName, }, ).then(function(creds) { @@ -238,8 +242,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { return Promise.resolve().then(() => { const lazyLoadEnabled = e.value; if (lazyLoadEnabled) { - const LazyLoadingResyncDialog = - sdk.getComponent("views.dialogs.LazyLoadingResyncDialog"); return new Promise((resolve) => { Modal.createDialog(LazyLoadingResyncDialog, { onFinished: resolve, @@ -250,8 +252,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { // between LL/non-LL version on same host. // as disabling LL when previously enabled // is a strong indicator of this (/develop & /app) - const LazyLoadingDisabledDialog = - sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog"); return new Promise((resolve) => { Modal.createDialog(LazyLoadingDisabledDialog, { onFinished: resolve, @@ -451,9 +451,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): async function handleLoadSessionFailure(e: Error): Promise { console.error("Unable to load session", e); - const SessionRestoreErrorDialog = - sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, }); @@ -612,7 +609,6 @@ async function doSetLoggedIn( } function showStorageEvictedDialog(): Promise { - const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog'); return new Promise(resolve => { Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, { onFinished: resolve, diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index cb6cb5c65c..f43351aab2 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix'; -import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { ICreateClientOpts, PendingEventOrdering } from 'matrix-js-sdk/src/matrix'; +import { IStartClientOpts, MatrixClient } from 'matrix-js-sdk/src/client'; import { MemoryStore } from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; @@ -47,25 +47,8 @@ export interface IMatrixClientCreds { freshLogin?: boolean; } -// TODO: Move this to the js-sdk -export interface IOpts { - initialSyncLimit?: number; - pendingEventOrdering?: "detached" | "chronological"; - lazyLoadMembers?: boolean; - clientWellKnownPollPeriod?: number; -} - export interface IMatrixClientPeg { - opts: IOpts; - - /** - * Sets the script href passed to the IndexedDB web worker - * If set, a separate web worker will be started to run the IndexedDB - * queries on. - * - * @param {string} script href to the script to be passed to the web worker - */ - setIndexedDbWorkerScript(script: string): void; + opts: IStartClientOpts; /** * Return the server name of the user's homeserver @@ -122,12 +105,12 @@ 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' // event is finished processing. - public opts: IOpts = { + public opts: IStartClientOpts = { initialSyncLimit: 20, }; @@ -141,10 +124,6 @@ class _MatrixClientPeg implements IMatrixClientPeg { constructor() { } - public setIndexedDbWorkerScript(script: string): void { - createMatrixClient.indexedDbWorkerScript = script; - } - public get(): MatrixClient { return this.matrixClient; } @@ -219,6 +198,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { } catch (e) { if (e && e.name === 'InvalidCryptoStoreError') { // The js-sdk found a crypto DB too new for it to use + // FIXME: Using an import will result in test failures const CryptoStoreTooNewDialog = sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); Modal.createDialog(CryptoStoreTooNewDialog); @@ -230,7 +210,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow - opts.pendingEventOrdering = "detached"; + opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours @@ -320,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/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 49ef123def..073f24523d 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -20,12 +20,15 @@ import { SettingLevel } from "./settings/SettingLevel"; import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; import EventEmitter from 'events'; -interface IMediaDevices { - audioOutput: Array; - audioInput: Array; - videoInput: Array; +// XXX: MediaDeviceKind is a union type, so we make our own enum +export enum MediaDeviceKindEnum { + AudioOutput = "audiooutput", + AudioInput = "audioinput", + VideoInput = "videoinput", } +export type IMediaDevices = Record>; + export enum MediaDeviceHandlerEvent { AudioOutputChanged = "audio_output_changed", } @@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter { try { const devices = await navigator.mediaDevices.enumerateDevices(); + const output = { + [MediaDeviceKindEnum.AudioOutput]: [], + [MediaDeviceKindEnum.AudioInput]: [], + [MediaDeviceKindEnum.VideoInput]: [], + }; - const audioOutput = []; - const audioInput = []; - const videoInput = []; - - devices.forEach((device) => { - switch (device.kind) { - case 'audiooutput': audioOutput.push(device); break; - case 'audioinput': audioInput.push(device); break; - case 'videoinput': videoInput.push(device); break; - } - }); - - return { audioOutput, audioInput, videoInput }; + devices.forEach((device) => output[device.kind].push(device)); + return output; } catch (error) { console.warn('Unable to refresh WebRTC Devices: ', error); } @@ -106,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter { setMatrixCallVideoInput(deviceId); } + public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void { + switch (kind) { + case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break; + case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break; + case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break; + } + } + public static getAudioOutput(): string { return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); } 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/Notifier.ts b/src/Notifier.ts index 2335dc59ac..1137e44aec 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -27,7 +27,6 @@ import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; import * as Avatar from './Avatar'; import dis from './dispatcher/dispatcher'; -import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import SettingsStore from "./settings/SettingsStore"; @@ -37,6 +36,7 @@ import { isPushNotifyDisabled } from "./settings/controllers/NotificationControl import RoomViewStore from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; /* * Dispatches: @@ -240,7 +240,6 @@ export const Notifier = { ? _t('%(brand)s does not have permission to send you notifications - ' + 'please check your browser settings', { brand }) : _t('%(brand)s was not given permission to send notifications - please try again', { brand }); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { title: _t('Unable to enable Notifications'), description, @@ -329,7 +328,7 @@ export const Notifier = { onEvent: function(ev: MatrixEvent) { if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; + if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return; MatrixClientPeg.get().decryptEventIfNeeded(ev); diff --git a/src/Registration.js b/src/Registration.js index 70dcd38454..c59d244149 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) { description: _t("Use your account or create a new one to continue."), button: _t("Create Account"), extraButtons: [ - , + , ], onFinished: (proceed) => { if (proceed) { diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index c7f377b6e8..7d093f4092 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -22,13 +22,13 @@ import { User } from "matrix-js-sdk/src/models/user"; import { MatrixClientPeg } from './MatrixClientPeg'; import MultiInviter, { CompletionStates } from './utils/MultiInviter'; import Modal from './Modal'; -import * as sdk from './'; import { _t } from './languageHandler'; import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; import BaseAvatar from "./components/views/avatars/BaseAvatar"; import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; export interface IInviteResult { states: CompletionStates; @@ -51,7 +51,6 @@ export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promi export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. - const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( 'Start DM', '', InviteDialog, { kind: KIND_DM, initialText }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, @@ -111,7 +110,6 @@ export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t("Failed to invite"), description: ((err && err.message) ? err.message : _t("Operation failed")), @@ -131,7 +129,6 @@ export function showAnyInviteErrors( // Just get the first message because there was a fatal problem on the first // user. This usually means that no other users were attempted, making it // pointless for us to list who failed exactly. - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { title: _t("Failed to invite users to the room:", { roomName: room.name }), description: inviter.getErrorText(failedUsers[0]), @@ -178,7 +175,6 @@ export function showAnyInviteErrors(
; - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { title: _t("Some invites couldn't be sent"), description, diff --git a/src/Rooms.ts b/src/Rooms.ts index 4d1682660b..6e2fd4d3a2 100644 --- a/src/Rooms.ts +++ b/src/Rooms.ts @@ -17,6 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from './MatrixClientPeg'; +import AliasCustomisations from './customisations/Alias'; /** * Given a room object, return the alias we should use for it, @@ -28,7 +29,18 @@ import { MatrixClientPeg } from './MatrixClientPeg'; * @returns {string} A display alias for the given room */ export function getDisplayAliasForRoom(room: Room): string { - return room.getCanonicalAlias() || room.getAltAliases()[0]; + return getDisplayAliasForAliasSet( + room.getCanonicalAlias(), room.getAltAliases(), + ); +} + +// The various display alias getters should all feed through this one path so +// there's a single place to change the logic. +export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string { + if (AliasCustomisations.getDisplayAliasForAliasSet) { + return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases); + } + return canonicalAlias || altAliases?.[0]; } export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { @@ -72,10 +84,8 @@ export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise this room as a DM room * @returns {object} A promise */ -export function setDMRoom(roomId: string, userId: string): Promise { - if (MatrixClientPeg.get().isGuest()) { - return Promise.resolve(); - } +export async function setDMRoom(roomId: string, userId: string): Promise { + if (MatrixClientPeg.get().isGuest()) return; const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct'); let dmRoomMap = {}; @@ -104,7 +114,7 @@ export function setDMRoom(roomId: string, userId: string): Promise { dmRoomMap[userId] = roomList; } - return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); + await MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); } /** diff --git a/src/Searching.js b/src/Searching.ts similarity index 79% rename from src/Searching.js rename to src/Searching.ts index d0666b1760..37f85efa77 100644 --- a/src/Searching.js +++ b/src/Searching.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 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. @@ -14,26 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { + IResultRoomEvents, + ISearchRequestBody, + ISearchResponse, + ISearchResult, + ISearchResults, + SearchOrderBy, +} from "matrix-js-sdk/src/@types/search"; +import { IRoomEventFilter } from "matrix-js-sdk/src/filter"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { ISearchArgs } from "./indexing/BaseEventIndexManager"; import EventIndexPeg from "./indexing/EventIndexPeg"; import { MatrixClientPeg } from "./MatrixClientPeg"; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; const SEARCH_LIMIT = 10; -async function serverSideSearch(term, roomId = undefined) { +async function serverSideSearch( + term: string, + roomId: string = undefined, +): Promise<{ response: ISearchResponse, query: ISearchRequestBody }> { const client = MatrixClientPeg.get(); - const filter = { + const filter: IRoomEventFilter = { limit: SEARCH_LIMIT, }; if (roomId !== undefined) filter.rooms = [roomId]; - const body = { + const body: ISearchRequestBody = { search_categories: { room_events: { search_term: term, filter: filter, - order_by: "recent", + order_by: SearchOrderBy.Recent, event_context: { before_limit: 1, after_limit: 1, @@ -45,31 +61,26 @@ async function serverSideSearch(term, roomId = undefined) { const response = await client.search({ body: body }); - const result = { - response: response, - query: body, - }; - - return result; + return { response, query: body }; } -async function serverSideSearchProcess(term, roomId = undefined) { +async function serverSideSearchProcess(term: string, roomId: string = undefined): Promise { const client = MatrixClientPeg.get(); const result = await serverSideSearch(term, roomId); // The js-sdk method backPaginateRoomEventsSearch() uses _query internally - // so we're reusing the concept here since we wan't to delegate the + // so we're reusing the concept here since we want to delegate the // pagination back to backPaginateRoomEventsSearch() in some cases. - const searchResult = { + const searchResults: ISearchResults = { _query: result.query, results: [], highlights: [], }; - return client.processRoomEventsSearch(searchResult, result.response); + return client.processRoomEventsSearch(searchResults, result.response); } -function compareEvents(a, b) { +function compareEvents(a: ISearchResult, b: ISearchResult): number { const aEvent = a.result; const bEvent = b.result; @@ -79,7 +90,7 @@ function compareEvents(a, b) { return 0; } -async function combinedSearch(searchTerm) { +async function combinedSearch(searchTerm: string): Promise { const client = MatrixClientPeg.get(); // Create two promises, one for the local search, one for the @@ -111,10 +122,10 @@ async function combinedSearch(searchTerm) { // returns since that one can be either a server-side one, a local one or a // fake one to fetch the remaining cached events. See the docs for // combineEvents() for an explanation why we need to cache events. - const emptyResult = { + const emptyResult: ISeshatSearchResults = { seshatQuery: localQuery, _query: serverQuery, - serverSideNextBatch: serverResponse.next_batch, + serverSideNextBatch: serverResponse.search_categories.room_events.next_batch, cachedEvents: [], oldestEventFrom: "server", results: [], @@ -125,7 +136,7 @@ async function combinedSearch(searchTerm) { const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); // Let the client process the combined result. - const response = { + const response: ISearchResponse = { search_categories: { room_events: combinedResult, }, @@ -139,10 +150,14 @@ async function combinedSearch(searchTerm) { return result; } -async function localSearch(searchTerm, roomId = undefined, processResult = true) { +async function localSearch( + searchTerm: string, + roomId: string = undefined, + processResult = true, +): Promise<{ response: IResultRoomEvents, query: ISearchArgs }> { const eventIndex = EventIndexPeg.get(); - const searchArgs = { + const searchArgs: ISearchArgs = { search_term: searchTerm, before_limit: 1, after_limit: 1, @@ -167,11 +182,18 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true) return result; } -async function localSearchProcess(searchTerm, roomId = undefined) { +export interface ISeshatSearchResults extends ISearchResults { + seshatQuery?: ISearchArgs; + cachedEvents?: ISearchResult[]; + oldestEventFrom?: "local" | "server"; + serverSideNextBatch?: string; +} + +async function localSearchProcess(searchTerm: string, roomId: string = undefined): Promise { const emptyResult = { results: [], highlights: [], - }; + } as ISeshatSearchResults; if (searchTerm === "") return emptyResult; @@ -179,7 +201,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) { emptyResult.seshatQuery = result.query; - const response = { + const response: ISearchResponse = { search_categories: { room_events: result.response, }, @@ -192,7 +214,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) { return processedResult; } -async function localPagination(searchResult) { +async function localPagination(searchResult: ISeshatSearchResults): Promise { const eventIndex = EventIndexPeg.get(); const searchArgs = searchResult.seshatQuery; @@ -221,10 +243,10 @@ async function localPagination(searchResult) { return result; } -function compareOldestEvents(firstResults, secondResults) { +function compareOldestEvents(firstResults: ISearchResult[], secondResults: ISearchResult[]): number { try { - const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result; - const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result; + const oldestFirstEvent = firstResults[firstResults.length - 1].result; + const oldestSecondEvent = secondResults[secondResults.length - 1].result; if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { return -1; @@ -236,7 +258,12 @@ function compareOldestEvents(firstResults, secondResults) { } } -function combineEventSources(previousSearchResult, response, a, b) { +function combineEventSources( + previousSearchResult: ISeshatSearchResults, + response: IResultRoomEvents, + a: ISearchResult[], + b: ISearchResult[], +): void { // Merge event sources and sort the events. const combinedEvents = a.concat(b).sort(compareEvents); // Put half of the events in the response, and cache the other half. @@ -353,8 +380,12 @@ function combineEventSources(previousSearchResult, response, a, b) { * different event sources. * */ -function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) { - const response = {}; +function combineEvents( + previousSearchResult: ISeshatSearchResults, + localEvents: IResultRoomEvents = undefined, + serverEvents: IResultRoomEvents = undefined, +): IResultRoomEvents { + const response = {} as IResultRoomEvents; const cachedEvents = previousSearchResult.cachedEvents; let oldestEventFrom = previousSearchResult.oldestEventFrom; @@ -364,7 +395,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven // This is a first search call, combine the events from the server and // the local index. Note where our oldest event came from, we shall // fetch the next batch of events from the other source. - if (compareOldestEvents(localEvents, serverEvents) < 0) { + if (compareOldestEvents(localEvents.results, serverEvents.results) < 0) { oldestEventFrom = "local"; } @@ -375,7 +406,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven // meaning that our oldest event was on the server. // Change the source of the oldest event if our local event is older // than the cached one. - if (compareOldestEvents(localEvents, cachedEvents) < 0) { + if (compareOldestEvents(localEvents.results, cachedEvents) < 0) { oldestEventFrom = "local"; } combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); @@ -384,7 +415,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven // meaning that our oldest event was in the local index. // Change the source of the oldest event if our server event is older // than the cached one. - if (compareOldestEvents(serverEvents, cachedEvents) < 0) { + if (compareOldestEvents(serverEvents.results, cachedEvents) < 0) { oldestEventFrom = "server"; } combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents); @@ -412,7 +443,11 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven * @return {object} A response object that combines the events from the * different event sources. */ -function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { +function combineResponses( + previousSearchResult: ISeshatSearchResults, + localEvents: IResultRoomEvents = undefined, + serverEvents: IResultRoomEvents = undefined, +): IResultRoomEvents { // Combine our events first. const response = combineEvents(previousSearchResult, localEvents, serverEvents); @@ -454,42 +489,51 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE return response; } -function restoreEncryptionInfo(searchResultSlice = []) { +interface IEncryptedSeshatEvent { + curve25519Key: string; + ed25519Key: string; + algorithm: string; + forwardingCurve25519KeyChain: string[]; +} + +function restoreEncryptionInfo(searchResultSlice: SearchResult[] = []): void { for (let i = 0; i < searchResultSlice.length; i++) { const timeline = searchResultSlice[i].context.getTimeline(); for (let j = 0; j < timeline.length; j++) { - const ev = timeline[j]; + const mxEv = timeline[j]; + const ev = mxEv.event as IEncryptedSeshatEvent; - if (ev.event.curve25519Key) { - ev.makeEncrypted( - "m.room.encrypted", - { algorithm: ev.event.algorithm }, - ev.event.curve25519Key, - ev.event.ed25519Key, + if (ev.curve25519Key) { + mxEv.makeEncrypted( + EventType.RoomMessageEncrypted, + { algorithm: ev.algorithm }, + ev.curve25519Key, + ev.ed25519Key, ); - ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + // @ts-ignore + mxEv.forwardingCurve25519KeyChain = ev.forwardingCurve25519KeyChain; - delete ev.event.curve25519Key; - delete ev.event.ed25519Key; - delete ev.event.algorithm; - delete ev.event.forwardingCurve25519KeyChain; + delete ev.curve25519Key; + delete ev.ed25519Key; + delete ev.algorithm; + delete ev.forwardingCurve25519KeyChain; } } } } -async function combinedPagination(searchResult) { +async function combinedPagination(searchResult: ISeshatSearchResults): Promise { const eventIndex = EventIndexPeg.get(); const client = MatrixClientPeg.get(); const searchArgs = searchResult.seshatQuery; const oldestEventFrom = searchResult.oldestEventFrom; - let localResult; - let serverSideResult; + let localResult: IResultRoomEvents; + let serverSideResult: ISearchResponse; - // Fetch events from the local index if we have a token for itand if it's + // Fetch events from the local index if we have a token for it and if it's // the local indexes turn or the server has exhausted its results. if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) { localResult = await eventIndex.search(searchArgs); @@ -502,7 +546,7 @@ async function combinedPagination(searchResult) { serverSideResult = await client.search(body); } - let serverEvents; + let serverEvents: IResultRoomEvents; if (serverSideResult) { serverEvents = serverSideResult.search_categories.room_events; @@ -532,8 +576,8 @@ async function combinedPagination(searchResult) { return result; } -function eventIndexSearch(term, roomId = undefined) { - let searchPromise; +function eventIndexSearch(term: string, roomId: string = undefined): Promise { + let searchPromise: Promise; if (roomId !== undefined) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { @@ -554,7 +598,7 @@ function eventIndexSearch(term, roomId = undefined) { return searchPromise; } -function eventIndexSearchPagination(searchResult) { +function eventIndexSearchPagination(searchResult: ISeshatSearchResults): Promise { const client = MatrixClientPeg.get(); const seshatQuery = searchResult.seshatQuery; @@ -580,7 +624,7 @@ function eventIndexSearchPagination(searchResult) { } } -export function searchPagination(searchResult) { +export function searchPagination(searchResult: ISearchResults): Promise { const eventIndex = EventIndexPeg.get(); const client = MatrixClientPeg.get(); @@ -590,7 +634,7 @@ export function searchPagination(searchResult) { else return eventIndexSearchPagination(searchResult); } -export default function eventSearch(term, roomId = undefined) { +export default function eventSearch(term: string, roomId: string = undefined): Promise { const eventIndex = EventIndexPeg.get(); if (eventIndex === null) return serverSideSearchProcess(term, roomId); diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 214047c4fa..370b21b396 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ICryptoCallbacks } from 'matrix-js-sdk/src/matrix'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; @@ -354,6 +355,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f throw new Error("Secret storage creation canceled"); } } else { + // FIXME: Using an import will result in test failures const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest) => { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 128ca9e5e2..b4deb6d8c4 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -23,7 +23,6 @@ import { User } from "matrix-js-sdk/src/models/user"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import * as sdk from './index'; import { _t, _td } from './languageHandler'; import Modal from './Modal'; import MultiInviter from './utils/MultiInviter'; @@ -35,7 +34,6 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; -import { inviteUsersToRoom } from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; @@ -50,6 +48,13 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import { upgradeRoom } from './utils/RoomUpgrade'; +import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; +import ErrorDialog from './components/views/dialogs/ErrorDialog'; +import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; +import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; +import InfoDialog from "./components/views/dialogs/InfoDialog"; +import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -63,7 +68,6 @@ const singleMxcUpload = async (): Promise => { fileSelector.onchange = (ev: HTMLInputEvent) => { const file = ev.target.files[0]; - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { file, onFinished: (shouldContinue) => { @@ -246,7 +250,6 @@ export const Commands = [ args: '', description: _td('Searches DuckDuckGo for results'), runFn: function() { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { title: _t('/ddg is not a command'), @@ -269,58 +272,13 @@ export const Commands = [ return reject(_t("You do not have the required permissions to use this command.")); } - const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog"); - const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { - if (!resp.continue) return; - - let checkForUpgradeFn; - try { - const upgradePromise = cli.upgradeRoom(roomId, args); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (resp.invite) { - checkForUpgradeFn = async (newRoom) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - await upgradePromise; - } catch (e) { - console.error(e); - - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { - title: _t('Error upgrading room'), - description: _t( - 'Double check that your server supports the room version chosen and try again.'), - }); - } + if (!resp?.continue) return; + await upgradeRoom(room, args, resp.invite); })); } return reject(this.getUsage()); @@ -434,7 +392,6 @@ export const Commands = [ const topic = topicEvents && topicEvents.getContent().topic; const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description:
, @@ -481,14 +438,14 @@ export const Commands = [ 'Identity server', QuestionDialog, { title: _t("Use an identity server"), - description:

{_t( + description:

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

, + ) }

, button: _t("Continue"), }, ); @@ -523,7 +480,7 @@ export const Commands = [ aliases: ['j', 'goto'], args: '', description: _td('Joins room with given address'), - runFn: function(_, args) { + runFn: function(roomId, args) { if (args) { // Note: we support 2 versions of this command. The first is // the public-facing one for most users and the other is a @@ -737,7 +694,6 @@ export const Commands = [ ignoredUsers.push(userId); // de-duped internally in the js-sdk return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { title: _t('Ignored user'), description:
@@ -768,7 +724,6 @@ export const Commands = [ if (index !== -1) ignoredUsers.splice(index, 1); return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { title: _t('Unignored user'), description:
@@ -838,7 +793,6 @@ export const Commands = [ command: 'devtools', description: _td('Opens the Developer Tools dialog'), runFn: function(roomId) { - const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); Modal.createDialog(DevtoolsDialog, { roomId }); return success(); }, @@ -943,7 +897,6 @@ export const Commands = [ await cli.setDeviceVerified(userId, deviceId, true); // Tell the user we verified everything - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { title: _t('Verified key'), description:
@@ -1000,8 +953,6 @@ export const Commands = [ command: "help", description: _td("Displays list of commands with usages and descriptions"), runFn: function() { - const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog'); - Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog); return success(); }, @@ -1076,7 +1027,7 @@ export const Commands = [ command: "msg", description: _td("Sends a message to the given user"), args: " ", - runFn: function(_, args) { + runFn: function(roomId, args) { if (args) { // matches the first whitespace delimited group and then the rest of the string const matches = args.match(/^(\S+?)(?: +(.*))?$/s); diff --git a/src/Terms.ts b/src/Terms.ts index e9ba5ff8be..351d1c0951 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -189,6 +189,7 @@ export function dialogTermsInteractionCallback( ): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); + // FIXME: Using an import will result in test failures const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); Modal.createTrackedDialog('Terms of Service', '', TermsDialog, { diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 844c79fbae..7bad8eb50e 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -13,9 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - import React from 'react'; -import { MatrixClientPeg } from './MatrixClientPeg'; import { _t } from './languageHandler'; import * as Roles from './Roles'; import { isValid3pidInvite } from "./RoomInvite"; @@ -32,7 +30,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // any text to display at all. For this reason they return deferred values // to avoid the expense of looking up translations when they're not needed. -function textForMemberEvent(ev): () => string | null { +function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); const targetName = ev.target ? ev.target.name : ev.getStateKey(); @@ -84,7 +82,7 @@ function textForMemberEvent(ev): () => string | null { return () => _t('%(senderName)s changed their profile picture', { senderName }); } else if (!prevContent.avatar_url && content.avatar_url) { return () => _t('%(senderName)s set a profile picture', { senderName }); - } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { + } else if (showHiddenEvents ?? SettingsStore.getValue("showHiddenEventsInTimeline")) { // This is a null rejoin, it will only be visible if using 'show hidden events' (labs) return () => _t("%(senderName)s made no change", { senderName }); } else { @@ -127,7 +125,7 @@ function textForMemberEvent(ev): () => string | null { } } -function textForTopicEvent(ev): () => string | null { +function textForTopicEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { senderDisplayName, @@ -135,7 +133,7 @@ function textForTopicEvent(ev): () => string | null { }); } -function textForRoomNameEvent(ev): () => string | null { +function textForRoomNameEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { @@ -154,12 +152,12 @@ function textForRoomNameEvent(ev): () => string | null { }); } -function textForTombstoneEvent(ev): () => string | null { +function textForTombstoneEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName }); } -function textForJoinRulesEvent(ev): () => string | null { +function textForJoinRulesEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().join_rule) { case "public": @@ -179,7 +177,7 @@ function textForJoinRulesEvent(ev): () => string | null { } } -function textForGuestAccessEvent(ev): () => string | null { +function textForGuestAccessEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().guest_access) { case "can_join": @@ -195,7 +193,7 @@ function textForGuestAccessEvent(ev): () => string | null { } } -function textForRelatedGroupsEvent(ev): () => string | null { +function textForRelatedGroupsEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const groups = ev.getContent().groups || []; const prevGroups = ev.getPrevContent().groups || []; @@ -225,7 +223,7 @@ function textForRelatedGroupsEvent(ev): () => string | null { } } -function textForServerACLEvent(ev): () => string | null { +function textForServerACLEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); const current = ev.getContent(); @@ -255,7 +253,7 @@ function textForServerACLEvent(ev): () => string | null { return getText; } -function textForMessageEvent(ev): () => string | null { +function textForMessageEvent(ev: MatrixEvent): () => string | null { return () => { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); let message = senderDisplayName + ': ' + ev.getContent().body; @@ -268,7 +266,7 @@ function textForMessageEvent(ev): () => string | null { }; } -function textForCanonicalAliasEvent(ev): () => string | null { +function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const oldAlias = ev.getPrevContent().alias; const oldAltAliases = ev.getPrevContent().alt_aliases || []; @@ -319,91 +317,7 @@ function textForCanonicalAliasEvent(ev): () => string | null { }); } -function textForCallAnswerEvent(event): () => 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): () => 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): () => string | null { - return () => { - const senderName = event.sender ? event.sender.name : _t('Someone'); - return _t('%(senderName)s declined the call.', { senderName }); - }; -} - -function textForCallInviteEvent(event): () => 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): () => string | null { +function textForThreePidInviteEvent(event: MatrixEvent): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); if (!isValid3pidInvite(event)) { @@ -419,7 +333,7 @@ function textForThreePidInviteEvent(event): () => string | null { }); } -function textForHistoryVisibilityEvent(event): () => string | null { +function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); switch (event.getContent().history_visibility) { case 'invited': @@ -441,13 +355,14 @@ function textForHistoryVisibilityEvent(event): () => string | null { } // Currently will only display a change if a user's power level is changed -function textForPowerEvent(event): () => string | null { +function textForPowerEvent(event: MatrixEvent): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); if (!event.getPrevContent() || !event.getPrevContent().users || !event.getContent() || !event.getContent().users) { return null; } - const userDefault = event.getContent().users_default || 0; + const previousUserDefault = event.getPrevContent().users_default || 0; + const currentUserDefault = event.getContent().users_default || 0; // Construct set of userIds const users = []; Object.keys(event.getContent().users).forEach( @@ -463,9 +378,16 @@ function textForPowerEvent(event): () => string | null { const diffs = []; users.forEach((userId) => { // Previous power level - const from = event.getPrevContent().users[userId]; + let from = event.getPrevContent().users[userId]; + if (!Number.isInteger(from)) { + from = previousUserDefault; + } // Current power level - const to = event.getContent().users[userId]; + let to = event.getContent().users[userId]; + if (!Number.isInteger(to)) { + to = currentUserDefault; + } + if (from === previousUserDefault && to === currentUserDefault) { return; } if (to !== from) { diffs.push({ userId, from, to }); } @@ -479,8 +401,8 @@ function textForPowerEvent(event): () => string | null { powerLevelDiffText: diffs.map(diff => _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { userId: diff.userId, - fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault), - toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault), + fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault), + toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault), }), ).join(", "), }); @@ -515,7 +437,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName }); } -function textForWidgetEvent(event): () => string | null { +function textForWidgetEvent(event: MatrixEvent): () => string | null { const senderName = event.getSender(); const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent(); const { name, type, url } = event.getContent() || {}; @@ -545,12 +467,12 @@ function textForWidgetEvent(event): () => string | null { } } -function textForWidgetLayoutEvent(event): () => string | null { +function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null { const senderName = event.sender?.name || event.getSender(); return () => _t("%(senderName)s has updated the widget layout", { senderName }); } -function textForMjolnirEvent(event): () => string | null { +function textForMjolnirEvent(event: MatrixEvent): () => string | null { const senderName = event.getSender(); const { entity: prevEntity } = event.getPrevContent(); const { entity, recommendation, reason } = event.getContent(); @@ -638,15 +560,13 @@ function textForMjolnirEvent(event): () => string | null { } interface IHandlers { - [type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null); + [type: string]: + (ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) => + (() => string | JSX.Element | null); } 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 = { @@ -674,14 +594,27 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -export function hasText(ev): boolean { +/** + * Determines whether the given event has text to display. + * @param ev The event + * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline + * to avoid hitting the settings store + */ +export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - return Boolean(handler?.(ev)); + return Boolean(handler?.(ev, false, showHiddenEvents)); } +/** + * Gets the textual content of the given event. + * @param ev The event + * @param allowJSX Whether to output rich JSX content + * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline + * to avoid hitting the settings store + */ export function textForEvent(ev: MatrixEvent): string; -export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element; -export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element { +export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element; +export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - return handler?.(ev, allowJSX)?.() || ''; + return handler?.(ev, allowJSX, showHiddenEvents)?.() || ''; } diff --git a/src/Unread.ts b/src/Unread.ts index 72f0bb4642..da5b883f92 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -30,7 +30,7 @@ import { haveTileForEvent } from "./components/views/rooms/EventTile"; * @returns {boolean} True if the given event should affect the unread message count */ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + if (ev.getSender() === MatrixClientPeg.get().credentials.userId) { return false; } @@ -63,9 +63,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at // https://github.com/vector-im/element-web/issues/3363 - if (room.timeline.length && - room.timeline[room.timeline.length - 1].sender && - room.timeline[room.timeline.length - 1].sender.userId === myUserId) { + if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) { return false; } diff --git a/src/UserAddress.ts b/src/UserAddress.ts index a2c546deb7..248814aa01 100644 --- a/src/UserAddress.ts +++ b/src/UserAddress.ts @@ -14,35 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from "prop-types"; - const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; - export enum AddressType { Email = "email", MatrixUserId = "mx-user-id", MatrixRoomId = "mx-room-id", } +export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId]; + // PropType definition for an object describing // an address that can be invited to a room (which // could be a third party identifier or a matrix ID) // along with some additional information about the // address / target. -export const UserAddressType = PropTypes.shape({ - addressType: PropTypes.oneOf(addressTypes).isRequired, - address: PropTypes.string.isRequired, - displayName: PropTypes.string, - avatarMxc: PropTypes.string, +export interface IUserAddress { + addressType: AddressType; + address: string; + displayName?: string; + avatarMxc?: string; // true if the address is known to be a valid address (eg. is a real // user we've seen) or false otherwise (eg. is just an address the // user has entered) - isKnown: PropTypes.bool, -}); + isKnown?: boolean; +} export function getAddressType(inputText: string): AddressType | null { if (emailRegex.test(inputText)) { diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 25c41f9db5..9cc7b60c99 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -17,10 +17,10 @@ limitations under the License. import * as React from "react"; import classNames from "classnames"; -import * as sdk from "../index"; import Modal from "../Modal"; import { _t, _td } from "../languageHandler"; import { isMac, Key } from "../Keyboard"; +import InfoDialog from "../components/views/dialogs/InfoDialog"; // TS: once languageHandler is TS we can probably inline this into the enum _td("Navigation"); @@ -370,12 +370,11 @@ export const toggleDialog = () => { const sections = categoryOrder.map(category => { const list = shortcuts[category]; return
-

{_t(category)}

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

{ _t(category) }

+
{ list.map(shortcut => ) }
; }); - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, { className: "mx_KeyboardShortcutsDialog", title: _t("Keyboard Shortcuts"), 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/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index 81e05f8678..a7f629c40d 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -19,13 +19,13 @@ import { asyncAction } from './actionCreators'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; -import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; import RoomListStore from "../stores/room-list/RoomListStore"; import { SortAlgorithm } from "../stores/room-list/algorithms/models"; import { DefaultTagID } from "../stores/room-list/models"; +import ErrorDialog from '../components/views/dialogs/ErrorDialog'; export default class RoomListActions { /** @@ -88,7 +88,6 @@ export default class RoomListActions { return Rooms.guessAndSetDMRoom( room, newTag === DefaultTagID.DM, ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { title: _t('Failed to set direct chat tag'), @@ -109,7 +108,6 @@ export default class RoomListActions { const promiseToDelete = matrixClient.deleteRoomTag( roomId, oldTag, ).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to remove tag " + oldTag + " from room: " + err); Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { title: _t('Failed to remove tag %(tagName)s from room', { tagName: oldTag }), @@ -129,7 +127,6 @@ export default class RoomListActions { metaData = metaData || {}; const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to add tag " + newTag + " to room: " + err); Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { title: _t('Failed to add tag %(tagName)s to room', { tagName: newTag }), 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 ? :
} void; @@ -132,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component { - 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, ); }; @@ -145,7 +148,6 @@ 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), - })}
+ }) }
); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( - {eventIndexingSettings} + { eventIndexingSettings } -

{_t( +

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

-

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

+

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

-

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

+ ) }

+

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

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

{_t( +

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

+ ) }

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

{_t( +

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

-

{_t( + ) }

+

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

+ ) }

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

{_t( +

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

+ ) }

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

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

+

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

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

{_t( +

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

+ ) }

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

- {_t("You'll need to authenticate with the server to confirm the upgrade.")} + { _t("You'll need to authenticate with the server to confirm the upgrade.") }

; } return -

{_t( +

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

-
{authPrompt}
+ ) }

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

{_t( +

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

+ ) }

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

{_t( +

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

+ ) }

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

{_t( +

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

+ ) }

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

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

+

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

-

{_t( +

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

-

{_t( + ) }

+

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

+ ) }

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

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

+

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

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

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

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

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

-

{_t( +

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

+

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

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

{ _t('Set a new password') }

- {resetPasswordJsx} + { resetPasswordJsx }
); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 61d3759dee..7a05d8c6c6 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -18,7 +18,6 @@ import React, { ReactNode } from 'react'; import { MatrixError } from "matrix-js-sdk/src/http-api"; import { _t, _td } from '../../../languageHandler'; -import * as sdk from '../../../index'; import Login, { ISSOFlow, LoginFlow } from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; @@ -36,6 +35,8 @@ import Spinner from "../../views/elements/Spinner"; import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from "../../views/elements/ServerPicker"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthBody from "../../views/auth/AuthBody"; +import AuthHeader from "../../views/auth/AuthHeader"; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -143,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); } @@ -153,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; @@ -238,8 +239,8 @@ export default class LoginComponent extends React.PureComponent ); errorText = (
-
{errorTop}
-
{errorDetail}
+
{ errorTop }
+
{ errorDetail }
); } else if (error.httpStatus === 401 || error.httpStatus === 403) { @@ -250,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 }, - )} + ) }
); @@ -462,7 +463,9 @@ export default class LoginComponent extends React.PureComponent "Either use HTTPS or enable unsafe scripts.", {}, { 'a': (sub) => { - return { sub } @@ -541,8 +544,6 @@ export default class LoginComponent extends React.PureComponent }; render() { - const AuthHeader = sdk.getComponent("auth.AuthHeader"); - const AuthBody = sdk.getComponent("auth.AuthBody"); const loader = this.isBusy() && !this.state.busyLoggingIn ?
: null; @@ -566,7 +567,7 @@ export default class LoginComponent extends React.PureComponent }); serverDeadSection = (
- {this.state.serverDeadError} + { this.state.serverDeadError }
); } @@ -579,15 +580,15 @@ export default class LoginComponent extends React.PureComponent { this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
{ this.props.isSyncing &&
- {_t("If you've joined lots of rooms, this might take a while")} + { _t("If you've joined lots of rooms, this might take a while") }
}
; } else if (SettingsStore.getValue(UIFeature.Registration)) { footer = ( - {_t("New? Create account", {}, { + { _t("New? Create account", {}, { a: sub => { sub }, - })} + }) } ); } @@ -597,8 +598,8 @@ export default class LoginComponent extends React.PureComponent

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

{ errorTextSection } { serverDeadSection } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 57b758091a..2b97650d4b 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -18,19 +18,24 @@ import { createClient } from 'matrix-js-sdk/src/matrix'; import React, { ReactNode } from 'react'; import { MatrixClient } from "matrix-js-sdk/src/client"; -import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; import Login, { ISSOFlow } from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from '../../views/elements/ServerPicker'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RegistrationForm from '../../views/auth/RegistrationForm'; +import AccessibleButton from '../../views/elements/AccessibleButton'; +import AuthBody from "../../views/auth/AuthBody"; +import AuthHeader from "../../views/auth/AuthHeader"; +import InteractiveAuth from "../InteractiveAuth"; +import Spinner from "../../views/elements/Spinner"; interface IProps { serverConfig: ValidatedServerConfig; @@ -47,13 +52,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: { - userId: string; - deviceId: string; - homeserverUrl: string; - identityServerUrl?: string; - accessToken: string; - }, password: string): void; + onLoggedIn(params: IMatrixClientCreds, password: string): void; makeRegistrationUrl(params: { /* eslint-disable camelcase */ client_secret: string; @@ -142,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; @@ -246,7 +245,7 @@ export default class Registration extends React.Component { } } - private onFormSubmit = formVals => { + private onFormSubmit = async (formVals): Promise => { this.setState({ errorText: "", busy: true, @@ -291,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; @@ -442,10 +441,6 @@ export default class Registration extends React.Component { }; private renderRegisterComponent() { - const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); - const Spinner = sdk.getComponent('elements.Spinner'); - const RegistrationForm = sdk.getComponent('auth.RegistrationForm'); - if (this.state.matrixClient && this.state.doingUIAuth) { return { fragmentAfterLogin={this.props.fragmentAfterLogin} />

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

; } @@ -516,10 +511,6 @@ export default class Registration extends React.Component { } render() { - const AuthHeader = sdk.getComponent('auth.AuthHeader'); - const AuthBody = sdk.getComponent("auth.AuthBody"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let errorText; const err = this.state.errorText; if (err) { @@ -535,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 @@ -559,43 +550,47 @@ export default class Registration extends React.Component { let regDoneText; if (this.state.differentLoggedInUserId) { regDoneText =
-

{_t( +

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

-

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

+

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

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

{_t( + regDoneText =

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

; + ) }; } else { // We're not the original client: the user probably got to us by clicking the // email validation link. We can't offer a 'go straight to your account' link // as we don't have the original creds. - regDoneText =

{_t( + regDoneText =

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

; + ) }; } body =
-

{_t("Registration Successful")}

+

{ _t("Registration Successful") }

{ regDoneText }
; } else { diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 13790c2e47..6731156807 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -21,7 +21,7 @@ import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; @@ -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 3790028fea..fffec949fe 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; @@ -26,6 +25,12 @@ import AuthPage from "../../views/auth/AuthPage"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; import SSOButtons from "../../views/elements/SSOButtons"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog'; +import Field from '../../views/elements/Field'; +import AccessibleButton from '../../views/elements/AccessibleButton'; +import Spinner from "../../views/elements/Spinner"; +import AuthHeader from "../../views/auth/AuthHeader"; +import AuthBody from "../../views/auth/AuthBody"; const LOGIN_VIEW = { LOADING: 1, @@ -94,7 +99,6 @@ export default class SoftLogout extends React.Component { } onClearAll = () => { - const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog'); Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { onFinished: (wipeData) => { if (!wipeData) return; @@ -202,7 +206,6 @@ export default class SoftLogout extends React.Component { private renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { - const Spinner = sdk.getComponent("elements.Spinner"); return ; } @@ -214,12 +217,9 @@ export default class SoftLogout extends React.Component { } if (this.state.loginView === LOGIN_VIEW.PASSWORD) { - const Field = sdk.getComponent("elements.Field"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - 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.", - )} + ) }

); } render() { - const AuthHeader = sdk.getComponent("auth.AuthHeader"); - const AuthBody = sdk.getComponent("auth.AuthBody"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return (

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

-

{_t("Sign in")}

+

{ _t("Sign in") }

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

{_t("Clear personal data")}

+

{ _t("Clear personal data") }

- {_t( + { _t( "Warning: Your personal data (including encryption keys) is still stored " + "in this session. Clear it if you're finished using this session, or want to sign " + "in to another account.", - )} + ) }

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

{_t( +

+

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

+ ) }

{ error }
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.tsx similarity index 95% rename from src/components/views/auth/CompleteSecurityBody.js rename to src/components/views/auth/CompleteSecurityBody.tsx index 745d7abbf2..8f6affb64e 100644 --- a/src/components/views/auth/CompleteSecurityBody.js +++ b/src/components/views/auth/CompleteSecurityBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.CompleteSecurityBody") export default class CompleteSecurityBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
{ this.props.children }
; diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.tsx similarity index 75% rename from src/components/views/auth/CountryDropdown.js rename to src/components/views/auth/CountryDropdown.tsx index cbc19e0f8d..eb5b27be9d 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.tsx @@ -15,21 +15,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; - -import { COUNTRIES, getEmojiFlag } from '../../../phonenumber'; +import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Dropdown from "../elements/Dropdown"; const COUNTRIES_BY_ISO2 = {}; for (const c of COUNTRIES) { COUNTRIES_BY_ISO2[c.iso2] = c; } -function countryMatchesSearchQuery(query, country) { +function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean { // Remove '+' if present (when searching for a prefix) if (query[0] === '+') { query = query.slice(1); @@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) { return false; } -@replaceableComponent("views.auth.CountryDropdown") -export default class CountryDropdown extends React.Component { - constructor(props) { - super(props); - this._onSearchChange = this._onSearchChange.bind(this); - this._onOptionChange = this._onOptionChange.bind(this); - this._getShortOption = this._getShortOption.bind(this); +interface IProps { + value?: string; + onOptionChange: (country: PhoneNumberCountryDefinition) => void; + isSmall: boolean; // if isSmall, show +44 in the selected value + showPrefix: boolean; + className?: string; + disabled?: boolean; +} - let defaultCountry = COUNTRIES[0]; +interface IState { + searchQuery: string; + defaultCountry: PhoneNumberCountryDefinition; +} + +@replaceableComponent("views.auth.CountryDropdown") +export default class CountryDropdown extends React.Component { + constructor(props: IProps) { + super(props); + + let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0]; const defaultCountryCode = SdkConfig.get()["defaultCountryCode"]; if (defaultCountryCode) { const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase()); @@ -62,7 +71,7 @@ export default class CountryDropdown extends React.Component { }; } - componentDidMount() { + public componentDidMount(): void { if (!this.props.value) { // If no value is given, we start with the default // country selected, but our parent component @@ -71,21 +80,21 @@ export default class CountryDropdown extends React.Component { } } - _onSearchChange(search) { + private onSearchChange = (search: string): void => { this.setState({ searchQuery: search, }); - } + }; - _onOptionChange(iso2) { + private onOptionChange = (iso2: string): void => { this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); - } + }; - _flagImgForIso2(iso2) { + private flagImgForIso2(iso2: string): React.ReactNode { return
{ getEmojiFlag(iso2) }
; } - _getShortOption(iso2) { + private getShortOption = (iso2: string): React.ReactNode => { if (!this.props.isSmall) { return undefined; } @@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component { countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; } return - { this._flagImgForIso2(iso2) } + { this.flagImgForIso2(iso2) } { countryPrefix } ; - } - - render() { - const Dropdown = sdk.getComponent('elements.Dropdown'); + }; + public render(): React.ReactNode { let displayedCountries; if (this.state.searchQuery) { displayedCountries = COUNTRIES.filter( @@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
- { this._flagImgForIso2(country.iso2) } + { this.flagImgForIso2(country.iso2) } { _t(country.name) } (+{ country.prefix })
; }); @@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component { return ; } } - -CountryDropdown.propTypes = { - className: PropTypes.string, - isSmall: PropTypes.bool, - // if isSmall, show +44 in the selected value - showPrefix: PropTypes.bool, - onOptionChange: PropTypes.func.isRequired, - value: PropTypes.string, - disabled: PropTypes.bool, -}; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index a032414eb0..0331a4d5b4 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -19,7 +19,6 @@ import classNames from 'classnames'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { AuthType, IAuthDict, IInputs, IStageStatus } from 'matrix-js-sdk/src/interactive-auth'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; @@ -27,6 +26,8 @@ import Spinner from "../elements/Spinner"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { LocalisedPolicy, Policies } from '../../../Terms'; +import Field from '../elements/Field'; +import CaptchaForm from "./CaptchaForm"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -41,7 +42,7 @@ import { LocalisedPolicy, Policies } from '../../../Terms'; * one HS whilst beign a guest on another). * loginType: the login type of the auth stage being attempted * authSessionId: session id from the server - * clientSecret: The client secret in use for ID server auth sessions + * clientSecret: The client secret in use for identity server auth sessions * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict @@ -54,8 +55,8 @@ import { LocalisedPolicy, Policies } from '../../../Terms'; * Defined keys for stages are: * m.login.email.identity: * * emailSid: string representing the sid of the active - * verification session from the ID server, or - * null if no session is active. + * verification session from the identity server, + * or null if no session is active. * fail: a function which should be called with an error object if an * error occurred during the auth stage. This will cause the auth * session to be failed and the process to go back to the start. @@ -138,8 +139,7 @@ export class PasswordAuthEntry extends React.Component; + submitButtonOrSpinner = ; } else { submitButtonOrSpinner = (

{ _t("Confirm your identity by entering your account password below.") }

@@ -210,13 +208,11 @@ export class RecaptchaAuthEntry extends React.Component; + return ; } let errorText = this.props.errorText; - const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm"); let sitePublicKey; if (!this.props.stageParams || !this.props.stageParams.public_key) { errorText = _t( @@ -364,8 +360,7 @@ export class TermsAuthEntry extends React.Component; + return ; } const checkboxes = []; @@ -395,13 +390,15 @@ export class TermsAuthEntry extends React.Component{_t("Accept")}; + submitButton = ; } return (
-

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

+

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

{ checkboxes } { errorSection } { submitButton } @@ -564,8 +561,7 @@ export class MsisdnAuthEntry extends React.Component; + return ; } else { const enableSubmit = Boolean(this.state.token); const submitClasses = classNames({ @@ -593,15 +589,17 @@ export class MsisdnAuthEntry extends React.Component
- - {errorSection} + { errorSection }
); @@ -697,21 +695,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") } ); } @@ -733,8 +731,8 @@ export class SSOAuthEntry extends React.Component { errorSection }
- {cancelButton} - {continueButton} + { cancelButton } + { continueButton }
; } @@ -805,7 +803,7 @@ export class FallbackAuthEntry extends React.Component { { _t("Start authentication") } - {errorSection} + { errorSection }
); } diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.tsx similarity index 85% rename from src/components/views/auth/LanguageSelector.js rename to src/components/views/auth/LanguageSelector.tsx index 88293310e7..c26b4797f3 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.tsx @@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig"; import { getCurrentLanguage } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; -import * as sdk from '../../../index'; import React from 'react'; import { SettingLevel } from "../../../settings/SettingLevel"; +import LanguageDropdown from "../elements/LanguageDropdown"; -function onChange(newLang) { +function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } } -export default function LanguageSelector({ disabled }) { - if (SdkConfig.get()['disable_login_language_selector']) return
; +interface IProps { + disabled?: boolean; +} - const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); +export default function LanguageSelector({ disabled }: IProps): JSX.Element { + if (SdkConfig.get()['disable_login_language_selector']) return
; return { kind="link" onClick={this.onForgotPasswordClick} > - {_t("Forgot password?")} + { _t("Forgot password?") } ; } @@ -441,16 +441,16 @@ export default class PasswordLogin extends React.PureComponent { disabled={this.props.disableSubmit} >
@@ -460,8 +460,8 @@ export default class PasswordLogin extends React.PureComponent { return (
- {loginType} - {loginField} + { loginType } + { loginField } { onValidate={this.onPasswordValidate} ref={field => this[LoginField.Password] = field} /> - {forgotPasswordJsx} + { forgotPasswordJsx } { !this.props.busy &&
- {this.renderUsername()} + { this.renderUsername() }
- {this.renderPassword()} - {this.renderPasswordConfirm()} + { this.renderPassword() } + { this.renderPasswordConfirm() }
- {this.renderEmail()} - {this.renderPhoneNumber()} + { this.renderEmail() } + { this.renderPhoneNumber() }
{ emailHelperText } { registerButton } diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.tsx similarity index 83% rename from src/components/views/auth/Welcome.js rename to src/components/views/auth/Welcome.tsx index e3f7a601f2..0e12025fbd 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import classNames from "classnames"; -import * as sdk from '../../../index'; +import * as sdk from "../../../index"; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; import { _td } from "../../../languageHandler"; @@ -25,21 +25,26 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import LanguageSelector from "./LanguageSelector"; // translatable strings for Welcome pages _td("Sign in with SSO"); +interface IProps { + +} + @replaceableComponent("views.auth.Welcome") -export default class Welcome extends React.PureComponent { - constructor(props) { +export default class Welcome extends React.PureComponent { + constructor(props: IProps) { super(props); CountlyAnalytics.instance.track("onboarding_welcome"); } - render() { - const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); - const LanguageSelector = sdk.getComponent('auth.LanguageSelector'); + public render(): React.ReactNode { + // FIXME: Using an import will result in wrench-element-tests failures + const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage"); const pagesConfig = SdkConfig.get().embeddedPages; let pageUrl = null; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 87cdbe7512..6aaef29854 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt={_t("Avatar")} + title={title} + alt={_t("Avatar")} inputRef={inputRef} {...otherProps} /> ); @@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt="" + title={title} + alt="" ref={inputRef} {...otherProps} /> ); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5e6bf45f07..99f2b70efc 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -205,8 +205,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent - {icon} - {badge} + { icon } + { badge }
; } } diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 61155e3880..11c24a5981 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component { } return ( - + ); } } diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index b8b23dc33e..82b7b8e400 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -145,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component { isExpanded={this.state.menuDisplayed} label={_t("User Status")} > - {avatar} + { avatar } { contextMenu } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 8ac8de8233..f285222f7b 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,15 +13,18 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + import React, { ComponentProps } from 'react'; import { Room } from 'matrix-js-sdk/src/models/room'; import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; +import classNames from "classnames"; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; +import DMRoomMap from "../../../utils/DMRoomMap"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; import { IOOBData } from '../../../stores/ThreepidInviteStore'; @@ -31,11 +34,14 @@ interface IProps extends Omit, "name" | "idNam // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData; + oobData?: IOOBData & { + roomId?: string; + }; width?: number; height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + className?: string; onClick?(): void; } @@ -128,14 +134,21 @@ export default class RoomAvatar extends React.Component { }; public render() { - const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; + const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; const roomName = room ? room.name : oobData.name; + // If the room is a DM, we use the other user's ID for the color hash + // in order to match the room avatar with their avatar + const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId; return ( - diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 3127e1a915..ec662d831b 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -105,7 +105,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
- { extraSettings &&
+ { extraSettings && value &&
{ extraSettings.map(key => ( )) } diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx index 428e18ed30..a61cdeedd3 100644 --- a/src/components/views/context_menus/CallContextMenu.tsx +++ b/src/components/views/context_menus/CallContextMenu.tsx @@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component { onTransferClick = () => { Modal.createTrackedDialog( 'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call }, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + /*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true, ); this.props.onFinished(); }; @@ -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/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 28a73ba8d4..aead3a266e 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import React from 'react'; -import { _t } from '../../../languageHandler'; +import AccessibleButton from "../elements/AccessibleButton"; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import Field from "../elements/Field"; -import Dialpad from '../voip/DialPad'; +import DialPad from '../voip/DialPad'; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps extends IContextMenuProps { @@ -45,24 +45,31 @@ export default class DialpadContextMenu extends React.Component this.setState({ value: this.state.value + digit }); }; + onCancelClick = () => { + this.props.onFinished(); + }; + onChange = (ev) => { this.setState({ value: ev.target.value }); }; render() { return -
+
- {_t("Dial pad")} + +
+
+ +
+
+
- -
-
-
-
; } 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.js b/src/components/views/context_menus/MessageContextMenu.tsx similarity index 71% rename from src/components/views/context_menus/MessageContextMenu.js rename to src/components/views/context_menus/MessageContextMenu.tsx index a2086451cd..8f5d3baa17 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 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. @@ -16,12 +16,11 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; +import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import Resend from '../../../Resend'; @@ -29,53 +28,69 @@ import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; -import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; import { Action } from "../../../dispatcher/actions"; +import ReportEventDialog from '../dialogs/ReportEventDialog'; +import ViewSource from '../../structures/ViewSource'; +import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog'; +import ErrorDialog from '../dialogs/ErrorDialog'; +import ShareDialog from '../dialogs/ShareDialog'; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -export function canCancel(eventStatus) { +export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } +export interface IEventTileOps { + isWidgetHidden(): boolean; + unhideWidget(): void; +} + +export interface IOperableEventTile { + getEventTileOps(): IEventTileOps; +} + +interface IProps { + /* the MatrixEvent associated with the context menu */ + mxEvent: MatrixEvent; + /* an optional EventTileOps implementation that can be used to unhide preview widgets */ + eventTileOps?: IEventTileOps; + permalinkCreator?: RoomPermalinkCreator; + /* an optional function to be called when the user clicks collapse thread, if not provided hide button */ + collapseReplyThread?(): void; + /* callback called when the menu is dismissed */ + onFinished(): void; + /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */ + onCloseDialog?(): void; +} + +interface IState { + canRedact: boolean; + canPin: boolean; +} + @replaceableComponent("views.context_menus.MessageContextMenu") -export default class MessageContextMenu extends React.Component { - static propTypes = { - /* the MatrixEvent associated with the context menu */ - mxEvent: PropTypes.object.isRequired, - - /* an optional EventTileOps implementation that can be used to unhide preview widgets */ - eventTileOps: PropTypes.object, - - /* an optional function to be called when the user clicks collapse thread, if not provided hide button */ - collapseReplyThread: PropTypes.func, - - /* callback called when the menu is dismissed */ - onFinished: PropTypes.func, - - /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */ - onCloseDialog: PropTypes.func, - }; - +export default class MessageContextMenu extends React.Component { state = { canRedact: false, canPin: false, }; componentDidMount() { - MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); - this._checkPermissions(); + MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions); + this.checkPermissions(); } componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener('RoomMember.powerLevel', this._checkPermissions); + cli.removeListener('RoomMember.powerLevel', this.checkPermissions); } } - _checkPermissions = () => { + private checkPermissions = (): void => { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); @@ -93,7 +108,7 @@ export default class MessageContextMenu extends React.Component { this.setState({ canRedact, canPin }); }; - _isPinned() { + private isPinned(): boolean { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); if (!pinnedEvent) return false; @@ -101,38 +116,35 @@ export default class MessageContextMenu extends React.Component { return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } - onResendReactionsClick = () => { - for (const reaction of this._getUnsentReactions()) { + private onResendReactionsClick = (): void => { + for (const reaction of this.getUnsentReactions()) { Resend.resend(reaction); } this.closeMenu(); }; - onReportEventClick = () => { - const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); + private onReportEventClick = (): void => { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_reportEvent'); this.closeMenu(); }; - onViewSourceClick = () => { - const ViewSource = sdk.getComponent('structures.ViewSource'); + private onViewSourceClick = (): void => { Modal.createTrackedDialog('View Event Source', '', ViewSource, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; - onRedactClick = () => { - const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog"); + private onRedactClick = (): void => { Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { - onFinished: async (proceed, reason) => { + onFinished: async (proceed: boolean, reason?: string) => { if (!proceed) return; const cli = MatrixClientPeg.get(); try { - if (this.props.onCloseDialog) this.props.onCloseDialog(); + this.props.onCloseDialog?.(); await cli.redactEvent( this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(), @@ -145,7 +157,6 @@ export default class MessageContextMenu extends React.Component { // (e.g. no errcode or statusCode) as in that case the redactions end up in the // detached queue and we show the room status bar to allow retry if (typeof code !== "undefined") { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // display error message stating you couldn't delete this. Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { title: _t('Error'), @@ -158,7 +169,7 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onForwardClick = () => { + private onForwardClick = (): void => { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { matrixClient: MatrixClientPeg.get(), event: this.props.mxEvent, @@ -167,12 +178,12 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onPinClick = () => { + private onPinClick = (): void => { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); const eventId = this.props.mxEvent.getId(); - const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; + const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || []; if (pinnedIds.includes(eventId)) { pinnedIds.splice(pinnedIds.indexOf(eventId), 1); } else { @@ -188,18 +199,16 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - closeMenu = () => { - if (this.props.onFinished) this.props.onFinished(); + private closeMenu = (): void => { + this.props.onFinished(); }; - onUnhidePreviewClick = () => { - if (this.props.eventTileOps) { - this.props.eventTileOps.unhideWidget(); - } + private onUnhidePreviewClick = (): void => { + this.props.eventTileOps?.unhideWidget(); this.closeMenu(); }; - onQuoteClick = () => { + private onQuoteClick = (): void => { dis.dispatch({ action: Action.ComposerInsert, event: this.props.mxEvent, @@ -207,9 +216,8 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onPermalinkClick = (e) => { + private onPermalinkClick = (e: React.MouseEvent): void => { e.preventDefault(); - const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { target: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, @@ -217,30 +225,27 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onCollapseReplyThreadClick = () => { + private onCollapseReplyThreadClick = (): void => { this.props.collapseReplyThread(); this.closeMenu(); }; - _getReactions(filter) { + private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); const eventId = this.props.mxEvent.getId(); return room.getPendingEvents().filter(e => { const relation = e.getRelation(); - return relation && - relation.rel_type === "m.annotation" && - relation.event_id === eventId && - filter(e); + return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e); }); } - _getPendingReactions() { - return this._getReactions(e => canCancel(e.status)); + private getPendingReactions(): MatrixEvent[] { + return this.getReactions(e => canCancel(e.status)); } - _getUnsentReactions() { - return this._getReactions(e => e.status === EventStatus.NOT_SENT); + private getUnsentReactions(): MatrixEvent[] { + return this.getReactions(e => e.status === EventStatus.NOT_SENT); } render() { @@ -248,16 +253,17 @@ export default class MessageContextMenu extends React.Component { const me = cli.getUserId(); const mxEvent = this.props.mxEvent; const eventStatus = mxEvent.status; - const unsentReactionsCount = this._getUnsentReactions().length; - let resendReactionsButton; - let redactButton; - let forwardButton; - let pinButton; - let unhidePreviewButton; - let externalURLButton; - let quoteButton; - let collapseReplyThread; - let redactItemList; + const unsentReactionsCount = this.getUnsentReactions().length; + + let resendReactionsButton: JSX.Element; + let redactButton: JSX.Element; + let forwardButton: JSX.Element; + let pinButton: JSX.Element; + let unhidePreviewButton: JSX.Element; + let externalURLButton: JSX.Element; + let quoteButton: JSX.Element; + let collapseReplyThread: JSX.Element; + let redactItemList: JSX.Element; // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; @@ -266,7 +272,7 @@ export default class MessageContextMenu extends React.Component { resendReactionsButton = ( ); @@ -296,7 +302,7 @@ export default class MessageContextMenu extends React.Component { pinButton = ( ); @@ -327,16 +333,20 @@ export default class MessageContextMenu extends React.Component { if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } - // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID) const permalinkButton = ( ); @@ -351,18 +361,23 @@ export default class MessageContextMenu extends React.Component { } // Bridges can provide a 'external_url' to link back to the source. - if (typeof (mxEvent.event.content.external_url) === "string" && - isUrlPermitted(mxEvent.event.content.external_url) + if (typeof (mxEvent.getContent().external_url) === "string" && + isUrlPermitted(mxEvent.getContent().external_url) ) { externalURLButton = ( ); } @@ -377,7 +392,7 @@ export default class MessageContextMenu extends React.Component { ); } - let reportEventButton; + let reportEventButton: JSX.Element; if (mxEvent.getSender() !== me) { reportEventButton = ( - {_t("Clear status")} + { _t("Clear status") } ; } else { actionButton = - {_t("Update status")} + { _t("Update status") } ; } } else { - actionButton = - {_t("Set status")} + { _t("Set status") } ; } @@ -121,17 +123,24 @@ export default class StatusMessageContextMenu extends React.Component { spinner = ; } - const form = -
- {actionButton} - {spinner} + { actionButton } + { spinner }
; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index b21efdceb9..26d7b640a4 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC = ({ onFinished(); }; streamAudioStreamButton = ; } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 5024b98def..3ef86e438d 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { sleep } from "matrix-js-sdk/src/utils"; import { _t } from '../../../languageHandler'; @@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; interface IProps extends IDialogProps { - matrixClient: MatrixClient; space: Room; - onCreateRoomClick(cli: MatrixClient, space: Room): void; + onCreateRoomClick(space: Room): void; } const Entry = ({ room, checked, onChange }) => { @@ -211,17 +209,23 @@ export const AddExistingToSpace: React.FC = ({ function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } return
= ({
; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished }) => { const [selectedSpace, setSelectedSpace] = useState(space); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); @@ -344,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onFinished={onFinished} fixedWidth={false} > - +
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(cli, space)} kind="link"> + onCreateRoomClick(space)} kind="link"> { _t("Create a new room") } } diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.tsx similarity index 77% rename from src/components/views/dialogs/AddressPickerDialog.js rename to src/components/views/dialogs/AddressPickerDialog.tsx index 09714e24e3..6b239ee570 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.tsx @@ -18,14 +18,12 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { sleep } from "matrix-js-sdk/src/utils"; import { _t, _td } from '../../../languageHandler'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import { addressTypes, getAddressType } from '../../../UserAddress'; +import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; @@ -34,6 +32,10 @@ import { abbreviateUrl } from '../../../utils/UrlUtils'; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AddressSelector from '../elements/AddressSelector'; +import AddressTile from '../elements/AddressTile'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -44,29 +46,64 @@ const addressTypeName = { 'email': _td("email address"), }; -@replaceableComponent("views.dialogs.AddressPickerDialog") -export default class AddressPickerDialog extends React.Component { - static propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.node, - // Extra node inserted after picker input, dropdown and errors - extraNode: PropTypes.node, - value: PropTypes.string, - placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - roomId: PropTypes.string, - button: PropTypes.string, - focus: PropTypes.bool, - validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)), - onFinished: PropTypes.func.isRequired, - groupId: PropTypes.string, - // The type of entity to search for. Default: 'user'. - pickerType: PropTypes.oneOf(['user', 'room']), - // Whether the current user should be included in the addresses returned. Only - // applicable when pickerType is `user`. Default: false. - includeSelf: PropTypes.bool, - }; +interface IResult { + user_id: string; // eslint-disable-line camelcase + room_id?: string; // eslint-disable-line camelcase + name?: string; + display_name?: string; // eslint-disable-line camelcase + avatar_url?: string;// eslint-disable-line camelcase +} - static defaultProps = { +interface IProps { + title: string; + description?: JSX.Element; + // Extra node inserted after picker input, dropdown and errors + extraNode?: JSX.Element; + value?: string; + placeholder?: ((validAddressTypes: any) => string) | string; + roomId?: string; + button?: string; + focus?: boolean; + validAddressTypes?: AddressType[]; + onFinished: (success: boolean, list?: IUserAddress[]) => void; + groupId?: string; + // The type of entity to search for. Default: 'user'. + pickerType?: 'user' | 'room'; + // Whether the current user should be included in the addresses returned. Only + // applicable when pickerType is `user`. Default: false. + includeSelf?: boolean; +} + +interface IState { + // Whether to show an error message because of an invalid address + invalidAddressError: boolean; + // List of UserAddressType objects representing + // the list of addresses we're going to invite + selectedList: IUserAddress[]; + // Whether a search is ongoing + busy: boolean; + // An error message generated during the user directory search + searchError: string; + // Whether the server supports the user_directory API + serverSupportsUserDirectory: boolean; + // The query being searched for + query: string; + // List of UserAddressType objects representing the set of + // auto-completion results for the current search query. + suggestedList: IUserAddress[]; + // List of address types initialised from props, but may change while the + // dialog is open and represents the supported list of address types at this time. + validAddressTypes: AddressType[]; +} + +@replaceableComponent("views.dialogs.AddressPickerDialog") +export default class AddressPickerDialog extends React.Component { + private textinput = createRef(); + private addressSelector = createRef(); + private queryChangedDebouncer: number; + private cancelThreepidLookup: () => void; + + static defaultProps: Partial = { value: "", focus: true, validAddressTypes: addressTypes, @@ -74,36 +111,23 @@ export default class AddressPickerDialog extends React.Component { includeSelf: false, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._textinput = createRef(); - let validAddressTypes = this.props.validAddressTypes; // Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user - if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) { - validAddressTypes = validAddressTypes.filter(type => type !== "email"); + if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) { + validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email); } this.state = { - // Whether to show an error message because of an invalid address invalidAddressError: false, - // List of UserAddressType objects representing - // the list of addresses we're going to invite selectedList: [], - // Whether a search is ongoing busy: false, - // An error message generated during the user directory search searchError: null, - // Whether the server supports the user_directory API serverSupportsUserDirectory: true, - // The query being searched for query: "", - // List of UserAddressType objects representing the set of - // auto-completion results for the current search query. suggestedList: [], - // List of address types initialised from props, but may change while the - // dialog is open and represents the supported list of address types at this time. validAddressTypes, }; } @@ -111,11 +135,11 @@ export default class AddressPickerDialog extends React.Component { componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input - this._textinput.current.value = this.props.value; + this.textinput.current.value = this.props.value; } } - getPlaceholder() { + private getPlaceholder(): string { const { placeholder } = this.props; if (typeof placeholder === "string") { return placeholder; @@ -124,23 +148,23 @@ export default class AddressPickerDialog extends React.Component { return placeholder(this.state.validAddressTypes); } - onButtonClick = () => { + private onButtonClick = (): void => { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local selectedList - if (this._textinput.current.value !== '') { - selectedList = this._addAddressesToList([this._textinput.current.value]); + if (this.textinput.current.value !== '') { + selectedList = this.addAddressesToList([this.textinput.current.value]); if (selectedList === null) return; } this.props.onFinished(true, selectedList); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - onKeyDown = e => { - const textInput = this._textinput.current ? this._textinput.current.value : undefined; + private onKeyDown = (e: React.KeyboardEvent): void => { + const textInput = this.textinput.current ? this.textinput.current.value : undefined; if (e.key === Key.ESCAPE) { e.stopPropagation(); @@ -149,15 +173,15 @@ export default class AddressPickerDialog extends React.Component { } else if (e.key === Key.ARROW_UP) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionUp(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp(); } else if (e.key === Key.ARROW_DOWN) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionDown(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown(); } else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.chooseSelection(); + if (this.addressSelector.current) this.addressSelector.current.chooseSelection(); } else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) { e.stopPropagation(); e.preventDefault(); @@ -169,17 +193,17 @@ export default class AddressPickerDialog extends React.Component { // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } } else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) { e.stopPropagation(); e.preventDefault(); - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } }; - onQueryChanged = ev => { - const query = ev.target.value; + private onQueryChanged = (ev: React.ChangeEvent): void => { + const query = (ev.target as HTMLTextAreaElement).value; if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); } @@ -188,17 +212,17 @@ export default class AddressPickerDialog extends React.Component { this.queryChangedDebouncer = setTimeout(() => { if (this.props.pickerType === 'user') { if (this.props.groupId) { - this._doNaiveGroupSearch(query); + this.doNaiveGroupSearch(query); } else if (this.state.serverSupportsUserDirectory) { - this._doUserDirectorySearch(query); + this.doUserDirectorySearch(query); } else { - this._doLocalSearch(query); + this.doLocalSearch(query); } } else if (this.props.pickerType === 'room') { if (this.props.groupId) { - this._doNaiveGroupRoomSearch(query); + this.doNaiveGroupRoomSearch(query); } else { - this._doRoomSearch(query); + this.doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -213,7 +237,7 @@ export default class AddressPickerDialog extends React.Component { } }; - onDismissed = index => () => { + private onDismissed = (index: number) => () => { const selectedList = this.state.selectedList.slice(); selectedList.splice(index, 1); this.setState({ @@ -221,25 +245,21 @@ export default class AddressPickerDialog extends React.Component { suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - onClick = index => () => { - this.onSelected(index); - }; - - onSelected = index => { + private onSelected = (index: number): void => { const selectedList = this.state.selectedList.slice(); - selectedList.push(this._getFilteredSuggestions()[index]); + selectedList.push(this.getFilteredSuggestions()[index]); this.setState({ selectedList, suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - _doNaiveGroupSearch(query) { + private doNaiveGroupSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); this.setState({ busy: true, @@ -260,7 +280,7 @@ export default class AddressPickerDialog extends React.Component { display_name: u.displayname, }); }); - this._processResults(results, query); + this.processResults(results, query); }).catch((err) => { console.error('Error whilst searching group rooms: ', err); this.setState({ @@ -273,7 +293,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doNaiveGroupRoomSearch(query) { + private doNaiveGroupRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const results = []; GroupStore.getGroupRooms(this.props.groupId).forEach((r) => { @@ -289,13 +309,13 @@ export default class AddressPickerDialog extends React.Component { name: r.name || r.canonical_alias, }); }); - this._processResults(results, query); + this.processResults(results, query); this.setState({ busy: false, }); } - _doRoomSearch(query) { + private doRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const rooms = MatrixClientPeg.get().getRooms(); const results = []; @@ -346,13 +366,13 @@ export default class AddressPickerDialog extends React.Component { return a.rank - b.rank; }); - this._processResults(sortedResults, query); + this.processResults(sortedResults, query); this.setState({ busy: false, }); } - _doUserDirectorySearch(query) { + private doUserDirectorySearch(query: string): void { this.setState({ busy: true, query, @@ -366,7 +386,7 @@ export default class AddressPickerDialog extends React.Component { if (this.state.query !== query) { return; } - this._processResults(resp.results, query); + this.processResults(resp.results, query); }).catch((err) => { console.error('Error whilst searching user directory: ', err); this.setState({ @@ -377,7 +397,7 @@ export default class AddressPickerDialog extends React.Component { serverSupportsUserDirectory: false, }); // Do a local search immediately - this._doLocalSearch(query); + this.doLocalSearch(query); } }).then(() => { this.setState({ @@ -386,7 +406,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doLocalSearch(query) { + private doLocalSearch(query: string): void { this.setState({ query, searchError: null, @@ -407,10 +427,10 @@ export default class AddressPickerDialog extends React.Component { avatar_url: user.avatarUrl, }); }); - this._processResults(results, query); + this.processResults(results, query); } - _processResults(results, query) { + private processResults(results: IResult[], query: string): void { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -465,27 +485,27 @@ export default class AddressPickerDialog extends React.Component { address: query, isKnown: false, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); if (addrType === 'email') { - this._lookupThreepid(addrType, query); + this.lookupThreepid(addrType, query); } } this.setState({ suggestedList, invalidAddressError: false, }, () => { - if (this.addressSelector) this.addressSelector.moveSelectionTop(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop(); }); } - _addAddressesToList(addressTexts) { + private addAddressesToList(addressTexts: string[]): IUserAddress[] { const selectedList = this.state.selectedList.slice(); let hasError = false; addressTexts.forEach((addressText) => { addressText = addressText.trim(); const addrType = getAddressType(addressText); - const addrObj = { + const addrObj: IUserAddress = { addressType: addrType, address: addressText, isKnown: false, @@ -504,7 +524,6 @@ export default class AddressPickerDialog extends React.Component { const room = MatrixClientPeg.get().getRoom(addrObj.address); if (room) { addrObj.displayName = room.name; - addrObj.avatarMxc = room.avatarUrl; addrObj.isKnown = true; } } @@ -518,17 +537,17 @@ export default class AddressPickerDialog extends React.Component { query: "", invalidAddressError: hasError ? true : this.state.invalidAddressError, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); return hasError ? null : selectedList; } - async _lookupThreepid(medium, address) { + private async lookupThreepid(medium: AddressType, address: string): Promise { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just // leave it: it's replacing the old one each time so it's // not like they leak. - this._cancelThreepidLookup = function() { + this.cancelThreepidLookup = function() { cancelled = true; }; @@ -570,7 +589,7 @@ export default class AddressPickerDialog extends React.Component { } } - _getFilteredSuggestions() { + private getFilteredSuggestions(): IUserAddress[] { // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({ address, addressType }) => { @@ -584,15 +603,15 @@ export default class AddressPickerDialog extends React.Component { }); } - _onPaste = e => { + private onPaste = (e: React.ClipboardEvent): void => { // Prevent the text being pasted into the textarea e.preventDefault(); const text = e.clipboardData.getData("text"); // Process it as a list of addresses to add instead - this._addAddressesToList(text.split(/[\s,]+/)); + this.addAddressesToList(text.split(/[\s,]+/)); }; - onUseDefaultIdentityServerClick = e => { + private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -601,33 +620,27 @@ export default class AddressPickerDialog extends React.Component { // Add email as a valid address type. const { validAddressTypes } = this.state; - validAddressTypes.push('email'); + validAddressTypes.push(AddressType.Email); this.setState({ validAddressTypes }); }; - onManageSettingsClick = e => { + private onManageSettingsClick = (e: React.MouseEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AddressSelector = sdk.getComponent("elements.AddressSelector"); - this.scrollElement = null; - let inputLabel; if (this.props.description) { inputLabel =
- +
; } const query = []; // create the invite list if (this.state.selectedList.length > 0) { - const AddressTile = sdk.getComponent("elements.AddressTile"); for (let i = 0; i < this.state.selectedList.length; i++) { query.push( - , + autoFocus={this.props.focus} + />, ); - const filteredSuggestedList = this._getFilteredSuggestions(); + const filteredSuggestedList = this.getFilteredSuggestions(); let error; let addressSelector; @@ -675,7 +688,7 @@ export default class AddressPickerDialog extends React.Component { error =
{ _t("No results") }
; } else { addressSelector = ( - {this.addressSelector = ref;}} + {_t( + identityServer =
{ _t( "Use an identity server to invite by email. " + "Use the default (%(defaultIdentityServerName)s) " + "or manage in Settings.", @@ -698,25 +711,29 @@ export default class AddressPickerDialog extends React.Component { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, { - default: sub => {sub}, - settings: sub => {sub}, + default: sub => { sub }, + settings: sub => { sub }, }, - )}
; + ) }
; } else { - identityServer =
{_t( + identityServer =
{ _t( "Use an identity server to invite by email. " + "Manage in Settings.", {}, { - settings: sub => {sub}, + settings: sub => { sub }, }, - )}
; + ) }
; } } return ( - - {inputLabel} + + { inputLabel }
{ query }
{ error } diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx index 970883aca2..3ae82f1026 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; interface IProps { unknownProfileUsers: Array<{ @@ -50,10 +50,8 @@ export default class AskInviteAnywayDialog extends React.Component { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const errorList = this.props.unknownProfileUsers - .map(address =>
  • {address.userId}: {address.errorText}
  • ); + .map(address =>
  • { address.userId }: { address.errorText }
  • ); return ( { contentId='mx_Dialog_content' >
    - {/* eslint-disable-next-line */} -

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

    +

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

      { errorList }
    diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e92bd6315e..42b21ec743 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -118,9 +118,7 @@ export default class BaseDialog extends React.Component { let headerImage; if (this.props.headerImage) { - headerImage = ; + headerImage = ; } return ( @@ -149,7 +147,7 @@ export default class BaseDialog extends React.Component { 'mx_Dialog_headerWithCancel': !!cancelButton, })}>
    - {headerImage} + { headerImage } { this.props.title }
    { this.props.headerButton } diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 5a2f16f169..34218a3399 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -69,15 +69,18 @@ const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => {
    { _t(info.feedbackSubheading) }   - { _t("Your platform and username will be noted to help us use your feedback as much as we can.")} + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } - { - onFinished(false); - defaultDispatcher.dispatch({ + { + onFinished(false); + defaultDispatcher.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Labs, - }); - }}> + }); + }} + > { _t("To leave the beta, visit your settings.") }
    diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index eeb3769bf9..3df05dac6e 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -18,13 +18,17 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake'; import AccessibleButton from "../elements/AccessibleButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import QuestionDialog from "./QuestionDialog"; +import BaseDialog from "./BaseDialog"; +import Field from '../elements/Field'; +import Spinner from "../elements/Spinner"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { onFinished: (success: boolean) => void; @@ -93,7 +97,6 @@ export default class BugReportDialog extends React.Component { }).then(() => { if (!this.unmounted) { this.props.onFinished(false); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // N.B. first param is passed to piwik and so doesn't want i18n Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, { title: _t('Logs sent'), @@ -160,15 +163,10 @@ export default class BugReportDialog extends React.Component { }; public render() { - const Loader = sdk.getComponent("elements.Spinner"); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('elements.Field'); - let error = null; if (this.state.err) { error =
    - {this.state.err} + { this.state.err }
    ; } @@ -176,8 +174,8 @@ export default class BugReportDialog extends React.Component { if (this.state.busy) { progress = (
    - - {this.state.progress} ... + + { this.state.progress } ...
    ); } @@ -190,7 +188,9 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -223,7 +223,7 @@ export default class BugReportDialog extends React.Component { { _t("Download logs") } - {this.state.downloadProgress && {this.state.downloadProgress} ...} + { this.state.downloadProgress && { this.state.downloadProgress } ... }
    { "please include those things here.", )} /> - {progress} - {error} + { progress } + { error }
    */ import React from 'react'; -import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; +import QuestionDialog from "./QuestionDialog"; +import Spinner from "../elements/Spinner"; interface IProps { newVersion: string; @@ -58,16 +59,13 @@ export default class ChangelogDialog extends React.Component { return (
  • - {commit.commit.message.split('\n')[0]} + { commit.commit.message.split('\n')[0] }
  • ); } public render() { - const Spinner = sdk.getComponent('views.elements.Spinner'); - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - const logs = REPOS.map(repo => { let content; if (this.state[repo] == null) { @@ -81,15 +79,15 @@ export default class ChangelogDialog extends React.Component { } return (
    -

    {repo}

    -
      {content}
    +

    { repo }

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

    {_t("Unavailable")}

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

    { _t("Unavailable") }

    : logs }
    ); diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index 7627489deb..6a8773ce45 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -156,8 +156,8 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< height={avatarSize} />
    - {person.user.name} - {person.userId} + { person.user.name } + { person.userId }
    this.setPersonToggle(person, e.target.checked)} />
    @@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< emailAddresses.push(( this.onAddressChange(e, emailAddresses.length)} label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} @@ -205,18 +205,21 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< people.push(( {_t("Show more")} + > + { _t("Show more") } + )); } } if (this.state.people.length > 0) { peopleIntro = (
    - {_t("People you know on %(brand)s", { brand: SdkConfig.get().brand })} + { _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) } - {this.state.showPeople ? _t("Hide") : _t("Show")} + { this.state.showPeople ? _t("Hide") : _t("Show") }
    ); @@ -236,14 +239,17 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< >
    - {emailAddresses} - {peopleIntro} - {people} + { emailAddresses } + { peopleIntro } + { people } {buttonText} + > + { buttonText } +
    diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 90b749b959..d21fde329c 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -15,9 +15,12 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ConfirmRedactDialog from './ConfirmRedactDialog'; +import ErrorDialog from './ErrorDialog'; +import BaseDialog from "./BaseDialog"; +import Spinner from "../elements/Spinner"; interface IProps { redact: () => Promise; @@ -73,7 +76,6 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent ); } else { - const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); - const Spinner = sdk.getComponent('elements.Spinner'); return ( ; } } diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 94f29a71fc..b346d2d44c 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import TextInputDialog from "./TextInputDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -29,7 +29,6 @@ interface IProps { @replaceableComponent("views.dialogs.ConfirmRedactDialog") export default class ConfirmRedactDialog extends React.Component { render() { - const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); return ( { "Note that if you delete a room name or topic change, it could undo the change.")} placeholder={_t("Reason (optional)")} focus - button={_t("Remove")}> - + button={_t("Remove")} + /> ); } } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 78fae390b5..7099556ac6 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -17,11 +17,14 @@ limitations under the License. import React from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import MemberAvatar from '../avatars/MemberAvatar'; +import BaseAvatar from '../avatars/BaseAvatar'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' @@ -67,11 +70,6 @@ export default class ConfirmUserActionDialog extends React.Component { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const confirmButtonClass = this.props.danger ? 'danger' : ''; let reasonBox; @@ -106,7 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component { } return ( - diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 2978179817..2577d5456d 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -16,8 +16,9 @@ limitations under the License. import React from 'react'; import { _t } from "../../../languageHandler"; -import * as sdk from "../../../index"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { onFinished: (success: boolean) => void; @@ -34,9 +35,6 @@ export default class ConfirmWipeDeviceDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( { >

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

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

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

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

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

    ; + publicPrivateLabel =

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

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

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

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

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

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

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

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

    {federateLabel}

    +

    { federateLabel }

    diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 2cdaf9cf4f..d03b668cd9 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -16,21 +16,22 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import QuestionDialog from "./QuestionDialog"; interface IProps { onFinished: (success: boolean) => void; } -export default (props: IProps) => { +const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, { title: _t("Sign out"), description: _t( @@ -58,8 +59,6 @@ export default (props: IProps) => { { brand }, ); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( { hasCancel={false} onPrimaryButtonClick={props.onFinished} > - ); }; + +export default CryptoStoreTooNewDialog; diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index d30f90d111..bbed866464 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. import React from 'react'; import { AuthType, IAuthData } from 'matrix-js-sdk/src/interactive-auth'; -import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as Lifecycle from '../../../Lifecycle'; @@ -27,6 +26,7 @@ import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/Interact import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import StyledCheckbox from "../elements/StyledCheckbox"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -169,8 +169,6 @@ export default class DeactivateAccountDialog extends React.Component @@ -178,11 +176,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 de958f8e9a..8ae9d0654f 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react'; -import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; @@ -42,6 +41,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SettingLevel } from '../../../settings/SettingLevel'; +import BaseDialog from "./BaseDialog"; +import TruncatedList from "../elements/TruncatedList"; interface IGenericEditorProps { onBack: () => void; @@ -181,14 +182,23 @@ export class SendCustomEvent extends GenericEditor - +
    { !this.state.message && } { showTglFlip &&
    - - +
    { !this.state.message && } { !this.state.message &&
    - - + key={this.props.children[0] ? this.props.children[0].key : ''} + /> ; + }} + />; } return
    @@ -494,7 +525,7 @@ class RoomStateExplorer extends React.PureComponent - {eventType} + { eventType } ; }) } @@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent; + }} + forceMode={true} + />; } return
    @@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent
    -
    Transaction
    -
    {txnId}
    +
    { txnId }
    Phase
    -
    {PHASE_MAP[request.phase] || request.phase}
    +
    { PHASE_MAP[request.phase] || request.phase }
    Timeout
    -
    {Math.floor(timeout / 1000)}
    +
    { Math.floor(timeout / 1000) }
    Methods
    -
    {request.methods && request.methods.join(", ")}
    +
    { request.methods && request.methods.join(", ") }
    requestingUserId
    -
    {request.requestingUserId}
    +
    { request.requestingUserId }
    observeOnly
    -
    {JSON.stringify(request.observeOnly)}
    +
    { JSON.stringify(request.observeOnly) }
    ); }; @@ -771,12 +806,12 @@ class VerificationExplorer extends React.PureComponent { return (
    - {Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => + { Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => , - )} + ) }
    - +
    ); } @@ -844,9 +879,9 @@ class WidgetExplorer extends React.Component ev.getId() === editWidget.eventId); if (!stateEv) { // "should never happen" return
    - {_t("There was an error finding this widget.")} + { _t("There was an error finding this widget.") }
    - +
    ; } @@ -865,17 +900,17 @@ class WidgetExplorer extends React.Component
    - {widgets.map(w => { + { widgets.map(w => { return ; - })} + >{ w.url }; + }) }
    - +
    ); } @@ -1007,7 +1042,7 @@ class SettingsExplorer extends React.PureComponent{canEdit.toString()}; + return
    ; } render() { @@ -1021,46 +1056,53 @@ class SettingsExplorer extends React.PureComponent
    {_t( + { _t( customVariables[row[0]].expl, customVariables[row[0]].getTextVariables ? customVariables[row[0]].getTextVariables() : null, - )}{ row[1] }
    { canEdit.toString() }
    - - - + + + - {allSettings.map(i => ( + { allSettings.map(i => ( - ))} + )) }
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}{ _t("Setting ID") }{ _t("Value") }{ _t("Value in this room") }
    this.onViewClick(e, i)}> - {i} + { i } - this.onEditClick(e, i)} + this.onEditClick(e, i)} className='mx_DevTools_SettingsExplorer_edit' > ✏ - {this.renderSettingValue(SettingsStore.getValue(i))} + { this.renderSettingValue(SettingsStore.getValue(i)) } - {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + { this.renderSettingValue(SettingsStore.getValue(i, room.roomId)) }
    - +
    ); @@ -1068,62 +1110,70 @@ class SettingsExplorer extends React.PureComponent
    -

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

    +

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

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

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

    +

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

    - {_t("Setting definition:")} -
    {JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}
    + { _t("Setting definition:") } +
    { JSON.stringify(SETTINGS[this.state.viewSetting], null, 4) }
    - {_t("Value:")}  - {this.renderSettingValue( + { _t("Value:") }  + { this.renderSettingValue( SettingsStore.getValue(this.state.viewSetting), - )} + ) }
    - {_t("Value in this room:")}  - {this.renderSettingValue( + { _t("Value in this room:") }  + { this.renderSettingValue( SettingsStore.getValue(this.state.viewSetting, room.roomId), - )} + ) }
    - {_t("Values at explicit levels:")} -
    {this.renderExplicitSettingValues(
    +                            { _t("Values at explicit levels:") }
    +                            
    { this.renderExplicitSettingValues(
                                     this.state.viewSetting, null,
    -                            )}
    + ) }
    - {_t("Values at explicit levels in this room:")} -
    {this.renderExplicitSettingValues(
    +                            { _t("Values at explicit levels in this room:") }
    +                            
    { this.renderExplicitSettingValues(
                                     this.state.viewSetting, room.roomId,
    -                            )}
    + ) }
    @@ -1171,7 +1221,7 @@ class SettingsExplorer extends React.PureComponent this.onEditClick(e, this.state.viewSetting)}>{ _t("Edit Values") } - + ); @@ -1232,12 +1282,12 @@ export default class DevtoolsDialog extends React.PureComponent if (this.state.mode) { body = - {(cli) => + { (cli) =>
    { this.state.mode.getLabel() }
    Room ID: { this.props.roomId }
    - } + } ; } else { const classes = "mx_DevTools_RoomStateExplorer_button"; @@ -1261,7 +1311,6 @@ export default class DevtoolsDialog extends React.PureComponent ; } - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { body } diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 217e4f2d37..a0e6046d71 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -144,23 +144,25 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent
    {preview} + >{ preview }
    - {_t("Add image (optional)")} + { _t("Add image (optional)") } - {_t("An image will help people identify your community.")} + { _t("An image will help people identify your community.") }
    - {_t("Save")} + { _t("Save") }
    diff --git a/src/components/views/dialogs/ErrorDialog.tsx b/src/components/views/dialogs/ErrorDialog.tsx index 0f675f0df7..56cd76237f 100644 --- a/src/components/views/dialogs/ErrorDialog.tsx +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -26,9 +26,9 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -57,7 +57,6 @@ export default class ErrorDialog extends React.Component { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { countlyFeedbackSection =
    -

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

    +

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

    -

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

    -

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

    +

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

    +

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

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

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

    +

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

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

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

    {_t("Report a bug")}

    +

    { _t("Report a bug") }

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

    - {bugReports} + { bugReports }
    { countlyFeedbackSection } } diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ba06436ae2..77e2b6ae0c 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -43,6 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; +import SpaceStore from "../../../stores/SpaceStore"; const AVATAR_SIZE = 30; @@ -105,12 +106,12 @@ const Entry: React.FC = ({ room, event, matrixClient: cli, onFinish className = "mx_ForwardList_sending"; disabled = true; title = _t("Sending"); - icon =
    ; + icon =
    ; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("Sent"); - icon =
    ; + icon =
    ; } else { className = "mx_ForwardList_sendFailed"; disabled = true; @@ -180,7 +181,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const spacesEnabled = useFeatureEnabled("feature_spaces"); + const spacesEnabled = SpaceStore.spacesEnabled; const flairEnabled = useFeatureEnabled(UIFeature.Flair); const previewLayout = useSettingValue("layout"); @@ -203,10 +204,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx index 64c080bf01..4b8b7f32f0 100644 --- a/src/components/views/dialogs/HostSignupDialog.tsx +++ b/src/components/views/dialogs/HostSignupDialog.tsx @@ -177,32 +177,32 @@ export default class HostSignupDialog extends React.PureComponent

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

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

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