diff --git a/.eslintrc.js b/.eslintrc.js index 069a67e511..fc82e75ce2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,3 @@ -const path = require('path'); - -// get the path of the js-sdk so we can extend the config -// eslint supports loading extended configs by module, -// but only if they come from a module that starts with eslint-config- -// So we load the filename directly (and it could be in node_modules/ -// or or ../node_modules/ etc) -// -// We add a `..` to the end because the js-sdk lives out of lib/, but the eslint -// config is at the project root. -const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); - module.exports = { extends: ["matrix-org", "matrix-org/react-legacy"], parser: "babel-eslint", @@ -31,7 +19,7 @@ module.exports = { }, overrides: [{ - files: ["src/**/*.{ts, tsx}"], + "files": ["src/**/*.{ts, tsx}"], "extends": ["matrix-org/ts"], "rules": { // We disable this while we're transitioning @@ -41,6 +29,6 @@ module.exports = { "quotes": "off", "no-extra-boolean-cast": "off", - } + }, }], }; diff --git a/CHANGELOG.md b/CHANGELOG.md index e08b2ad612..d944d58f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,125 @@ +Changes in [3.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.0.0) (2020-07-27) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.1...v3.0.0) + +BREAKING CHANGES +--- + + * The room list components have been replaced as part of this release, so the list, tiles, and other associated components now use a different prop / state contract. + + +All Changes +--- + + * Upgrade to JS SDK 8.0.0 + * Update from Weblate + [\#5053](https://github.com/matrix-org/matrix-react-sdk/pull/5053) + * RoomList listen to notificationState updates for bolding + [\#5051](https://github.com/matrix-org/matrix-react-sdk/pull/5051) + * Ensure notification badges stop listening when they unmount + [\#5049](https://github.com/matrix-org/matrix-react-sdk/pull/5049) + * Improve RoomTile performance + [\#5048](https://github.com/matrix-org/matrix-react-sdk/pull/5048) + * Reward users for using stable ordering in their room list + [\#5047](https://github.com/matrix-org/matrix-react-sdk/pull/5047) + * Fix autocomplete suggesting a different thing mid-composition + [\#5030](https://github.com/matrix-org/matrix-react-sdk/pull/5030) + * Put low priority xor toggle back in the room list context menu + [\#5026](https://github.com/matrix-org/matrix-react-sdk/pull/5026) + * Fix autocompletion of Community IDs + [\#5040](https://github.com/matrix-org/matrix-react-sdk/pull/5040) + * Use OpenType tabular numbers in timestamps + [\#5042](https://github.com/matrix-org/matrix-react-sdk/pull/5042) + * Update packages to modern versions + [\#5046](https://github.com/matrix-org/matrix-react-sdk/pull/5046) + * Add dismiss button to rebrand toast + [\#5044](https://github.com/matrix-org/matrix-react-sdk/pull/5044) + * Fix Firefox composer regression exception + [\#5039](https://github.com/matrix-org/matrix-react-sdk/pull/5039) + * Fix BaseAvatar wrongly using Buttons when it needs not + [\#5037](https://github.com/matrix-org/matrix-react-sdk/pull/5037) + * Performance improvements round 2: Maps, freezing, dispatching, and flexbox + obliteration + [\#5038](https://github.com/matrix-org/matrix-react-sdk/pull/5038) + * Mixed bag of performance improvements: ScrollPanel and notifications + [\#5034](https://github.com/matrix-org/matrix-react-sdk/pull/5034) + * Update message previews + [\#5025](https://github.com/matrix-org/matrix-react-sdk/pull/5025) + * Translate create room buttons + [\#5035](https://github.com/matrix-org/matrix-react-sdk/pull/5035) + * Escape single quotes in composer placeholder + [\#5033](https://github.com/matrix-org/matrix-react-sdk/pull/5033) + * Don't hammer on the layout engine with avatar updates for the background + [\#5032](https://github.com/matrix-org/matrix-react-sdk/pull/5032) + * Ensure incremental updates to the ImportanceAlgorithm trigger A-Z order + [\#5031](https://github.com/matrix-org/matrix-react-sdk/pull/5031) + * don't syntax highlight languages that begin with "_" + [\#5029](https://github.com/matrix-org/matrix-react-sdk/pull/5029) + * Convert Modal to TypeScript + [\#4956](https://github.com/matrix-org/matrix-react-sdk/pull/4956) + * Use new eslint dependency and remove tslint + [\#4815](https://github.com/matrix-org/matrix-react-sdk/pull/4815) + * Support custom tags in the room list again + [\#5024](https://github.com/matrix-org/matrix-react-sdk/pull/5024) + * Fix the tag panel context menu + [\#5028](https://github.com/matrix-org/matrix-react-sdk/pull/5028) + * Tag Watcher don't create new filter if not needed, confuses references + [\#5021](https://github.com/matrix-org/matrix-react-sdk/pull/5021) + * Convert editor to TypeScript + [\#4978](https://github.com/matrix-org/matrix-react-sdk/pull/4978) + * Query Matcher use unhomoglyph for a little bit more leniency + [\#4977](https://github.com/matrix-org/matrix-react-sdk/pull/4977) + * Fix Breadcrumbs2 ending up with 2 tabIndexes on Firefox + [\#5017](https://github.com/matrix-org/matrix-react-sdk/pull/5017) + * Add min-width to floating Jitsi + [\#5023](https://github.com/matrix-org/matrix-react-sdk/pull/5023) + * Update crypto event icon to match rest of app styling + [\#5020](https://github.com/matrix-org/matrix-react-sdk/pull/5020) + * Fix Reactions Row Button vertical misalignment due to forced height + [\#5019](https://github.com/matrix-org/matrix-react-sdk/pull/5019) + * Use mouseleave instead of mouseout for hover events. Fix tooltip flicker + [\#5016](https://github.com/matrix-org/matrix-react-sdk/pull/5016) + * Fix slash commands null guard + [\#5015](https://github.com/matrix-org/matrix-react-sdk/pull/5015) + * Fix field tooltips + [\#5014](https://github.com/matrix-org/matrix-react-sdk/pull/5014) + * Fix community right panel button regression + [\#5022](https://github.com/matrix-org/matrix-react-sdk/pull/5022) + * [BREAKING] Remove the old room list + [\#5013](https://github.com/matrix-org/matrix-react-sdk/pull/5013) + * ellipse senders for images and videos + [\#4990](https://github.com/matrix-org/matrix-react-sdk/pull/4990) + * Sprinkle and consolidate some tooltips + [\#5012](https://github.com/matrix-org/matrix-react-sdk/pull/5012) + * Hopefully make cancel dialog a bit less weird + [\#4833](https://github.com/matrix-org/matrix-react-sdk/pull/4833) + * Fix emoji filterString + [\#5011](https://github.com/matrix-org/matrix-react-sdk/pull/5011) + * Fix size call for devtools state events + [\#5008](https://github.com/matrix-org/matrix-react-sdk/pull/5008) + * Fix `this` context in _setupHomeserverManagers for IntegrationManagers + [\#5010](https://github.com/matrix-org/matrix-react-sdk/pull/5010) + * Sync recently used reactions list across sessions + [\#4993](https://github.com/matrix-org/matrix-react-sdk/pull/4993) + * Null guard no e2ee for UserInfo + [\#5009](https://github.com/matrix-org/matrix-react-sdk/pull/5009) + * stop Inter from clobbering Twemoji + [\#5007](https://github.com/matrix-org/matrix-react-sdk/pull/5007) + * use a proper HTML sanitizer to strip , rather than a regexp + [\#5006](https://github.com/matrix-org/matrix-react-sdk/pull/5006) + * Convert room list log setting to a real setting + [\#5005](https://github.com/matrix-org/matrix-react-sdk/pull/5005) + * Bump lodash from 4.17.15 to 4.17.19 in /test/end-to-end-tests + [\#5003](https://github.com/matrix-org/matrix-react-sdk/pull/5003) + * Bump lodash from 4.17.15 to 4.17.19 + [\#5004](https://github.com/matrix-org/matrix-react-sdk/pull/5004) + * Convert devtools dialog to use new room state format + [\#4936](https://github.com/matrix-org/matrix-react-sdk/pull/4936) + * Update checkbox + [\#5000](https://github.com/matrix-org/matrix-react-sdk/pull/5000) + * Increase width for country code dropdown + [\#5001](https://github.com/matrix-org/matrix-react-sdk/pull/5001) + Changes in [2.10.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.10.1) (2020-07-16) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.0...v2.10.1) diff --git a/docs/local-echo-dev.md b/docs/local-echo-dev.md new file mode 100644 index 0000000000..e4725a9b07 --- /dev/null +++ b/docs/local-echo-dev.md @@ -0,0 +1,39 @@ +# Local echo (developer docs) + +The React SDK provides some local echo functionality to allow for components to do something +quickly and fall back when it fails. This is all available in the `local-echo` directory within +`stores`. + +Echo is handled in EchoChambers, with `GenericEchoChamber` being the base implementation for all +chambers. The `EchoChamber` class is provided as semantic access to a `GenericEchoChamber` +implementation, such as the `RoomEchoChamber` (which handles echoable details of a room). + +Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation. +The echo chamber will also need to deal with external changes, and has full control over whether +or not something has successfully been echoed. + +An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext` +gets provided to a `RoomEchoChamber` for example) with details about their intended area of +effect, as well as manage `EchoTransaction`s. An `EchoTransaction` is simply a unit of work that +needs to be locally echoed. + +The `EchoStore` manages echo chamber instances, builds contexts, and is generally less semantically +accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things +tidy, this is an intentional design decision. + +**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and +`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal +with listeners being torn down. Once the reference count of the Whenable causes garbage collection, +the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface +usage. + +## Audit functionality + +The UI supports a "Server isn't responding" dialog which includes a partial audit log-like +structure to it. This is partially the reason for added complexity of `EchoTransaction`s +and `EchoContext`s - this information feeds the UI states which then provide direct retry +mechanisms. + +The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left) +is set up, where the dialog then drives through the contexts and transactions. + diff --git a/package.json b/package.json index d136129180..61a9a21815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.10.1", + "version": "3.0.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -105,6 +105,7 @@ "devDependencies": { "@babel/cli": "^7.10.5", "@babel/core": "^7.10.5", + "@babel/parser": "^7.11.0", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-proposal-export-default-from": "^7.10.4", @@ -117,6 +118,7 @@ "@babel/preset-react": "^7.10.4", "@babel/preset-typescript": "^7.10.4", "@babel/register": "^7.10.5", + "@babel/traverse": "^7.11.0", "@peculiar/webcrypto": "^1.1.2", "@types/classnames": "^2.2.10", "@types/counterpart": "^0.18.1", @@ -140,16 +142,12 @@ "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "eslint": "7.5.0", - "eslint-config-google": "^0.14.0", "eslint-config-matrix-org": "^0.1.2", "eslint-plugin-babel": "^5.3.1", "eslint-plugin-flowtype": "^2.50.3", - "eslint-plugin-jest": "^23.18.0", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^2.5.1", - "estree-walker": "^0.9.0", "file-loader": "^3.0.1", - "flow-parser": "0.57.3", "glob": "^5.0.15", "jest": "^24.9.0", "jest-canvas-mock": "^2.2.0", diff --git a/res/css/_components.scss b/res/css/_components.scss index 23e4af780a..fcc87e2061 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -15,6 +15,7 @@ @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; +@import "./structures/_NonUrgentToastContainer.scss"; @import "./structures/_NotificationPanel.scss"; @import "./structures/_RightPanel.scss"; @import "./structures/_RoomDirectory.scss"; @@ -75,6 +76,7 @@ @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; +@import "./views/dialogs/_ServerOfflineDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; @@ -215,6 +217,7 @@ @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; +@import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 135a51c7cd..3feb2565be 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -54,5 +54,5 @@ limitations under the License. position: absolute; left: -9px; border-radius: 0 3px 3px 0; - top: 12px; // just feels right (see comment above about designs needing to be updated) + top: 5px; // just feels right (see comment above about designs needing to be updated) } diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 2472bcd780..21b30d804a 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -115,3 +115,7 @@ limitations under the License. .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/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 25e1153fce..aee7b5a154 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -21,8 +21,20 @@ limitations under the License. height: 100%; } -// move hit area 5px to the right so it doesn't overlap with the timeline scrollbar -.mx_MainSplit > .mx_ResizeHandle.mx_ResizeHandle_horizontal { - margin: 0 -10px 0 0; - padding: 0 10px 0 0; +.mx_MainSplit > .mx_RightPanel_ResizeWrapper { + padding: 5px; + + &:hover .mx_RightPanel_ResizeHandle { + // Need to use important to override element style attributes + // set by re-resizable + top: 50% !important; + transform: translate(0, -50%); + + height: 64px !important; // to match width of the ones on roomlist + width: 4px !important; + border-radius: 4px !important; + + background-color: $primary-fg-color; + opacity: 0.8; + } } diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 88b29a96e8..af6f6c79e9 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -78,3 +78,24 @@ limitations under the License. */ height: 100%; } + +.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal, +.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover { + position: relative; + + &::before { + position: absolute; + left: 6px; + top: 50%; + transform: translate(0, -50%); + + height: 64px; // to match width of the ones on roomlist + width: 4px; + border-radius: 4px; + + content: ' '; + + background-color: $primary-fg-color; + opacity: 0.8; + } +} diff --git a/res/css/structures/_NonUrgentToastContainer.scss b/res/css/structures/_NonUrgentToastContainer.scss new file mode 100644 index 0000000000..826a812406 --- /dev/null +++ b/res/css/structures/_NonUrgentToastContainer.scss @@ -0,0 +1,35 @@ +/* +Copyright 2020 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_NonUrgentToastContainer { + position: absolute; + bottom: 30px; + left: 28px; + z-index: 101; // same level as other toasts + + .mx_NonUrgentToastContainer_toast { + padding: 10px 12px; + border-radius: 8px; + width: 320px; + font-size: $font-13px; + margin-top: 8px; + + // We don't use variables on the colours because we want it to be the same + // in all themes. + background-color: #17191c; + color: #fff; + } +} diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 561ab1446f..715a94fe2c 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -99,3 +99,7 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile_content { margin-right: 0px; } + +.mx_NotificationPanel_empty::before { + mask-image: url('$(res)/img/element-icons/notifications.svg'); +} diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 0515b03118..c7c0d6fac4 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -19,13 +19,12 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 264px; - max-width: 50%; display: flex; flex-direction: column; border-radius: 8px; - margin: 5px; padding: 4px 0; + box-sizing: border-box; + height: 100%; .mx_RoomView_MessageList { padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above @@ -145,3 +144,28 @@ limitations under the License. order: 2; margin: auto; } + +.mx_RightPanel_empty { + margin-right: -42px; + + h2 { + font-weight: 700; + margin: 16px 0; + } + + h2, p { + font-size: $font-14px; + } + + &::before { + content: ''; + display: block; + margin: 11px auto 29px auto; + height: 42px; + width: 42px; + background-color: $rightpanel-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } +} diff --git a/res/css/views/dialogs/_RebrandDialog.scss b/res/css/views/dialogs/_RebrandDialog.scss index 6c916e0f1d..534584ae2a 100644 --- a/res/css/views/dialogs/_RebrandDialog.scss +++ b/res/css/views/dialogs/_RebrandDialog.scss @@ -53,11 +53,12 @@ limitations under the License. .mx_RebrandDialog_chevron::after { content: ''; display: inline-block; - width: 24px; - height: 24px; + width: 30px; + height: 30px; mask-position: center; mask-size: contain; mask-repeat: no-repeat; background-color: $muted-fg-color; - mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + transform: rotate(-90deg); } diff --git a/res/css/views/dialogs/_ServerOfflineDialog.scss b/res/css/views/dialogs/_ServerOfflineDialog.scss new file mode 100644 index 0000000000..ae4b70beb3 --- /dev/null +++ b/res/css/views/dialogs/_ServerOfflineDialog.scss @@ -0,0 +1,72 @@ +/* +Copyright 2020 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_ServerOfflineDialog { + .mx_ServerOfflineDialog_content { + padding-right: 85px; + color: $primary-fg-color; + + hr { + border-color: $primary-fg-color; + opacity: 0.1; + border-bottom: none; + } + + ul { + padding: 16px; + + li:nth-child(n + 2) { + margin-top: 16px; + } + } + + .mx_ServerOfflineDialog_content_context { + .mx_ServerOfflineDialog_content_context_timestamp { + display: inline-block; + width: 115px; + color: $muted-fg-color; + line-height: 24px; // same as avatar + vertical-align: top; + } + + .mx_ServerOfflineDialog_content_context_timeline { + display: inline-block; + width: calc(100% - 155px); // 115px timestamp width + 40px right margin + + .mx_ServerOfflineDialog_content_context_timeline_header { + span { + margin-left: 8px; + vertical-align: middle; + } + } + + .mx_ServerOfflineDialog_content_context_txn { + position: relative; + margin-top: 8px; + + .mx_ServerOfflineDialog_content_context_txn_desc { + width: calc(100% - 100px); // 100px is an arbitrary margin for the button + } + + .mx_AccessibleButton { + float: right; + padding: 0; + } + } + } + } + } +} diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index bd5c67c7ed..ae0927386a 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -145,13 +145,14 @@ limitations under the License. &::after { content: ""; position: absolute; - width: 24px; - height: 24px; - right: -28px; // - (24 + 4) + width: 26px; + height: 26px; + right: -27.5px; // - (width: 26 + spacing to align with X above: 1.5) + top: -3px; mask-repeat: no-repeat; mask-position: center; mask-size: contain; - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg'); background-color: $primary-fg-color; } diff --git a/res/css/views/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss index 5544799a34..5189f80b30 100644 --- a/res/css/views/elements/_ResizeHandle.scss +++ b/res/css/views/elements/_ResizeHandle.scss @@ -34,7 +34,7 @@ limitations under the License. .mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal { margin: 0 -10px 0 0; - padding: 0 10px 0 0; + padding: 0 8px 0 0; } .mx_ResizeHandle > div { diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss index cb3803fe5b..6cb3b6bce9 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -67,8 +67,8 @@ limitations under the License. bottom: 0; left: 0; right: 0; - mask: url('$(res)/img/icon-jump-to-bottom.svg'); + mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg'); mask-repeat: no-repeat; - mask-position: 9px 14px; + mask-size: contain; background: $muted-fg-color; } diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 8708f13ada..0b1da7a41c 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -58,11 +58,6 @@ limitations under the License. } } -.mx_RoomPreviewBar_dark { - background-color: $tagpanel-bg-color; - color: $accent-fg-color; -} - .mx_RoomPreviewBar_actions { display: flex; } diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index b907d06d36..d3c9b79c69 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -142,26 +142,24 @@ limitations under the License. .mx_RoomSublist_collapseBtn { display: inline-block; position: relative; - width: 12px; - height: 12px; - margin-right: 8px; + width: 14px; + height: 14px; + margin-right: 6px; &::before { content: ''; - width: 12px; - height: 12px; + width: 18px; + height: 18px; position: absolute; - top: 1px; - left: 1px; mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background-color: $primary-fg-color; + background-color: $roomlist-header-color; mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } &.mx_RoomSublist_collapseBtn_collapsed::before { - mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); + transform: rotate(-90deg); } } } @@ -251,22 +249,24 @@ limitations under the License. .mx_RoomSublist_showNButtonChevron { position: relative; - width: 16px; - height: 16px; + width: 18px; + height: 18px; margin-left: 12px; - margin-right: 18px; + margin-right: 16px; mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $roomtile-preview-color; + background: $roomlist-header-color; + left: -1px; // adjust for image position } - .mx_RoomSublist_showMoreButtonChevron { + .mx_RoomSublist_showMoreButtonChevron, + .mx_RoomSublist_showLessButtonChevron { mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } .mx_RoomSublist_showLessButtonChevron { - mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); + transform: rotate(180deg); } } diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index c0545660c2..8841b042a0 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -28,7 +28,7 @@ limitations under the License. content: ""; position: absolute; top: -8px; - left: 11px; + left: 10.5px; width: 4px; height: 4px; border-radius: 16px; @@ -49,12 +49,13 @@ limitations under the License. .mx_TopUnreadMessagesBar_scrollUp::before { content: ""; position: absolute; - width: 38px; - height: 38px; - mask-image: url('$(res)/img/icon-jump-to-first-unread.svg'); + width: 36px; + height: 36px; + mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg'); mask-repeat: no-repeat; - mask-position: 9px 13px; + mask-size: contain; background: $muted-fg-color; + transform: rotate(180deg); } .mx_TopUnreadMessagesBar_markAsRead { diff --git a/res/css/views/toasts/_NonUrgentEchoFailureToast.scss b/res/css/views/toasts/_NonUrgentEchoFailureToast.scss new file mode 100644 index 0000000000..9a8229b38e --- /dev/null +++ b/res/css/views/toasts/_NonUrgentEchoFailureToast.scss @@ -0,0 +1,37 @@ +/* +Copyright 2020 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_NonUrgentEchoFailureToast { + .mx_NonUrgentEchoFailureToast_icon { + display: inline-block; + width: $font-18px; + height: $font-18px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: #fff; // we know that non-urgent toasts are always styled the same + mask-image: url('$(res)/img/element-icons/cloud-off.svg'); + margin-right: 8px; + } + + span { // includes the i18n block + vertical-align: middle; + } + + .mx_AccessibleButton { + padding: 0; + } +} diff --git a/res/img/attach.png b/res/img/attach.png deleted file mode 100644 index 1bcb70045d..0000000000 Binary files a/res/img/attach.png and /dev/null differ diff --git a/res/img/button-text-block-quote-on.svg b/res/img/button-text-block-quote-on.svg deleted file mode 100644 index f8a86125c9..0000000000 --- a/res/img/button-text-block-quote-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 3B24B8C7-64BE-4B3E-A748-94DB72E1210F - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-block-quote.svg b/res/img/button-text-block-quote.svg deleted file mode 100644 index d70c261f5d..0000000000 --- a/res/img/button-text-block-quote.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - BFC0418B-9081-4789-A231-B75953157748 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bold-on.svg b/res/img/button-text-bold-on.svg deleted file mode 100644 index 161e740e90..0000000000 --- a/res/img/button-text-bold-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 01F3F9B2-8F38-4BAF-A345-AECAC3D88E79 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bold.svg b/res/img/button-text-bold.svg deleted file mode 100644 index 0fd0baa07e..0000000000 --- a/res/img/button-text-bold.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 9BC64A5B-F157-43FF-BCC4-02D30CDF520B - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bulleted-list-on.svg b/res/img/button-text-bulleted-list-on.svg deleted file mode 100644 index d4a40e889c..0000000000 --- a/res/img/button-text-bulleted-list-on.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - 654917CF-20A4-49B6-B0A1-9875D7B733C8 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bulleted-list.svg b/res/img/button-text-bulleted-list.svg deleted file mode 100644 index ae3e640d8e..0000000000 --- a/res/img/button-text-bulleted-list.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - B7D94619-44BC-4184-A60A-DBC5BB54E5F9 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-deleted-on.svg b/res/img/button-text-deleted-on.svg deleted file mode 100644 index 2914fcabe6..0000000000 --- a/res/img/button-text-deleted-on.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - 69B11088-0F3A-4E14-BD9F-4FEF4115E99B - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-deleted.svg b/res/img/button-text-deleted.svg deleted file mode 100644 index 5f262dc350..0000000000 --- a/res/img/button-text-deleted.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - A34F2223-34C6-46AE-AA47-38EC8984E9B3 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-formatting.svg b/res/img/button-text-formatting.svg deleted file mode 100644 index d697010d40..0000000000 --- a/res/img/button-text-formatting.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/res/img/button-text-inline-code-on.svg b/res/img/button-text-inline-code-on.svg deleted file mode 100644 index 8d1439c97b..0000000000 --- a/res/img/button-text-inline-code-on.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - B76754AB-42E6-48D2-9443-80CBC0DE02ED - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-inline-code.svg b/res/img/button-text-inline-code.svg deleted file mode 100644 index 24026cb709..0000000000 --- a/res/img/button-text-inline-code.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - 4CAFF494-61AE-4916-AFE8-D1E62F7CF0DE - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-italic-on.svg b/res/img/button-text-italic-on.svg deleted file mode 100644 index 15fe588596..0000000000 --- a/res/img/button-text-italic-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 116426C2-0B55-480E-92B3-57D4B3ABAB90 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-italic.svg b/res/img/button-text-italic.svg deleted file mode 100644 index b5722e827b..0000000000 --- a/res/img/button-text-italic.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 9FBC844D-96CF-4DCB-B545-FCD23727218B - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-numbered-list-on.svg b/res/img/button-text-numbered-list-on.svg deleted file mode 100644 index 869a2c2cc2..0000000000 --- a/res/img/button-text-numbered-list-on.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - 294F929B-31AA-4D0C-98B3-9CA96764060D - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-numbered-list.svg b/res/img/button-text-numbered-list.svg deleted file mode 100644 index 8e5b8b87b6..0000000000 --- a/res/img/button-text-numbered-list.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - F0F58459-A13A-48C5-9332-ABFB96726F05 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-underlined-on.svg b/res/img/button-text-underlined-on.svg deleted file mode 100644 index 870be3ce6a..0000000000 --- a/res/img/button-text-underlined-on.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - FD84FF7C-43E4-4312-90AB-5A59AD018377 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-underlined.svg b/res/img/button-text-underlined.svg deleted file mode 100644 index 26f448539c..0000000000 --- a/res/img/button-text-underlined.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - 13E7EE68-9B16-4A3D-8F9F-31E4BAB7E438 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/call.png b/res/img/call.png deleted file mode 100644 index a7805e0596..0000000000 Binary files a/res/img/call.png and /dev/null differ diff --git a/res/img/cancel-black.png b/res/img/cancel-black.png deleted file mode 100644 index 87dcfd41a8..0000000000 Binary files a/res/img/cancel-black.png and /dev/null differ diff --git a/res/img/cancel-black2.png b/res/img/cancel-black2.png deleted file mode 100644 index a928c61b09..0000000000 Binary files a/res/img/cancel-black2.png and /dev/null differ diff --git a/res/img/cancel.png b/res/img/cancel.png deleted file mode 100644 index 2bda8ff5bf..0000000000 Binary files a/res/img/cancel.png and /dev/null differ diff --git a/res/img/chevron-left.png b/res/img/chevron-left.png deleted file mode 100644 index efb0065de9..0000000000 Binary files a/res/img/chevron-left.png and /dev/null differ diff --git a/res/img/chevron-right.png b/res/img/chevron-right.png deleted file mode 100644 index 18a4684e47..0000000000 Binary files a/res/img/chevron-right.png and /dev/null differ diff --git a/res/img/chevron.png b/res/img/chevron.png deleted file mode 100644 index 81236f91bc..0000000000 Binary files a/res/img/chevron.png and /dev/null differ diff --git a/res/img/close-white.png b/res/img/close-white.png deleted file mode 100644 index d8752ed9fe..0000000000 Binary files a/res/img/close-white.png and /dev/null differ diff --git a/res/img/create-big.png b/res/img/create-big.png deleted file mode 100644 index b7307a11c7..0000000000 Binary files a/res/img/create-big.png and /dev/null differ diff --git a/res/img/create-big.svg b/res/img/create-big.svg deleted file mode 100644 index 2450542b63..0000000000 --- a/res/img/create-big.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - icons_create_room - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/create.png b/res/img/create.png deleted file mode 100644 index 2d6107ac50..0000000000 Binary files a/res/img/create.png and /dev/null differ diff --git a/res/img/delete.png b/res/img/delete.png deleted file mode 100644 index 8ff20a116d..0000000000 Binary files a/res/img/delete.png and /dev/null differ diff --git a/res/img/directory-big.png b/res/img/directory-big.png deleted file mode 100644 index 03cab69c4a..0000000000 Binary files a/res/img/directory-big.png and /dev/null differ diff --git a/res/img/download.png b/res/img/download.png deleted file mode 100644 index 1999ebf7ab..0000000000 Binary files a/res/img/download.png and /dev/null differ diff --git a/res/img/e2e/blacklisted.svg b/res/img/e2e/blacklisted.svg deleted file mode 100644 index ac99d23f05..0000000000 --- a/res/img/e2e/blacklisted.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/e2e/lock-verified.svg b/res/img/e2e/lock-verified.svg deleted file mode 100644 index 819dfacc49..0000000000 --- a/res/img/e2e/lock-verified.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg deleted file mode 100644 index de2bded7f8..0000000000 --- a/res/img/e2e/lock-warning.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/res/img/edit.png b/res/img/edit.png deleted file mode 100644 index 6f373d3f3d..0000000000 Binary files a/res/img/edit.png and /dev/null differ diff --git a/res/img/edit.svg b/res/img/edit.svg deleted file mode 100644 index 9674b31690..0000000000 --- a/res/img/edit.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/img/element-icons/cloud-off.svg b/res/img/element-icons/cloud-off.svg new file mode 100644 index 0000000000..7faea7d3b5 --- /dev/null +++ b/res/img/element-icons/cloud-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/composer/send.svg b/res/img/element-icons/room/composer/send.svg deleted file mode 100644 index b255a9b23b..0000000000 --- a/res/img/element-icons/room/composer/send.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/clear-input.svg b/res/img/element-icons/roomlist/clear-input.svg deleted file mode 100644 index 29fc097600..0000000000 --- a/res/img/element-icons/roomlist/clear-input.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/direct-chat.svg b/res/img/element-icons/roomlist/direct-chat.svg deleted file mode 100644 index 4b92dd9521..0000000000 --- a/res/img/element-icons/roomlist/direct-chat.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/element-icons/roomlist/e2ee-default.svg b/res/img/element-icons/roomlist/e2ee-default.svg deleted file mode 100644 index 76525f48b2..0000000000 --- a/res/img/element-icons/roomlist/e2ee-default.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/e2ee-error.svg b/res/img/element-icons/roomlist/e2ee-error.svg deleted file mode 100644 index 7f1a761dde..0000000000 --- a/res/img/element-icons/roomlist/e2ee-error.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/roomlist/e2ee-none.svg b/res/img/element-icons/roomlist/e2ee-none.svg deleted file mode 100644 index cfafe6d09d..0000000000 --- a/res/img/element-icons/roomlist/e2ee-none.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/roomlist/e2ee-trusted.svg b/res/img/element-icons/roomlist/e2ee-trusted.svg deleted file mode 100644 index 577d6a31e1..0000000000 --- a/res/img/element-icons/roomlist/e2ee-trusted.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/roomlist/explore-rooms.svg b/res/img/element-icons/roomlist/explore-rooms.svg deleted file mode 100644 index 3786ce1153..0000000000 --- a/res/img/element-icons/roomlist/explore-rooms.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/roomlist/search.svg b/res/img/element-icons/roomlist/search.svg deleted file mode 100644 index 3786ce1153..0000000000 --- a/res/img/element-icons/roomlist/search.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/explore.svg b/res/img/explore.svg deleted file mode 100644 index 3956e912ac..0000000000 --- a/res/img/explore.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/archive.svg b/res/img/feather-customised/archive.svg deleted file mode 100644 index 428882c87b..0000000000 --- a/res/img/feather-customised/archive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/arrow-down.svg b/res/img/feather-customised/arrow-down.svg deleted file mode 100644 index 4f84f627bd..0000000000 --- a/res/img/feather-customised/arrow-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/bell-crossed.svg b/res/img/feather-customised/bell-crossed.svg deleted file mode 100644 index 3ca24662b9..0000000000 --- a/res/img/feather-customised/bell-crossed.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/bell-mentions.custom.svg b/res/img/feather-customised/bell-mentions.custom.svg deleted file mode 100644 index fcc02f337f..0000000000 --- a/res/img/feather-customised/bell-mentions.custom.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/bell-notification.custom.svg b/res/img/feather-customised/bell-notification.custom.svg deleted file mode 100644 index 7bfd551f97..0000000000 --- a/res/img/feather-customised/bell-notification.custom.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/bell.svg b/res/img/feather-customised/bell.svg deleted file mode 100644 index b6bc5ec502..0000000000 --- a/res/img/feather-customised/bell.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/brush.svg b/res/img/feather-customised/brush.svg deleted file mode 100644 index d7f2738629..0000000000 --- a/res/img/feather-customised/brush.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/chevron-down-thin.svg b/res/img/feather-customised/chevron-down-thin.svg new file mode 100644 index 0000000000..109c83def6 --- /dev/null +++ b/res/img/feather-customised/chevron-down-thin.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg index bcb185ede7..a091913b42 100644 --- a/res/img/feather-customised/chevron-down.svg +++ b/res/img/feather-customised/chevron-down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/feather-customised/chevron-right.svg b/res/img/feather-customised/chevron-right.svg deleted file mode 100644 index 258de414a1..0000000000 --- a/res/img/feather-customised/chevron-right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/chevron-up.svg b/res/img/feather-customised/chevron-up.svg deleted file mode 100644 index 4eb5ecc33e..0000000000 --- a/res/img/feather-customised/chevron-up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/emoji3.custom.svg b/res/img/feather-customised/emoji3.custom.svg deleted file mode 100644 index d91ba1c132..0000000000 --- a/res/img/feather-customised/emoji3.custom.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/face.svg b/res/img/feather-customised/face.svg deleted file mode 100644 index a8ca856b67..0000000000 --- a/res/img/feather-customised/face.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/favourites.svg b/res/img/feather-customised/favourites.svg deleted file mode 100644 index 80f08f6e55..0000000000 --- a/res/img/feather-customised/favourites.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/flag.svg b/res/img/feather-customised/flag.svg deleted file mode 100644 index 983c02762b..0000000000 --- a/res/img/feather-customised/flag.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/flair.svg b/res/img/feather-customised/flair.svg deleted file mode 100644 index ce3a5ed6ad..0000000000 --- a/res/img/feather-customised/flair.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/grid.svg b/res/img/feather-customised/grid.svg deleted file mode 100644 index 4f7ab30d97..0000000000 --- a/res/img/feather-customised/grid.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/img/feather-customised/lock-solid.svg b/res/img/feather-customised/lock-solid.svg deleted file mode 100644 index 9eb8b6a4c5..0000000000 --- a/res/img/feather-customised/lock-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/lock.svg b/res/img/feather-customised/lock.svg deleted file mode 100644 index 1330903b30..0000000000 --- a/res/img/feather-customised/lock.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/more-horizontal.svg b/res/img/feather-customised/more-horizontal.svg deleted file mode 100644 index dc6a85564e..0000000000 --- a/res/img/feather-customised/more-horizontal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/notifications.svg b/res/img/feather-customised/notifications.svg deleted file mode 100644 index a590031ac3..0000000000 --- a/res/img/feather-customised/notifications.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/paperclip.svg b/res/img/feather-customised/paperclip.svg deleted file mode 100644 index 74a90e0fa3..0000000000 --- a/res/img/feather-customised/paperclip.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/phone.svg b/res/img/feather-customised/phone.svg deleted file mode 100644 index 85661c5320..0000000000 --- a/res/img/feather-customised/phone.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/search.svg b/res/img/feather-customised/search.svg deleted file mode 100644 index 9ce0724ea7..0000000000 --- a/res/img/feather-customised/search.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/feather-customised/share.svg b/res/img/feather-customised/share.svg deleted file mode 100644 index 7098af58aa..0000000000 --- a/res/img/feather-customised/share.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/sliders.svg b/res/img/feather-customised/sliders.svg deleted file mode 100644 index 5b5ec8656c..0000000000 --- a/res/img/feather-customised/sliders.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/star.svg b/res/img/feather-customised/star.svg deleted file mode 100644 index bcdc31aa47..0000000000 --- a/res/img/feather-customised/star.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/sticker.custom.svg b/res/img/feather-customised/sticker.custom.svg deleted file mode 100644 index 691e3b3925..0000000000 --- a/res/img/feather-customised/sticker.custom.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/sun.svg b/res/img/feather-customised/sun.svg deleted file mode 100644 index 7f51b94d1c..0000000000 --- a/res/img/feather-customised/sun.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/feather-customised/upload.svg b/res/img/feather-customised/upload.svg deleted file mode 100644 index 30c89d3819..0000000000 --- a/res/img/feather-customised/upload.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/user-add.svg b/res/img/feather-customised/user-add.svg deleted file mode 100644 index 6b5210c1d6..0000000000 --- a/res/img/feather-customised/user-add.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/img/feather-customised/users-sm.svg b/res/img/feather-customised/users-sm.svg deleted file mode 100644 index 6098be38c3..0000000000 --- a/res/img/feather-customised/users-sm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/feather-customised/users.svg b/res/img/feather-customised/users.svg deleted file mode 100644 index b90aafdd4a..0000000000 --- a/res/img/feather-customised/users.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/video.svg b/res/img/feather-customised/video.svg deleted file mode 100644 index da77b6c57a..0000000000 --- a/res/img/feather-customised/video.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/filegrid.png b/res/img/filegrid.png deleted file mode 100644 index c2c2799f37..0000000000 Binary files a/res/img/filegrid.png and /dev/null differ diff --git a/res/img/filelist.png b/res/img/filelist.png deleted file mode 100644 index 3cf6cb494e..0000000000 Binary files a/res/img/filelist.png and /dev/null differ diff --git a/res/img/fullscreen.svg b/res/img/fullscreen.svg deleted file mode 100644 index e333abb6fb..0000000000 --- a/res/img/fullscreen.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - Zoom - Created with Sketch. - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/hide.png b/res/img/hide.png deleted file mode 100644 index c5aaf0dd0d..0000000000 Binary files a/res/img/hide.png and /dev/null differ diff --git a/res/img/icon-jump-to-bottom.svg b/res/img/icon-jump-to-bottom.svg deleted file mode 100644 index c4210b4ebe..0000000000 --- a/res/img/icon-jump-to-bottom.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/res/img/icon-jump-to-first-unread.svg b/res/img/icon-jump-to-first-unread.svg deleted file mode 100644 index 652ccec20d..0000000000 --- a/res/img/icon-jump-to-first-unread.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/res/img/icon-text-cancel.svg b/res/img/icon-text-cancel.svg deleted file mode 100644 index ce28d128aa..0000000000 --- a/res/img/icon-text-cancel.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - 28D80248-63BA-4A5F-9216-4CFE72784BAC - Created with sketchtool. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/icons-pin.svg b/res/img/icons-pin.svg deleted file mode 100644 index a6fbf13baa..0000000000 --- a/res/img/icons-pin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/res/img/icons-room-nobg.svg b/res/img/icons-room-nobg.svg deleted file mode 100644 index 8ca7ab272b..0000000000 --- a/res/img/icons-room-nobg.svg +++ /dev/null @@ -1,28 +0,0 @@ - -image/svg+xml - - - - - - - \ No newline at end of file diff --git a/res/img/icons-share.svg b/res/img/icons-share.svg deleted file mode 100644 index aac19080f4..0000000000 --- a/res/img/icons-share.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/info.png b/res/img/info.png deleted file mode 100644 index 699fd64e01..0000000000 Binary files a/res/img/info.png and /dev/null differ diff --git a/res/img/leave.svg b/res/img/leave.svg deleted file mode 100644 index 1acbe59313..0000000000 --- a/res/img/leave.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/res/img/list-close.png b/res/img/list-close.png deleted file mode 100644 index 82b322f9d4..0000000000 Binary files a/res/img/list-close.png and /dev/null differ diff --git a/res/img/list-open.png b/res/img/list-open.png deleted file mode 100644 index f8c8063197..0000000000 Binary files a/res/img/list-open.png and /dev/null differ diff --git a/res/img/matrix-m.svg b/res/img/matrix-m.svg deleted file mode 100644 index ccb1df0fc5..0000000000 --- a/res/img/matrix-m.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/res/img/menu.png b/res/img/menu.png deleted file mode 100755 index b45f88950f..0000000000 Binary files a/res/img/menu.png and /dev/null differ diff --git a/res/img/network-matrix.svg b/res/img/network-matrix.svg deleted file mode 100644 index bb8278ae39..0000000000 --- a/res/img/network-matrix.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/res/img/newmessages.png b/res/img/newmessages.png deleted file mode 100644 index a22156ab21..0000000000 Binary files a/res/img/newmessages.png and /dev/null differ diff --git a/res/img/placeholder.png b/res/img/placeholder.png deleted file mode 100644 index 7da32f259c..0000000000 Binary files a/res/img/placeholder.png and /dev/null differ diff --git a/res/img/react.svg b/res/img/react.svg deleted file mode 100644 index dd23c41c2c..0000000000 --- a/res/img/react.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/reply.svg b/res/img/reply.svg deleted file mode 100644 index 540e228883..0000000000 --- a/res/img/reply.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/img/search.png b/res/img/search.png deleted file mode 100644 index 2f98d29048..0000000000 Binary files a/res/img/search.png and /dev/null differ diff --git a/res/img/selected.png b/res/img/selected.png deleted file mode 100644 index 8931cba75f..0000000000 Binary files a/res/img/selected.png and /dev/null differ diff --git a/res/img/settings-big.png b/res/img/settings-big.png deleted file mode 100644 index cb2e0a62d0..0000000000 Binary files a/res/img/settings-big.png and /dev/null differ diff --git a/res/img/settings.png b/res/img/settings.png deleted file mode 100644 index 264b3c9bc3..0000000000 Binary files a/res/img/settings.png and /dev/null differ diff --git a/res/img/sound-indicator.svg b/res/img/sound-indicator.svg deleted file mode 100644 index 9b8de53d81..0000000000 --- a/res/img/sound-indicator.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - sound_indicator - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/trans.png b/res/img/trans.png deleted file mode 100644 index 8ba2310a06..0000000000 Binary files a/res/img/trans.png and /dev/null differ diff --git a/res/img/typing.png b/res/img/typing.png deleted file mode 100644 index 066a0ce8fd..0000000000 Binary files a/res/img/typing.png and /dev/null differ diff --git a/res/img/upload-big.png b/res/img/upload-big.png deleted file mode 100644 index c11c0c452d..0000000000 Binary files a/res/img/upload-big.png and /dev/null differ diff --git a/res/img/upload.png b/res/img/upload.png deleted file mode 100644 index 7457bcd0f1..0000000000 Binary files a/res/img/upload.png and /dev/null differ diff --git a/res/img/video-mute.svg b/res/img/video-mute.svg deleted file mode 100644 index 6de60ba39b..0000000000 --- a/res/img/video-mute.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - icons_video copy - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/video-unmute.svg b/res/img/video-unmute.svg deleted file mode 100644 index a6c6c3b681..0000000000 --- a/res/img/video-unmute.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - icons_video copy - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/video.png b/res/img/video.png deleted file mode 100644 index 2a788f6fa4..0000000000 Binary files a/res/img/video.png and /dev/null differ diff --git a/res/img/voice-mute.svg b/res/img/voice-mute.svg deleted file mode 100644 index 336641078e..0000000000 --- a/res/img/voice-mute.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - Audio - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/res/img/voice-unmute.svg b/res/img/voice-unmute.svg deleted file mode 100644 index 0d7e6f429f..0000000000 --- a/res/img/voice-unmute.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - Audio - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/voice.png b/res/img/voice.png deleted file mode 100644 index 5ba765b0f4..0000000000 Binary files a/res/img/voice.png and /dev/null differ diff --git a/res/img/voip-chevron.svg b/res/img/voip-chevron.svg deleted file mode 100644 index 5f7cbe7153..0000000000 --- a/res/img/voip-chevron.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - Triangle 1 - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/res/img/voip-mute.png b/res/img/voip-mute.png deleted file mode 100644 index a16d1001e5..0000000000 Binary files a/res/img/voip-mute.png and /dev/null differ diff --git a/res/img/voip.png b/res/img/voip.png deleted file mode 100644 index e8f05bcc37..0000000000 Binary files a/res/img/voip.png and /dev/null differ diff --git a/res/img/warning.png b/res/img/warning.png deleted file mode 100644 index c5553530a8..0000000000 Binary files a/res/img/warning.png and /dev/null differ diff --git a/res/img/warning2.png b/res/img/warning2.png deleted file mode 100644 index db0fd4a897..0000000000 Binary files a/res/img/warning2.png and /dev/null differ diff --git a/res/img/zoom.png b/res/img/zoom.png deleted file mode 100644 index f05ea959b4..0000000000 Binary files a/res/img/zoom.png and /dev/null differ diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index ba3ddc160c..8175e7d33d 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -18,6 +18,10 @@ $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; +// additional text colors +$secondary-fg-color: #A9B2BC; +$tertiary-fg-color: #8E99A4; + // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -111,10 +115,10 @@ $theme-button-bg-color: #e3e8f0; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-bg-color: rgba(33, 38, 44, 0.90); -$roomlist-header-color: #8E99A4; +$roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$roomtile-preview-color: #A9B2BC; +$roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index dcc2ff6b7b..e9ade7eb97 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -19,8 +19,8 @@ $accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; -$roomlist-header-color: $primary-fg-color; -$notice-secondary-color: $roomlist-header-color; +$secondary-fg-color: #737D8C; +$tertiary-fg-color: #8D99A5; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) @@ -52,10 +52,6 @@ $info-bg-color: #2A9EDF; $mention-user-pill-bg-color: $warning-color; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); -// pinned events indicator -$pinned-unread-color: $notice-primary-color; -$pinned-color: $notice-secondary-color; - // informational plinth $info-plinth-bg-color: #f7f7f7; $info-plinth-fg-color: #888; @@ -177,9 +173,10 @@ $theme-button-bg-color: #e3e8f0; $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-bg-color: rgba(245, 245, 245, 0.90); +$roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$roomtile-preview-color: #737D8C; +$roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #FFF; @@ -198,8 +195,14 @@ $username-variant6-color: #2dc2c5; $username-variant7-color: #5c56f5; $username-variant8-color: #74d12c; +$notice-secondary-color: $roomlist-header-color; + $panel-divider-color: transparent; +// pinned events indicator +$pinned-unread-color: $notice-primary-color; +$pinned-color: $notice-secondary-color; + // ******************** $widget-menu-bar-bg-color: $secondary-accent-color; diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1823cdf50..c30ac62e3b 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -18,7 +18,7 @@ limitations under the License. /** * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with flow-parser. Emits a JSON file with the + * parsing each file with the appropriate parser. Emits a JSON file with the * translatable strings mapped to themselves in the order they appeared * in the files and grouped by the file they appeared in. * @@ -29,8 +29,8 @@ const path = require('path'); const walk = require('walk'); -const flowParser = require('flow-parser'); -const estreeWalker = require('estree-walker'); +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse"); const TRANSLATIONS_FUNCS = ['_t', '_td']; @@ -44,17 +44,9 @@ const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; // to a project that's actively maintained. const SEARCH_PATHS = ['src', 'res']; -const FLOW_PARSER_OPTS = { - esproposal_class_instance_fields: true, - esproposal_class_static_fields: true, - esproposal_decorators: true, - esproposal_export_star_as: true, - types: true, -}; - function getObjectValue(obj, key) { for (const prop of obj.properties) { - if (prop.key.type == 'Identifier' && prop.key.name == key) { + if (prop.key.type === 'Identifier' && prop.key.name === key) { return prop.value; } } @@ -62,11 +54,11 @@ function getObjectValue(obj, key) { } function getTKey(arg) { - if (arg.type == 'Literal') { + if (arg.type === 'Literal' || arg.type === "StringLiteral") { return arg.value; - } else if (arg.type == 'BinaryExpression' && arg.operator == '+') { + } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type == 'TemplateLiteral') { + } else if (arg.type === 'TemplateLiteral') { return arg.quasis.map((q) => { return q.value.raw; }).join(''); @@ -110,81 +102,112 @@ function getFormatStrings(str) { } function getTranslationsJs(file) { - const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS); + const contents = fs.readFileSync(file, { encoding: 'utf8' }); const trs = new Set(); - estreeWalker.walk(tree, { - enter: function(node, parent) { - if ( - node.type == 'CallExpression' && - TRANSLATIONS_FUNCS.includes(node.callee.name) - ) { - const tKey = getTKey(node.arguments[0]); - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; + try { + const plugins = [ + // https://babeljs.io/docs/en/babel-parser#plugins + "classProperties", + "objectRestSpread", + "throwExpressions", + "exportDefaultFrom", + "decorators-legacy", + ]; - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } + if (file.endsWith(".js") || file.endsWith(".jsx")) { + // all JS is assumed to be flow or react + plugins.push("flow", "jsx"); + } else if (file.endsWith(".ts")) { + // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) + plugins.push("typescript"); + } else if (file.endsWith(".tsx")) { + // When the file is a TSX file though, enable JSX parsing + plugins.push("typescript", "jsx"); + } - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); + const babelParsed = parser.parse(contents, { + allowImportExportEverywhere: true, + errorRecovery: true, + sourceFilename: file, + tokens: true, + plugins, + }); + traverse.default(babelParsed, { + enter: (p) => { + const node = p.node; + if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { + const tKey = getTKey(node.arguments[0]); + + // This happens whenever we call _t with non-literals (ie. whenever we've + // had to use a _td to compensate) so is expected. + if (tKey === null) return; + + // check the format string against the args + // We only check _t: _td has no args + if (node.callee.name === '_t') { + try { + const placeholders = getFormatStrings(tKey); + for (const placeholder of placeholders) { + if (node.arguments.length < 2) { + throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); + } + const value = getObjectValue(node.arguments[1], placeholder); + if (value === null) { + throw new Error(`No value found for placeholder '${placeholder}'`); + } + } + + // Validate tag replacements + if (node.arguments.length > 2) { + const tagMap = node.arguments[2]; + for (const prop of tagMap.properties || []) { + if (prop.key.type === 'Literal') { + const tag = prop.key.value; + // RegExp same as in src/languageHandler.js + const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); + if (!tKey.match(regexp)) { + throw new Error(`No match for ${regexp} in ${tKey}`); + } } } } - } - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type == 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); + } catch (e) { + console.log(); + console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); + console.error(e); + process.exit(1); } } - } else { - trs.add(tKey); + + let isPlural = false; + if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { + const countVal = getObjectValue(node.arguments[1], 'count'); + if (countVal) { + isPlural = true; + } + } + + if (isPlural) { + trs.add(tKey + "|other"); + const plurals = enPlurals[tKey]; + if (plurals) { + for (const pluralType of Object.keys(plurals)) { + trs.add(tKey + "|" + pluralType); + } + } + } else { + trs.add(tKey); + } } - } - } - }); + }, + }); + } catch (e) { + console.error(e); + process.exit(1); + } return trs; } diff --git a/src/@types/common.ts b/src/@types/common.ts index a24d47ac9e..b887bd4090 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { JSXElementConstructor } from "react"; + // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude] ? : never}; export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; export type Writeable = { -readonly [P in keyof T]: T[P] }; + +export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 080cdacafd..6510c02160 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -25,6 +25,7 @@ import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {ModalManager} from "../Modal"; +import SettingsStore from "../settings/SettingsStore"; declare global { interface Window { @@ -43,6 +44,7 @@ declare global { mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; singletonModalManager: ModalManager; + mxSettingsStore: SettingsStore; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 diff --git a/src/CallHandler.js b/src/CallHandler.js index 4414bce457..d5e058ef1e 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -62,10 +62,11 @@ import Matrix from 'matrix-js-sdk'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import SettingsStore, { SettingLevel } from './settings/SettingsStore'; +import SettingsStore from './settings/SettingsStore'; import {generateHumanReadableId} from "./utils/NamingUtils"; import {Jitsi} from "./widgets/Jitsi"; import {WidgetType} from "./widgets/WidgetType"; +import {SettingLevel} from "./settings/SettingLevel"; global.mxCalls = { //room_id: MatrixCall diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index a0364f798a..8d56467c57 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -15,7 +15,8 @@ */ import * as Matrix from 'matrix-js-sdk'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import SettingsStore from "./settings/SettingsStore"; +import {SettingLevel} from "./settings/SettingLevel"; export default { hasAnyLabeledDevices: async function() { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index a05392c3e9..2bebe22f14 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -306,6 +306,11 @@ async function _restoreFromLocalStorage(opts) { } const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); + if (pickleKey) { + console.log("Got pickle key"); + } else { + console.log("No pickle key available"); + } console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ @@ -364,6 +369,12 @@ export async function setLoggedIn(credentials) { ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) : null; + if (pickleKey) { + console.log("Created pickle key"); + } else { + console.log("Pickle key not created"); + } + return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true); } @@ -501,6 +512,14 @@ function _persistCredentialsToLocalStorage(credentials) { localStorage.setItem("mx_access_token", credentials.accessToken); localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + if (credentials.pickleKey) { + localStorage.setItem("mx_has_pickle_key", true); + } else { + if (localStorage.getItem("mx_has_pickle_key")) { + console.error("Expected a pickle key, but none provided. Encryption may not work."); + } + } + // if we didn't get a deviceId from the login, leave mx_device_id unset, // rather than setting it to "undefined". // diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 5f334a639c..be16f5fe10 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -256,7 +256,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, - forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), + forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [ verificationMethods.SAS, diff --git a/src/Notifier.js b/src/Notifier.js index c6fc7d7985..2ed302267e 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -27,10 +27,11 @@ import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import SettingsStore from "./settings/SettingsStore"; import { hideToast as hideNotificationsToast, } from "./toasts/DesktopNotificationsToast"; +import {SettingLevel} from "./settings/SettingLevel"; /* * Dispatches: diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js index ec4b88f759..de50feaedb 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -20,9 +20,10 @@ import PropTypes from 'prop-types'; import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; -import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import SettingsStore from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {Action} from "../../../../dispatcher/actions"; +import {SettingLevel} from "../../../../settings/SettingLevel"; /* * Allows the user to disable the Event Index. diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index a9dd5be34b..be3368b87b 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -19,11 +19,12 @@ import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; import SdkConfig from '../../../../SdkConfig'; -import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import SettingsStore from "../../../../settings/SettingsStore"; import Modal from '../../../../Modal'; import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; +import {SettingLevel} from "../../../../settings/SettingLevel"; /* * Allows the user to introspect the event index state and disable it. diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index f717fb11f6..9c91414556 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -142,7 +142,8 @@ export default class QueryMatcher { private processQuery(query: string): string { if (this._options.fuzzy !== false) { - return removeHiddenChars(query).toLowerCase(); + // lower case both the input and the output for consistency + return removeHiddenChars(query.toLowerCase()).toLowerCase(); } return query.toLowerCase(); } diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index f14fa3bbfa..b18b2d132c 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -118,7 +118,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return '💬 ' + _t('Rooms'); + return _t('Rooms'); } renderCompletions(completions: React.ReactNode[]): React.ReactNode { diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index eeb6c7a522..c957b5e597 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -137,7 +137,7 @@ export default class UserProvider extends AutocompleteProvider { } getName(): string { - return '👥 ' + _t('Users'); + return _t('Users'); } _makeUsers() { diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index f8c03be864..d873dd4094 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -210,6 +210,11 @@ const FilePanel = createReactClass({ const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner"); + const emptyState = (
+

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

+

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

+
); + if (this.state.timelineSet) { // console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " + // "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId); @@ -223,7 +228,7 @@ const FilePanel = createReactClass({ onPaginationRequest={this.onPaginationRequest} tileShape="file_grid" resizeNotifier={this.props.resizeNotifier} - empty={_t('There are no visible files in this room')} + empty={emptyState} /> ); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 86136233d8..bc17bbe23f 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -44,7 +44,6 @@ interface IProps { } interface IState { - searchFilter: string; showBreadcrumbs: boolean; showTagPanel: boolean; } @@ -69,7 +68,6 @@ export default class LeftPanel extends React.Component { super(props); this.state = { - searchFilter: "", showBreadcrumbs: BreadcrumbsStore.instance.visible, showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -97,10 +95,6 @@ export default class LeftPanel extends React.Component { this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } - private onSearch = (term: string): void => { - this.setState({searchFilter: term}); - }; - private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -366,7 +360,6 @@ export default class LeftPanel extends React.Component { onKeyDown={this.onKeyDown} > { onKeyDown={this.onKeyDown} resizeNotifier={null} collapsed={false} - searchFilter={this.state.searchFilter} onFocus={this.onFocus} onBlur={this.onBlur} isMinimized={this.props.isMinimized} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 1f561e68ef..48669a3721 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -54,6 +54,8 @@ import LeftPanel from "./LeftPanel"; import CallContainer from '../views/voip/CallContainer'; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import RoomListStore from "../../stores/room-list/RoomListStore"; +import NonUrgentToastContainer from "./NonUrgentToastContainer"; +import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -472,8 +474,8 @@ class LoggedInView extends React.Component { case Key.PERIOD: if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) { - dis.dispatch({ - action: 'toggle_right_panel', + dis.dispatch({ + action: Action.ToggleRightPanel, type: this.props.page_type === "room_view" ? "room" : "group", }); handled = true; @@ -687,6 +689,7 @@ class LoggedInView extends React.Component { + ); } diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 7c66f21a04..800ed76bb9 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -16,77 +16,24 @@ limitations under the License. */ import React from 'react'; -import ResizeHandle from '../views/elements/ResizeHandle'; -import {Resizer, FixedDistributor} from '../../resizer'; +import { Resizable } from 're-resizable'; export default class MainSplit extends React.Component { - constructor(props) { - super(props); - this._setResizeContainerRef = this._setResizeContainerRef.bind(this); - this._onResized = this._onResized.bind(this); + _onResized = (event, direction, refToElement, delta) => { + window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width); } - _onResized(size) { - window.localStorage.setItem("mx_rhs_size", size); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.notifyRightHandleResized(); - } - } + _loadSidePanelSize() { + let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10); - _createResizer() { - const classNames = { - handle: "mx_ResizeHandle", - vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse", - }; - const resizer = new Resizer( - this.resizeContainer, - FixedDistributor, - {onResized: this._onResized}, - ); - resizer.setClassNames(classNames); - let rhsSize = window.localStorage.getItem("mx_rhs_size"); - if (rhsSize !== null) { - rhsSize = parseInt(rhsSize, 10); - } else { + if (isNaN(rhsSize)) { rhsSize = 350; } - resizer.forHandleAt(0).resize(rhsSize); - resizer.attach(); - this.resizer = resizer; - } - - _setResizeContainerRef(div) { - this.resizeContainer = div; - } - - componentDidMount() { - if (this.props.panel) { - this._createResizer(); - } - } - - componentWillUnmount() { - if (this.resizer) { - this.resizer.detach(); - this.resizer = null; - } - } - - componentDidUpdate(prevProps) { - const wasPanelSet = this.props.panel && !prevProps.panel; - const wasPanelCleared = !this.props.panel && prevProps.panel; - - if (this.resizeContainer && wasPanelSet) { - // The resizer can only be created when **both** expanded and the panel is - // set. Once both are true, the container ref will mount, which is required - // for the resizer to work. - this._createResizer(); - } else if (this.resizer && wasPanelCleared) { - this.resizer.detach(); - this.resizer = null; - } + return { + height: "100%", + width: rhsSize, + }; } render() { @@ -97,13 +44,29 @@ export default class MainSplit extends React.Component { let children; if (hasResizer) { - children = - + children = { panelView } - ; + ; } - return
+ return
{ bodyView } { children }
; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e68e1c53ae..a66d4c043f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -51,7 +51,7 @@ import { getHomePageUrl } from '../../utils/pages'; import createRoom from "../../createRoom"; import {_t, _td, getCurrentLanguage} from '../../languageHandler'; -import SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; +import SettingsStore from "../../settings/SettingsStore"; import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; @@ -75,6 +75,7 @@ import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificat import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import ErrorDialog from "../views/dialogs/ErrorDialog"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; +import { SettingLevel } from "../../settings/SettingLevel"; /** constants for MatrixChat.state.view */ export enum Views { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 7567786af3..230d136e04 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -346,9 +346,9 @@ export default class MessagePanel extends React.Component { } } - _isUnmounting() { + _isUnmounting = () => { return !this._isMounted; - } + }; // TODO: Implement granular (per-room) hide options _shouldShowEvent(mxEv) { @@ -571,12 +571,10 @@ export default class MessagePanel extends React.Component { const readReceipts = this._readReceiptsByEvent[eventId]; - // Dev note: `this._isUnmounting.bind(this)` is important - it ensures that - // the function is run in the context of this class and not EventTile, therefore - // ensuring the right `this._mounted` variable is used by read receipts (which - // don't update their position if we, the MessagePanel, is unmounting). + // use txnId as key if available so that we don't remount during sending ret.push( -
  • @@ -590,7 +588,7 @@ export default class MessagePanel extends React.Component { readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} showUrlPreview={this.props.showUrlPreview} - checkUnmounting={this._isUnmounting.bind(this)} + checkUnmounting={this._isUnmounting} eventSendStatus={mxEv.getAssociatedStatus()} tileShape={this.props.tileShape} isTwelveHour={this.props.isTwelveHour} diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx new file mode 100644 index 0000000000..8d415df4dd --- /dev/null +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -0,0 +1,63 @@ +/* +Copyright 2020 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 * as React from "react"; +import { ComponentClass } from "../../@types/common"; +import NonUrgentToastStore from "../../stores/NonUrgentToastStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; + +interface IProps { +} + +interface IState { + toasts: ComponentClass[], +} + +export default class NonUrgentToastContainer extends React.PureComponent { + public constructor(props, context) { + super(props, context); + + this.state = { + toasts: NonUrgentToastStore.instance.components, + }; + + NonUrgentToastStore.instance.on(UPDATE_EVENT, this.onUpdateToasts); + } + + public componentWillUnmount() { + NonUrgentToastStore.instance.off(UPDATE_EVENT, this.onUpdateToasts); + } + + private onUpdateToasts = () => { + this.setState({toasts: NonUrgentToastStore.instance.components}); + }; + + public render() { + const toasts = this.state.toasts.map((t, i) => { + return ( +
    + {React.createElement(t, {})} +
    + ); + }); + + return ( +
    + {toasts} +
    + ); + } +} diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index c1a0ec9c4b..c1f78cffda 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -36,6 +36,11 @@ const NotificationPanel = createReactClass({ const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner"); + const emptyState = (
    +

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

    +

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

    +
    ); + const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { return ( @@ -46,7 +51,7 @@ const NotificationPanel = createReactClass({ timelineSet={timelineSet} showUrlPreview={false} tileShape="notif" - empty={_t('You have no visible notifications')} + empty={emptyState} />
  • ); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 776130e709..a4e3254e4c 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -26,7 +26,7 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; -import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; +import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import {Action} from "../../dispatcher/actions"; @@ -75,8 +75,8 @@ export default class RightPanel extends React.Component { const userForPanel = this._getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { - dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList}); - return RIGHT_PANEL_PHASES.GroupMemberList; + dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); + return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; } else if (userForPanel) { @@ -98,11 +98,11 @@ export default class RightPanel extends React.Component { ) { return rps.roomPanelPhase; } - return RIGHT_PANEL_PHASES.RoomMemberInfo; + return RightPanelPhases.RoomMemberInfo; } else { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) { - dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList}); - return RIGHT_PANEL_PHASES.RoomMemberList; + dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList}); + return RightPanelPhases.RoomMemberList; } return rps.roomPanelPhase; } @@ -149,7 +149,7 @@ export default class RightPanel extends React.Component { onInviteToGroupButtonClick() { showGroupInviteDialog(this.props.groupId).then(() => { this.setState({ - phase: RIGHT_PANEL_PHASES.GroupMemberList, + phase: RightPanelPhases.GroupMemberList, }); }); } @@ -165,9 +165,9 @@ export default class RightPanel extends React.Component { return; } // redraw the badge on the membership list - if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) { + if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) { this._delayedUpdate(); - } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId && + } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) this._delayedUpdate(); @@ -175,7 +175,7 @@ export default class RightPanel extends React.Component { } onAction(payload) { - if (payload.action === "after_right_panel_phase_change") { + if (payload.action === Action.AfterRightPanelPhaseChange) { this.setState({ phase: payload.phase, groupRoomId: payload.groupRoomId, @@ -206,7 +206,7 @@ export default class RightPanel extends React.Component { // or the member list if we were in the member panel... phew. dis.dispatch({ action: Action.ViewUser, - member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, + member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null, }); } }; @@ -225,21 +225,21 @@ export default class RightPanel extends React.Component { let panel =
    ; switch (this.state.phase) { - case RIGHT_PANEL_PHASES.RoomMemberList: + case RightPanelPhases.RoomMemberList: if (this.props.roomId) { panel = ; } break; - case RIGHT_PANEL_PHASES.GroupMemberList: + case RightPanelPhases.GroupMemberList: if (this.props.groupId) { panel = ; } break; - case RIGHT_PANEL_PHASES.GroupRoomList: + case RightPanelPhases.GroupRoomList: panel = ; break; - case RIGHT_PANEL_PHASES.RoomMemberInfo: - case RIGHT_PANEL_PHASES.EncryptionPanel: + case RightPanelPhases.RoomMemberInfo: + case RightPanelPhases.EncryptionPanel: panel = ; break; - case RIGHT_PANEL_PHASES.Room3pidMemberInfo: + case RightPanelPhases.Room3pidMemberInfo: panel = ; break; - case RIGHT_PANEL_PHASES.GroupMemberInfo: + case RightPanelPhases.GroupMemberInfo: panel = ; break; - case RIGHT_PANEL_PHASES.GroupRoomInfo: + case RightPanelPhases.GroupRoomInfo: panel = ; break; - case RIGHT_PANEL_PHASES.NotificationPanel: + case RightPanelPhases.NotificationPanel: panel = ; break; - case RIGHT_PANEL_PHASES.FilePanel: + case RightPanelPhases.FilePanel: panel = ; break; } diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 1451630c97..69504e9ab8 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -24,9 +24,10 @@ import { throttle } from 'lodash'; import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; import { Action } from "../../dispatcher/actions"; +import RoomListStore from "../../stores/room-list/RoomListStore"; +import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; interface IProps { - onQueryUpdate: (newQuery: string) => void; isMinimized: boolean; onVerticalArrow(ev: React.KeyboardEvent): void; onEnter(ev: React.KeyboardEvent): boolean; @@ -40,6 +41,7 @@ interface IState { export default class RoomSearch extends React.PureComponent { private dispatcherRef: string; private inputRef: React.RefObject = createRef(); + private searchFilter: NameFilterCondition = new NameFilterCondition(); constructor(props: IProps) { super(props); @@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent { this.dispatcherRef = defaultDispatcher.register(this.onAction); } + public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if (prevState.query !== this.state.query) { + const hadSearch = !!this.searchFilter.search.trim(); + const haveSearch = !!this.state.query.trim(); + this.searchFilter.search = this.state.query; + if (!hadSearch && haveSearch) { + // started a new filter - add the condition + RoomListStore.instance.addFilter(this.searchFilter); + } else if (hadSearch && !haveSearch) { + // cleared a filter - remove the condition + RoomListStore.instance.removeFilter(this.searchFilter); + } // else the filter hasn't changed enough for us to care here + } + } + public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); } @@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent { private onChange = () => { if (!this.inputRef.current) return; this.setState({query: this.inputRef.current.value}); - this.onSearchUpdated(); }; - // it wants this at the top of the file, but we know better - // tslint:disable-next-line - private onSearchUpdated = throttle( - () => { - // We can't use the state variable because it can lag behind the input. - // The lag is most obvious when deleting/clearing text with the keyboard. - this.props.onQueryUpdate(this.inputRef.current.value); - }, 200, {trailing: true, leading: true}, - ); - private onFocus = (ev: React.FocusEvent) => { this.setState({focused: true}); ev.target.select(); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 7dc2d57ff0..f585a97fde 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -48,7 +48,7 @@ import RightPanel from './RightPanel'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; -import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import SettingsStore from "../../settings/SettingsStore"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; @@ -56,6 +56,7 @@ import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; import {Action} from "../../dispatcher/actions"; +import {SettingLevel} from "../../settings/SettingLevel"; const DEBUG = false; let debuglog = function() {}; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 8f9ca8d734..3f2e387ccb 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -26,7 +26,7 @@ import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; -import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import SettingsStore from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; import {ButtonEvent} from "../views/elements/AccessibleButton"; @@ -37,6 +37,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; import classNames from "classnames"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; +import { SettingLevel } from "../../settings/SettingLevel"; interface IProps { isMinimized: boolean; diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 6577386fae..a539c8c9ee 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -72,7 +72,7 @@ export default class SoftLogout extends React.Component { this._initLogin(); - MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => { + MatrixClientPeg.get().countSessionsNeedingBackup().then(remaining => { this.setState({keyBackupNeeded: remaining > 0}); }); } diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.js index 83db5d225b..0738ee43e4 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.js @@ -16,10 +16,11 @@ limitations under the License. import SdkConfig from "../../../SdkConfig"; import {getCurrentLanguage} from "../../../languageHandler"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; import * as sdk from '../../../index'; import React from 'react'; +import {SettingLevel} from "../../../settings/SettingLevel"; function onChange(newLang) { if (getCurrentLanguage() !== newLang) { diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index 120ad8deca..7a12d2bd20 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -19,8 +19,8 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default createReactClass({ propTypes: { diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 4d7a66e957..c90811ed5a 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -349,7 +349,7 @@ export default class InviteDialog extends React.PureComponent { // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM]; + const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || []; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx new file mode 100644 index 0000000000..f6767dcb8d --- /dev/null +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -0,0 +1,124 @@ +/* +Copyright 2020 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 * as React from 'react'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import { EchoStore } from "../../../stores/local-echo/EchoStore"; +import { formatTime } from "../../../DateUtils"; +import SettingsStore from "../../../settings/SettingsStore"; +import { RoomEchoContext } from "../../../stores/local-echo/RoomEchoContext"; +import RoomAvatar from "../avatars/RoomAvatar"; +import { TransactionStatus } from "../../../stores/local-echo/EchoTransaction"; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; + +interface IProps { + onFinished: (bool) => void; +} + +export default class ServerOfflineDialog extends React.PureComponent { + public componentDidMount() { + EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated); + } + + public componentWillUnmount() { + EchoStore.instance.off(UPDATE_EVENT, this.onEchosUpdated); + } + + private onEchosUpdated = () => { + this.forceUpdate(); // no state to worry about + }; + + private renderTimeline(): React.ReactElement[] { + return EchoStore.instance.contexts.map((c, i) => { + if (!c.firstFailedTime) return null; // not useful + if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c); + const header = ( +
    + + {c.room.name} +
    + ); + const entries = c.transactions + .filter(t => t.status === TransactionStatus.Error || t.didPreviouslyFail) + .map((t, j) => { + let button = ; + if (t.status === TransactionStatus.Error) { + button = ( + t.run()}>{_t("Resend")} + ); + } + return ( +
    + + {t.auditName} + + {button} +
    + ); + }); + return ( +
    +
    + {formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))} +
    +
    + {header} + {entries} +
    +
    + ) + }); + } + + public render() { + let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check + if (timeline.length === 0) { + timeline = [
    {_t("You're all caught up.")}
    ]; + } + + const serverName = MatrixClientPeg.getHomeserverName(); + return +
    +

    {_t( + "Your server isn't responding to some of your requests. " + + "Below are some of the most likely reasons.", + )}

    +
      +
    • {_t("The server (%(serverName)s) took too long to respond.", {serverName})}
    • +
    • {_t("Your firewall or anti-virus is blocking the request.")}
    • +
    • {_t("A browser extension is preventing the request.")}
    • +
    • {_t("The server is offline.")}
    • +
    • {_t("The server has denied your request.")}
    • +
    • {_t("Your area is experiencing difficulties connecting to the internet.")}
    • +
    • {_t("A connection error occurred while trying to contact the server.")}
    • +
    • {_t("The server is not configured to indicate what the problem is (CORS).")}
    • +
    +
    +

    {_t("Recent changes that have not yet been received")}

    + {timeline} +
    +
    ; + } +} diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index 162cb4736a..42a5304f13 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -17,10 +17,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import WidgetUtils from "../../../utils/WidgetUtils"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default class WidgetOpenIDPermissionsDialog extends React.Component { static propTypes = { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 3e4418f945..d0fc56743f 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -35,12 +35,13 @@ import dis from '../../../dispatcher/dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import PersistedElement from "./PersistedElement"; import {WidgetType} from "../../../widgets/WidgetType"; import {Capability} from "../../../widgets/WidgetApi"; import {sleep} from "../../../utils/promise"; +import {SettingLevel} from "../../../settings/SettingLevel"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 65140707d5..1098d0293e 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -15,8 +15,9 @@ limitations under the License. */ import React from 'react'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import Draggable, {ILocationState} from './Draggable'; +import { SettingLevel } from "../../../settings/SettingLevel"; interface IProps { // Current room diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 4f41db51e2..03e91fac62 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -20,11 +20,12 @@ import SettingsStore from "../../../settings/SettingsStore"; import { _t } from '../../../languageHandler'; import ToggleSwitch from "./ToggleSwitch"; import StyledCheckbox from "./StyledCheckbox"; +import { SettingLevel } from "../../../settings/SettingLevel"; interface IProps { // The setting must be a boolean name: string; - level: string; + level: SettingLevel; roomId?: string; // for per-room settings label?: string; // untranslated isExplicit?: boolean; @@ -52,8 +53,8 @@ export default class SettingsFlag extends React.Component { }; } - private onChange = (checked: boolean): void => { - this.save(checked); + private onChange = async (checked: boolean) => { + await this.save(checked); this.setState({ value: checked }); if (this.props.onChange) this.props.onChange(checked); }; @@ -62,8 +63,8 @@ export default class SettingsFlag extends React.Component { this.onChange(e.target.checked); }; - private save = (val?: boolean): void => { - return SettingsStore.setValue( + private save = async (val?: boolean) => { + await SettingsStore.setValue( this.props.name, this.props.roomId, this.props.level, diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 0c29e01995..b0405dc4c9 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -47,11 +47,10 @@ export default class TextWithTooltip extends React.Component { return ( {this.props.children} - + className={"mx_TextWithTooltip_tooltip"} /> } ); } diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 7b643c7346..031b875409 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -24,8 +24,9 @@ import GroupStore from '../../../stores/GroupStore'; import PropTypes from 'prop-types'; import { showGroupInviteDialog } from '../../../GroupAddressPicker'; import AccessibleButton from '../elements/AccessibleButton'; -import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import {Action} from "../../../dispatcher/actions"; const INITIAL_LOAD_NUM_MEMBERS = 30; @@ -164,9 +165,9 @@ export default createReactClass({ onInviteToGroupButtonClick() { showGroupInviteDialog(this.props.groupId).then(() => { dis.dispatch({ - action: 'set_right_panel_phase', - phase: RIGHT_PANEL_PHASES.GroupMemberList, - groupId: this.props.groupId, + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.GroupMemberList, + refireParams: { groupId: this.props.groupId }, }); }); }, diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index a5b1ae26bb..01a5c2663e 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler'; import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; import dis from "../../../dispatcher/dispatcher"; -import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import {Action} from "../../../dispatcher/actions"; export default class MKeyVerificationRequest extends React.Component { constructor(props) { @@ -48,8 +49,8 @@ export default class MKeyVerificationRequest extends React.Component { const {verificationRequest} = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); dis.dispatch({ - action: "set_right_panel_phase", - phase: RIGHT_PANEL_PHASES.EncryptionPanel, + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.EncryptionPanel, refireParams: {verificationRequest, member}, }); }; diff --git a/src/components/views/right_panel/EncryptionInfo.js b/src/components/views/right_panel/EncryptionInfo.tsx similarity index 87% rename from src/components/views/right_panel/EncryptionInfo.js rename to src/components/views/right_panel/EncryptionInfo.tsx index 007e2831ce..f62af65543 100644 --- a/src/components/views/right_panel/EncryptionInfo.js +++ b/src/components/views/right_panel/EncryptionInfo.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; import * as sdk from "../../../index"; import {_t} from "../../../languageHandler"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; export const PendingActionSpinner = ({text}) => { const Spinner = sdk.getComponent('elements.Spinner'); @@ -28,7 +28,17 @@ export const PendingActionSpinner = ({text}) => {
    ; }; -const EncryptionInfo = ({ +interface IProps { + waitingForOtherParty: boolean; + waitingForNetwork: boolean; + member: RoomMember; + onStartVerification: () => Promise; + isRoomEncrypted: boolean; + inDialog: boolean; + isSelfVerification: boolean; +} + +const EncryptionInfo: React.FC = ({ waitingForOtherParty, waitingForNetwork, member, @@ -36,10 +46,10 @@ const EncryptionInfo = ({ isRoomEncrypted, inDialog, isSelfVerification, -}) => { - let content; +}: IProps) => { + let content: JSX.Element; if (waitingForOtherParty || waitingForNetwork) { - let text; + let text: string; if (waitingForOtherParty) { if (isSelfVerification) { text = _t("Waiting for you to accept on your other session…"); @@ -61,7 +71,7 @@ const EncryptionInfo = ({ ); } - let description; + let description: JSX.Element; if (isRoomEncrypted) { description = (
    @@ -97,10 +107,5 @@ const EncryptionInfo = ({
    ; }; -EncryptionInfo.propTypes = { - member: PropTypes.object.isRequired, - onStartVerification: PropTypes.func.isRequired, - request: PropTypes.object, -}; export default EncryptionInfo; diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.tsx similarity index 86% rename from src/components/views/right_panel/EncryptionPanel.js rename to src/components/views/right_panel/EncryptionPanel.tsx index e9f94729fa..df52e5cabd 100644 --- a/src/components/views/right_panel/EncryptionPanel.js +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, {useCallback, useEffect, useState} from "react"; -import PropTypes from "prop-types"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; @@ -26,11 +25,23 @@ import Modal from "../../../Modal"; import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import * as sdk from "../../../index"; import {_t} from "../../../languageHandler"; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; -const EncryptionPanel = (props) => { +interface IProps { + member: RoomMember; + onClose: () => void; + verificationRequest: VerificationRequest; + verificationRequestPromise: Promise; + layout: string; + inDialog: boolean; + isRoomEncrypted: boolean; +} + +const EncryptionPanel: React.FC = (props: IProps) => { const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props; const [request, setRequest] = useState(verificationRequest); // state to show a spinner immediately after clicking "start verification", @@ -48,10 +59,10 @@ const EncryptionPanel = (props) => { useEffect(() => { async function awaitPromise() { setRequesting(true); - const request = await verificationRequestPromise; + const requestFromPromise = await verificationRequestPromise; setRequesting(false); - setRequest(request); - setPhase(request.phase); + setRequest(requestFromPromise); + setPhase(requestFromPromise.phase); } if (verificationRequestPromise) { awaitPromise(); @@ -90,7 +101,7 @@ const EncryptionPanel = (props) => { } }, [request]); - let cancelButton; + let cancelButton: JSX.Element; if (layout !== "dialog" && request && request.pending) { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); cancelButton = ( { setRequesting(true); const cli = MatrixClientPeg.get(); const roomId = await ensureDMExists(cli, member.userId); - const verificationRequest = await cli.requestVerificationDM(member.userId, roomId); - setRequest(verificationRequest); - setPhase(verificationRequest.phase); + const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId); + setRequest(verificationRequest_); + setPhase(verificationRequest_.phase); }, [member.userId]); const requested = @@ -144,12 +155,5 @@ const EncryptionPanel = (props) => { ); } }; -EncryptionPanel.propTypes = { - member: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired, - verificationRequest: PropTypes.object, - layout: PropTypes.string, - inDialog: PropTypes.bool, -}; export default EncryptionPanel; diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.tsx similarity index 60% rename from src/components/views/right_panel/GroupHeaderButtons.js rename to src/components/views/right_panel/GroupHeaderButtons.tsx index 33d9325433..44237e401f 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.js +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -21,65 +21,68 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons'; -import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import HeaderButtons, {HeaderKind} from './HeaderButtons'; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {Action} from "../../../dispatcher/actions"; import {ActionPayload} from "../../../dispatcher/payloads"; +import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; const GROUP_PHASES = [ - RIGHT_PANEL_PHASES.GroupMemberInfo, - RIGHT_PANEL_PHASES.GroupMemberList, + RightPanelPhases.GroupMemberInfo, + RightPanelPhases.GroupMemberList, ]; const ROOM_PHASES = [ - RIGHT_PANEL_PHASES.GroupRoomList, - RIGHT_PANEL_PHASES.GroupRoomInfo, + RightPanelPhases.GroupRoomList, + RightPanelPhases.GroupRoomInfo, ]; +interface IProps {} + export default class GroupHeaderButtons extends HeaderButtons { - constructor(props) { - super(props, HEADER_KIND_GROUP); - this._onMembersClicked = this._onMembersClicked.bind(this); - this._onRoomsClicked = this._onRoomsClicked.bind(this); + constructor(props: IProps) { + super(props, HeaderKind.Group); + this.onMembersClicked = this.onMembersClicked.bind(this); + this.onRoomsClicked = this.onRoomsClicked.bind(this); } - onAction(payload: ActionPayload) { + protected onAction(payload: ActionPayload) { super.onAction(payload); if (payload.action === Action.ViewUser) { - if (payload.member) { - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); + if ((payload as ViewUserPayload).member) { + this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member}); } else { - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); + this.setPhase(RightPanelPhases.GroupMemberList); } } else if (payload.action === "view_group") { - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); + this.setPhase(RightPanelPhases.GroupMemberList); } else if (payload.action === "view_group_room") { this.setPhase( - RIGHT_PANEL_PHASES.GroupRoomInfo, + RightPanelPhases.GroupRoomInfo, {groupRoomId: payload.groupRoomId, groupId: payload.groupId}, ); } else if (payload.action === "view_group_room_list") { - this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList); + this.setPhase(RightPanelPhases.GroupRoomList); } else if (payload.action === "view_group_member_list") { - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); + this.setPhase(RightPanelPhases.GroupMemberList); } else if (payload.action === "view_group_user") { - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo, {member: payload.member}); + this.setPhase(RightPanelPhases.GroupMemberInfo, {member: payload.member}); } } - _onMembersClicked() { - if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) { + private onMembersClicked() { + if (this.state.phase === RightPanelPhases.GroupMemberInfo) { // send the active phase to trigger a toggle - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo); + this.setPhase(RightPanelPhases.GroupMemberInfo); } else { // This toggles for us, if needed - this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); + this.setPhase(RightPanelPhases.GroupMemberList); } } - _onRoomsClicked() { + private onRoomsClicked() { // This toggles for us, if needed - this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList); + this.setPhase(RightPanelPhases.GroupRoomList); } renderButtons() { @@ -87,13 +90,13 @@ export default class GroupHeaderButtons extends HeaderButtons { , , ]; diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.tsx similarity index 80% rename from src/components/views/right_panel/HeaderButton.js rename to src/components/views/right_panel/HeaderButton.tsx index 0091b7a5c0..ff092ca060 100644 --- a/src/components/views/right_panel/HeaderButton.js +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -19,25 +19,40 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import Analytics from '../../../Analytics'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +interface IProps { + // Whether this button is highlighted + isHighlighted: boolean; + // click handler + onClick: () => void; + // The badge to display above the icon + badge?: React.ReactNode; + // The parameters to track the click event + analytics: string[]; + + // Button name + name: string; + // Button title + title: string; +} + // TODO: replace this, the composer buttons and the right panel buttons with a unified // representation -export default class HeaderButton extends React.Component { - constructor() { - super(); +export default class HeaderButton extends React.Component { + constructor(props: IProps) { + super(props); this.onClick = this.onClick.bind(this); } - onClick(ev) { + private onClick() { Analytics.trackEvent(...this.props.analytics); this.props.onClick(); } - render() { + public render() { const classes = classNames({ mx_RightPanel_headerButton: true, mx_RightPanel_headerButton_highlight: this.props.isHighlighted, @@ -53,19 +68,3 @@ export default class HeaderButton extends React.Component { />; } } - -HeaderButton.propTypes = { - // Whether this button is highlighted - isHighlighted: PropTypes.bool.isRequired, - // click handler - onClick: PropTypes.func.isRequired, - // The badge to display above the icon - badge: PropTypes.node, - // The parameters to track the click event - analytics: PropTypes.arrayOf(PropTypes.string).isRequired, - - // Button name - name: PropTypes.string.isRequired, - // Button title - title: PropTypes.string.isRequired, -}; diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js deleted file mode 100644 index 1c66fe5828..0000000000 --- a/src/components/views/right_panel/HeaderButtons.js +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 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 React from 'react'; -import dis from '../../../dispatcher/dispatcher'; -import RightPanelStore from "../../../stores/RightPanelStore"; - -export const HEADER_KIND_ROOM = "room"; -export const HEADER_KIND_GROUP = "group"; - -const HEADER_KINDS = [HEADER_KIND_GROUP, HEADER_KIND_ROOM]; - -export default class HeaderButtons extends React.Component { - constructor(props, kind) { - super(props); - - if (!HEADER_KINDS.includes(kind)) throw new Error(`Invalid header kind: ${kind}`); - - const rps = RightPanelStore.getSharedInstance(); - this.state = { - headerKind: kind, - phase: kind === HEADER_KIND_ROOM ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase, - }; - } - - componentDidMount() { - this._storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this)); - this._dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses - } - - componentWillUnmount() { - if (this._storeToken) this._storeToken.remove(); - if (this._dispatcherRef) dis.unregister(this._dispatcherRef); - } - - onAction(payload) { - // Ignore - intended to be overridden by subclasses - } - - setPhase(phase, extras) { - dis.dispatch({ - action: 'set_right_panel_phase', - phase: phase, - refireParams: extras, - }); - } - - isPhase(phases: string | string[]) { - if (Array.isArray(phases)) { - return phases.includes(this.state.phase); - } else { - return phases === this.state.phase; - } - } - - onRightPanelUpdate() { - const rps = RightPanelStore.getSharedInstance(); - if (this.state.headerKind === HEADER_KIND_ROOM) { - this.setState({phase: rps.visibleRoomPanelPhase}); - } else if (this.state.headerKind === HEADER_KIND_GROUP) { - this.setState({phase: rps.visibleGroupPanelPhase}); - } - } - - render() { - // inline style as this will be swapped around in future commits - return
    - {this.renderButtons()} -
    ; - } -} diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx new file mode 100644 index 0000000000..57d3075739 --- /dev/null +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -0,0 +1,110 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 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 React from 'react'; +import dis from '../../../dispatcher/dispatcher'; +import RightPanelStore from "../../../stores/RightPanelStore"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import {Action} from '../../../dispatcher/actions'; +import {SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams} from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; +import {EventSubscription} from "fbemitter"; + +export enum HeaderKind { + Room = "room", + Group = "group", +} + +interface IState { + headerKind: HeaderKind; + phase: RightPanelPhases; +} + +interface IProps {} + +export default class HeaderButtons extends React.Component { + private storeToken: EventSubscription; + private dispatcherRef: string; + + constructor(props: IProps, kind: HeaderKind) { + super(props); + + const rps = RightPanelStore.getSharedInstance(); + this.state = { + headerKind: kind, + phase: kind === HeaderKind.Room ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase, + }; + } + + public componentDidMount() { + this.storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this)); + this.dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses + } + + public componentWillUnmount() { + if (this.storeToken) this.storeToken.remove(); + if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + } + + protected onAction(payload) { + // Ignore - intended to be overridden by subclasses + } + + public setPhase(phase: RightPanelPhases, extras?: Partial) { + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: phase, + refireParams: extras, + }); + } + + public isPhase(phases: string | string[]) { + if (Array.isArray(phases)) { + return phases.includes(this.state.phase); + } else { + return phases === this.state.phase; + } + } + + private onRightPanelUpdate() { + const rps = RightPanelStore.getSharedInstance(); + if (this.state.headerKind === HeaderKind.Room) { + this.setState({phase: rps.visibleRoomPanelPhase}); + } else if (this.state.headerKind === HeaderKind.Group) { + this.setState({phase: rps.visibleGroupPanelPhase}); + } + } + + // XXX: Make renderButtons a prop + public renderButtons(): JSX.Element[] { + // Ignore - intended to be overridden by subclasses + // Return empty fragment to satisfy the type + return [ + + + ]; + } + + public render() { + // inline style as this will be swapped around in future commits + return
    + {this.renderButtons()} +
    ; + } +} diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.tsx similarity index 59% rename from src/components/views/right_panel/RoomHeaderButtons.js rename to src/components/views/right_panel/RoomHeaderButtons.tsx index 838727981d..7ac547f499 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -21,82 +21,82 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons'; -import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import HeaderButtons, {HeaderKind} from './HeaderButtons'; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {Action} from "../../../dispatcher/actions"; import {ActionPayload} from "../../../dispatcher/payloads"; const MEMBER_PHASES = [ - RIGHT_PANEL_PHASES.RoomMemberList, - RIGHT_PANEL_PHASES.RoomMemberInfo, - RIGHT_PANEL_PHASES.EncryptionPanel, - RIGHT_PANEL_PHASES.Room3pidMemberInfo, + RightPanelPhases.RoomMemberList, + RightPanelPhases.RoomMemberInfo, + RightPanelPhases.EncryptionPanel, + RightPanelPhases.Room3pidMemberInfo, ]; export default class RoomHeaderButtons extends HeaderButtons { constructor(props) { - super(props, HEADER_KIND_ROOM); - this._onMembersClicked = this._onMembersClicked.bind(this); - this._onFilesClicked = this._onFilesClicked.bind(this); - this._onNotificationsClicked = this._onNotificationsClicked.bind(this); + super(props, HeaderKind.Room); + this.onMembersClicked = this.onMembersClicked.bind(this); + this.onFilesClicked = this.onFilesClicked.bind(this); + this.onNotificationsClicked = this.onNotificationsClicked.bind(this); } - onAction(payload: ActionPayload) { + protected onAction(payload: ActionPayload) { super.onAction(payload); if (payload.action === Action.ViewUser) { if (payload.member) { - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); + this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member}); } else { - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); + this.setPhase(RightPanelPhases.RoomMemberList); } } else if (payload.action === "view_3pid_invite") { if (payload.event) { - this.setPhase(RIGHT_PANEL_PHASES.Room3pidMemberInfo, {event: payload.event}); + this.setPhase(RightPanelPhases.Room3pidMemberInfo, {event: payload.event}); } else { - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); + this.setPhase(RightPanelPhases.RoomMemberList); } } } - _onMembersClicked() { - if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) { + private onMembersClicked() { + if (this.state.phase === RightPanelPhases.RoomMemberInfo) { // send the active phase to trigger a toggle // XXX: we should pass refireParams here but then it won't collapse as we desire it to - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo); + this.setPhase(RightPanelPhases.RoomMemberInfo); } else { // This toggles for us, if needed - this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); + this.setPhase(RightPanelPhases.RoomMemberList); } } - _onFilesClicked() { + private onFilesClicked() { // This toggles for us, if needed - this.setPhase(RIGHT_PANEL_PHASES.FilePanel); + this.setPhase(RightPanelPhases.FilePanel); } - _onNotificationsClicked() { + private onNotificationsClicked() { // This toggles for us, if needed - this.setPhase(RIGHT_PANEL_PHASES.NotificationPanel); + this.setPhase(RightPanelPhases.NotificationPanel); } - renderButtons() { + public renderButtons() { return [ , , , ]; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 719a64063d..b52792b3d1 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,7 +40,7 @@ import E2EIcon from "../rooms/E2EIcon"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {textualPowerLevel} from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification'; @@ -550,7 +550,9 @@ const RedactMessagesButton = ({member}) => { let eventsToRedact = []; while (timeline) { eventsToRedact = timeline.getEvents().reduce((events, event) => { - if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) { + if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction() && + event.getType() !== "m.room.create" + ) { return events.concat(event); } else { return events; @@ -1480,7 +1482,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => { ; }; -const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.RoomMemberInfo, ...props}) => { +const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => { const cli = useContext(MatrixClientContext); // Load room if we are given a room id and memoize it @@ -1500,8 +1502,8 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room let content; switch (phase) { - case RIGHT_PANEL_PHASES.RoomMemberInfo: - case RIGHT_PANEL_PHASES.GroupMemberInfo: + case RightPanelPhases.RoomMemberInfo: + case RightPanelPhases.GroupMemberInfo: content = ( ); break; - case RIGHT_PANEL_PHASES.EncryptionPanel: + case RightPanelPhases.EncryptionPanel: classes.push("mx_UserInfo_smallAvatar"); content = ( diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.tsx similarity index 79% rename from src/components/views/right_panel/VerificationPanel.js rename to src/components/views/right_panel/VerificationPanel.tsx index 0b6790eac8..9cb7f54dd8 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -15,12 +15,15 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import * as sdk from '../../../index'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode"; +import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS"; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import {_t} from "../../../languageHandler"; @@ -36,37 +39,51 @@ import { } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import Spinner from "../elements/Spinner"; -export default class VerificationPanel extends React.PureComponent { - static propTypes = { - layout: PropTypes.string, - request: PropTypes.object.isRequired, - member: PropTypes.object.isRequired, - phase: PropTypes.oneOf([ - PHASE_UNSENT, - PHASE_REQUESTED, - PHASE_READY, - PHASE_STARTED, - PHASE_CANCELLED, - PHASE_DONE, - ]).isRequired, - onClose: PropTypes.func.isRequired, - isRoomEncrypted: PropTypes.bool, - }; +// XXX: Should be defined in matrix-js-sdk +enum VerificationPhase { + PHASE_UNSENT, + PHASE_REQUESTED, + PHASE_READY, + PHASE_DONE, + PHASE_STARTED, + PHASE_CANCELLED, +} - constructor(props) { +interface IProps { + layout: string; + request: VerificationRequest; + member: RoomMember; + phase: VerificationPhase; + onClose: () => void; + isRoomEncrypted: boolean; + inDialog: boolean; + key: number; +} + +interface IState { + sasEvent?: SAS; + emojiButtonClicked?: boolean; + reciprocateButtonClicked?: boolean; + reciprocateQREvent?: ReciprocateQRCode; +} + +export default class VerificationPanel extends React.PureComponent { + private hasVerifier: boolean; + + constructor(props: IProps) { super(props); this.state = {}; - this._hasVerifier = false; + this.hasVerifier = false; } - renderQRPhase() { + private renderQRPhase() { const {member, request} = this.props; - const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS); - const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD); + const showSAS: boolean = request.otherPartySupportsMethod(verificationMethods.SAS); + const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const brand = SdkConfig.get().brand; - const noCommonMethodError = !showSAS && !showQR ? + const noCommonMethodError: JSX.Element = !showSAS && !showQR ?

    {_t( "The session you are trying to verify doesn't support scanning a " + "QR code or emoji verification, which is what %(brand)s supports. Try " + @@ -77,41 +94,41 @@ export default class VerificationPanel extends React.PureComponent { if (this.props.layout === 'dialog') { // HACK: This is a terrible idea. - let qrBlock; - let sasBlock; + let qrBlockDialog: JSX.Element; + let sasBlockDialog: JSX.Element; if (showQR) { - qrBlock = + qrBlockDialog =

    {_t("Scan this unique code")}

    ; } if (showSAS) { - sasBlock = + sasBlockDialog =

    {_t("Compare unique emoji")}

    {_t("Compare a unique set of emoji if you don't have a camera on either device")} - + {_t("Start")}
    ; } - const or = qrBlock && sasBlock ? + const or = qrBlockDialog && sasBlockDialog ?
    {_t("or")}
    : null; return (
    {_t("Verify this session by completing one of the following:")}
    - {qrBlock} + {qrBlockDialog} {or} - {sasBlock} + {sasBlockDialog} {noCommonMethodError}
    ); } - let qrBlock; + let qrBlock: JSX.Element; if (showQR) { qrBlock =

    {_t("Verify by scanning")}

    @@ -125,7 +142,7 @@ export default class VerificationPanel extends React.PureComponent {
    ; } - let sasBlock; + let sasBlock: JSX.Element; if (showSAS) { const disabled = this.state.emojiButtonClicked; const sasLabel = showQR ? @@ -140,7 +157,7 @@ export default class VerificationPanel extends React.PureComponent { disabled={disabled} kind="primary" className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton" - onClick={this._startSAS} + onClick={this.startSAS} > {_t("Verify by emoji")}
    @@ -159,22 +176,22 @@ export default class VerificationPanel extends React.PureComponent { ; } - _onReciprocateYesClick = () => { + private onReciprocateYesClick = () => { this.setState({reciprocateButtonClicked: true}); this.state.reciprocateQREvent.confirm(); }; - _onReciprocateNoClick = () => { + private onReciprocateNoClick = () => { this.setState({reciprocateButtonClicked: true}); this.state.reciprocateQREvent.cancel(); }; - _getDevice() { + private getDevice() { const deviceId = this.props.request && this.props.request.channel.deviceId; return MatrixClientPeg.get().getStoredDevice(MatrixClientPeg.get().getUserId(), deviceId); } - renderQRReciprocatePhase() { + private renderQRReciprocatePhase() { const {member, request} = this.props; let Button; // a bit of a hack, but the FormButton should only be used in the right panel @@ -189,7 +206,7 @@ export default class VerificationPanel extends React.PureComponent { _t("Almost there! Is %(displayName)s showing the same shield?", { displayName: member.displayName || member.name || member.userId, }); - let body; + let body: JSX.Element; if (this.state.reciprocateQREvent) { // riot web doesn't support scanning yet, so assume here we're the client being scanned. // @@ -202,11 +219,11 @@ export default class VerificationPanel extends React.PureComponent { + onClick={this.onReciprocateNoClick}>{_t("No")} + onClick={this.onReciprocateYesClick}>{_t("Yes")} ; } else { @@ -218,10 +235,10 @@ export default class VerificationPanel extends React.PureComponent { ; } - renderVerifiedPhase() { + private renderVerifiedPhase() { const {member, request} = this.props; - let text; + let text: string; if (!request.isSelfVerification) { if (this.props.isRoomEncrypted) { text = _t("Verify all users in a room to ensure it's secure."); @@ -230,9 +247,9 @@ export default class VerificationPanel extends React.PureComponent { } } - let description; + let description: string; if (request.isSelfVerification) { - const device = this._getDevice(); + const device = this.getDevice(); if (!device) { // This can happen if the device is logged out while we're still showing verification // UI for it. @@ -264,19 +281,19 @@ export default class VerificationPanel extends React.PureComponent { ); } - renderCancelledPhase() { + private renderCancelledPhase() { const {member, request} = this.props; const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let startAgainInstruction; + let startAgainInstruction: string; if (request.isSelfVerification) { startAgainInstruction = _t("Start verification again from the notification."); } else { startAgainInstruction = _t("Start verification again from their profile."); } - let text; + let text: string; if (request.cancellationCode === "m.timeout") { text = _t("Verification timed out.") + ` ${startAgainInstruction}`; } else if (request.cancellingUserId === request.otherUserId) { @@ -304,7 +321,7 @@ export default class VerificationPanel extends React.PureComponent { ); } - render() { + public render() { const {member, phase, request} = this.props; const displayName = member.displayName || member.name || member.userId; @@ -321,10 +338,10 @@ export default class VerificationPanel extends React.PureComponent { const emojis = this.state.sasEvent ? : ; @@ -345,7 +362,7 @@ export default class VerificationPanel extends React.PureComponent { return null; } - _startSAS = async () => { + private startSAS = async () => { this.setState({emojiButtonClicked: true}); const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); try { @@ -355,31 +372,31 @@ export default class VerificationPanel extends React.PureComponent { } }; - _onSasMatchesClick = () => { + private onSasMatchesClick = () => { this.state.sasEvent.confirm(); }; - _onSasMismatchesClick = () => { + private onSasMismatchesClick = () => { this.state.sasEvent.mismatch(); }; - _updateVerifierState = () => { + private updateVerifierState = () => { const {request} = this.props; const {sasEvent, reciprocateQREvent} = request.verifier; - request.verifier.off('show_sas', this._updateVerifierState); - request.verifier.off('show_reciprocate_qr', this._updateVerifierState); + request.verifier.off('show_sas', this.updateVerifierState); + request.verifier.off('show_reciprocate_qr', this.updateVerifierState); this.setState({sasEvent, reciprocateQREvent}); }; - _onRequestChange = async () => { + private onRequestChange = async () => { const {request} = this.props; - const hadVerifier = this._hasVerifier; - this._hasVerifier = !!request.verifier; - if (!hadVerifier && this._hasVerifier) { - request.verifier.on('show_sas', this._updateVerifierState); - request.verifier.on('show_reciprocate_qr', this._updateVerifierState); + const hadVerifier = this.hasVerifier; + this.hasVerifier = !!request.verifier; + if (!hadVerifier && this.hasVerifier) { + request.verifier.on('show_sas', this.updateVerifierState); + request.verifier.on('show_reciprocate_qr', this.updateVerifierState); try { - // on the requester side, this is also awaited in _startSAS, + // on the requester side, this is also awaited in startSAS, // but that's ok as verify should return the same promise. await request.verifier.verify(); } catch (err) { @@ -388,23 +405,22 @@ export default class VerificationPanel extends React.PureComponent { } }; - componentDidMount() { + public componentDidMount() { const {request} = this.props; - request.on("change", this._onRequestChange); + request.on("change", this.onRequestChange); if (request.verifier) { - const {request} = this.props; const {sasEvent, reciprocateQREvent} = request.verifier; this.setState({sasEvent, reciprocateQREvent}); } - this._onRequestChange(); + this.onRequestChange(); } - componentWillUnmount() { + public componentWillUnmount() { const {request} = this.props; if (request.verifier) { - request.verifier.off('show_sas', this._updateVerifierState); - request.verifier.off('show_reciprocate_qr', this._updateVerifierState); + request.verifier.off('show_sas', this.updateVerifierState); + request.verifier.off('show_reciprocate_qr', this.updateVerifierState); } - request.off("change", this._onRequestChange); + request.off("change", this.onRequestChange); } } diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index 1d26e956ab..4e8b6ba42f 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -20,7 +20,8 @@ import createReactClass from 'create-react-class'; import Tinter from '../../../Tinter'; import dis from '../../../dispatcher/dispatcher'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; const ROOM_COLORS = [ // magic room default values courtesy of Ribot diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 51f6954975..fa5c1baf8f 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -22,10 +22,11 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from "../../../index"; import { _t, _td } from '../../../languageHandler'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import dis from "../../../dispatcher/dispatcher"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {Action} from "../../../dispatcher/actions"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default createReactClass({ diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f6eaf5f003..13554eb3d6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -685,6 +685,9 @@ export default createReactClass({ mx_EventTile_emote: msgtype === 'm.emote', }); + // If the tile is in the Sending state, don't speak the message. + const ariaLive = (this.props.eventSendStatus !== null) ? 'off' : undefined; + let permalink = "#"; if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); @@ -819,7 +822,7 @@ export default createReactClass({ case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return ( -
    +
    { room ? room.name : '' } @@ -845,7 +848,7 @@ export default createReactClass({ } case 'file_grid': { return ( -
    +
    +
    { ircTimestamp } { avatar } { sender } @@ -911,7 +914,7 @@ export default createReactClass({ // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
    +
    { ircTimestamp }
    { readAvatars } diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index d215df9126..ab6ec3fbed 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -72,6 +72,7 @@ export default class NotificationBadge extends React.PureComponent) { diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index e7cd2b4c0d..de70338245 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -73,7 +73,7 @@ export default class ReplyPreview extends React.Component { return
    - { '💬 ' + _t('Replying') } + { _t('Replying') }
    void; @@ -50,7 +51,6 @@ interface IProps { onResize: () => void; resizeNotifier: ResizeNotifier; collapsed: boolean; - searchFilter: string; isMinimized: boolean; } @@ -80,7 +80,7 @@ interface ITagAesthetics { sectionLabel: string; sectionLabelRaw?: string; addRoomLabel?: string; - onAddRoom?: (dispatcher: Dispatcher) => void; + onAddRoom?: (dispatcher?: Dispatcher) => void; isInvite: boolean; defaultHidden: boolean; } @@ -104,14 +104,18 @@ const TAG_AESTHETICS: { isInvite: false, defaultHidden: false, addRoomLabel: _td("Start chat"), - onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_chat'}), + onAddRoom: (dispatcher?: Dispatcher) => { + (dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'}); + }, }, [DefaultTagID.Untagged]: { sectionLabel: _td("Rooms"), isInvite: false, defaultHidden: false, addRoomLabel: _td("Create room"), - onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_room'}), + onAddRoom: (dispatcher?: Dispatcher) => { + (dispatcher || defaultDispatcher).dispatch({action: 'view_create_room'}) + }, }, [DefaultTagID.LowPriority]: { sectionLabel: _td("Low priority"), @@ -144,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics { }; } -export default class RoomList extends React.Component { - private searchFilter: NameFilterCondition = new NameFilterCondition(); +export default class RoomList extends React.PureComponent { private dispatcherRef; private customTagStoreRef; @@ -159,21 +162,6 @@ export default class RoomList extends React.Component { this.dispatcherRef = defaultDispatcher.register(this.onAction); } - public componentDidUpdate(prevProps: Readonly): void { - if (prevProps.searchFilter !== this.props.searchFilter) { - const hadSearch = !!this.searchFilter.search.trim(); - const haveSearch = !!this.props.searchFilter.trim(); - this.searchFilter.search = this.props.searchFilter; - if (!hadSearch && haveSearch) { - // started a new filter - add the condition - RoomListStore.instance.addFilter(this.searchFilter); - } else if (hadSearch && !haveSearch) { - // cleared a filter - remove the condition - RoomListStore.instance.removeFilter(this.searchFilter); - } // else the filter hasn't changed enough for us to care here - } - } - public componentDidMount(): void { RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); @@ -231,17 +219,46 @@ export default class RoomList extends React.Component { console.log("new lists", newLists); } - this.setState({sublists: newLists}, () => { - this.props.onResize(); + const previousListIds = Object.keys(this.state.sublists); + const newListIds = Object.keys(newLists).filter(t => { + if (!isCustomTag(t)) return true; // always include non-custom tags + + // if the tag is custom though, only include it if it is enabled + return CustomRoomTagStore.getTags()[t]; }); + + let doUpdate = arrayHasDiff(previousListIds, newListIds); + if (!doUpdate) { + // so we didn't have the visible sublists change, but did the contents of those + // sublists change significantly enough to break the sticky headers? Probably, so + // let's check the length of each. + for (const tagId of newListIds) { + const oldRooms = this.state.sublists[tagId]; + const newRooms = newLists[tagId]; + if (oldRooms.length !== newRooms.length) { + doUpdate = true; + break; + } + } + } + + if (doUpdate) { + // We have to break our reference to the room list store if we want to be able to + // diff the object for changes, so do that. + const newSublists = objectWithOnly(newLists, newListIds); + const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); + + this.setState({sublists}, () => { + this.props.onResize(); + }); + } }; - private renderCommunityInvites(): React.ReactElement[] { + private renderCommunityInvites(): TemporaryTile[] { // TODO: Put community invites in a more sensible place (not in the room list) // See https://github.com/vector-im/riot-web/issues/14456 return MatrixClientPeg.get().getGroups().filter(g => { - if (g.myMembership !== 'invite') return false; - return !this.searchFilter || this.searchFilter.matches(g.name || ""); + return g.myMembership === 'invite'; }).map(g => { const avatar = ( { const tagOrder = TAG_ORDER.reduce((p, c) => { if (c === CUSTOM_TAGS_BEFORE_TAG) { const customTags = Object.keys(this.state.sublists) - .filter(t => isCustomTag(t)) - .filter(t => CustomRoomTagStore.getTags()[t]); // isSelected + .filter(t => isCustomTag(t)); p.push(...customTags); } p.push(c); @@ -298,21 +314,18 @@ export default class RoomList extends React.Component { : TAG_AESTHETICS[orderedTagId]; if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); - const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; components.push( ); } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 5c181ec3c4..9aa78fbbfd 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -288,7 +288,6 @@ export default createReactClass({ const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let showSpinner = false; - let darkStyle = false; let title; let subTitle; let primaryActionHandler; @@ -316,7 +315,6 @@ export default createReactClass({ break; } case MessageCase.NotLoggedIn: { - darkStyle = true; title = _t("Join the conversation with an account"); primaryActionLabel = _t("Sign Up"); primaryActionHandler = this.onRegisterClick; @@ -557,7 +555,6 @@ export default createReactClass({ const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, { "mx_RoomPreviewBar_panel": this.props.canPreview, "mx_RoomPreviewBar_dialog": !this.props.canPreview, - "mx_RoomPreviewBar_dark": darkStyle, }); return ( diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index a29467b07f..859df6dd1b 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -21,7 +21,8 @@ import * as sdk from "../../../index"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default class RoomRecoveryReminder extends React.PureComponent { static propTypes = { diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 18bf56e9ba..f6d0d1c22e 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -32,13 +32,12 @@ import { StyledMenuItemCheckbox, StyledMenuItemRadio, } from "../../structures/ContextMenu"; -import RoomListStore from "../../../stores/room-list/RoomListStore"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import dis from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import NotificationBadge from "./NotificationBadge"; -import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { Key } from "../../../Keyboard"; import { ActionPayload } from "../../../dispatcher/payloads"; @@ -47,6 +46,10 @@ import { Direction } from "re-resizable/lib/resizer"; import { polyfillTouchEvent } from "../../../@types/polyfill"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; +import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; +import { objectExcluding, objectHasDiff } from "../../../utils/objects"; +import TemporaryTile from "./TemporaryTile"; +import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS @@ -59,7 +62,6 @@ polyfillTouchEvent(); interface IProps { forRooms: boolean; - rooms?: Room[]; startAsHidden: boolean; label: string; onAddRoom?: () => void; @@ -67,11 +69,10 @@ interface IProps { isMinimized: boolean; tagId: TagID; onResize: () => void; - isFiltered: boolean; // TODO: Don't use this. It's for community invites, and community invites shouldn't be here. // You should feel bad if you use this. - extraBadTilesThatShouldntExist?: React.ReactElement[]; + extraBadTilesThatShouldntExist?: TemporaryTile[]; // TODO: Account for https://github.com/vector-im/riot-web/issues/14179 } @@ -85,11 +86,12 @@ interface ResizeDelta { type PartialDOMRect = Pick; interface IState { - notificationState: ListNotificationState; contextMenuPosition: PartialDOMRect; isResizing: boolean; isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered height: number; + rooms: Room[]; + filteredExtraTiles?: TemporaryTile[]; } export default class RoomSublist extends React.Component { @@ -98,22 +100,27 @@ export default class RoomSublist extends React.Component { private dispatcherRef: string; private layout: ListLayout; private heightAtStart: number; + private isBeingFiltered: boolean; + private notificationState: ListNotificationState; constructor(props: IProps) { super(props); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); this.heightAtStart = 0; - const height = this.calculateInitialHeight(); + this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition(); + this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId); this.state = { - notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId), contextMenuPosition: null, isResizing: false, - isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed, - height, + isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed, + height: 0, // to be fixed in a moment, we need `rooms` to calculate this. + rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []), }; - this.state.notificationState.setRooms(this.props.rooms); + // Why Object.assign() and not this.state.height? Because TypeScript says no. + this.state = Object.assign(this.state, {height: this.calculateInitialHeight()}); this.dispatcherRef = defaultDispatcher.register(this.onAction); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); } private calculateInitialHeight() { @@ -141,12 +148,22 @@ export default class RoomSublist extends React.Component { return padding; } - private get numTiles(): number { - return RoomSublist.calcNumTiles(this.props); + private get extraTiles(): TemporaryTile[] | null { + if (this.state.filteredExtraTiles) { + return this.state.filteredExtraTiles; + } + if (this.props.extraBadTilesThatShouldntExist) { + return this.props.extraBadTilesThatShouldntExist; + } + return null; } - private static calcNumTiles(props) { - return (props.rooms || []).length + (props.extraBadTilesThatShouldntExist || []).length; + private get numTiles(): number { + return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles); + } + + private static calcNumTiles(rooms: Room[], extraTiles: any[]) { + return (rooms || []).length + (extraTiles || []).length; } private get numVisibleTiles(): number { @@ -154,33 +171,116 @@ export default class RoomSublist extends React.Component { return Math.min(nVisible, this.numTiles); } - public componentDidUpdate(prevProps: Readonly) { - this.state.notificationState.setRooms(this.props.rooms); - if (prevProps.isFiltered !== this.props.isFiltered) { - if (this.props.isFiltered) { - this.setState({isExpanded: true}); - } else { - this.setState({isExpanded: !this.layout.isCollapsed}); - } - } + public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { + const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist; // as the rooms can come in one by one we need to reevaluate // the amount of available rooms to cap the amount of requested visible rooms by the layout - if (RoomSublist.calcNumTiles(prevProps) !== this.numTiles) { + if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) { this.setState({height: this.calculateInitialHeight()}); } } - public componentWillUnmount() { - this.state.notificationState.destroy(); - defaultDispatcher.unregister(this.dispatcherRef); + public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { + if (objectHasDiff(this.props, nextProps)) { + // Something we don't care to optimize has updated, so update. + return true; + } + + // Do the same check used on props for state, without the rooms we're going to no-op + const prevStateNoRooms = objectExcluding(this.state, ['rooms']); + const nextStateNoRooms = objectExcluding(nextState, ['rooms']); + if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) { + return true; + } + + // If we're supposed to handle extra tiles, take the performance hit and re-render all the + // time so we don't have to consider them as part of the visible room optimization. + const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || []; + const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || []; + if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) { + return true; + } + + // If we're about to update the height of the list, we don't really care about which rooms + // are visible or not for no-op purposes, so ensure that the height calculation runs through. + if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) { + return true; + } + + // Before we go analyzing the rooms, we can see if we're collapsed. If we're collapsed, we don't need + // to render anything. We do this after the height check though to ensure that the height gets appropriately + // calculated for when/if we become uncollapsed. + if (!nextState.isExpanded) { + return false; + } + + // Quickly double check we're not about to break something due to the number of rooms changing. + if (this.state.rooms.length !== nextState.rooms.length) { + return true; + } + + // Finally, determine if the room update (as presumably that's all that's left) is within + // our visible range. If it is, then do a render. If the update is outside our visible range + // then we can skip the update. + // + // We also optimize for order changing here: if the update did happen in our visible range + // but doesn't result in the list re-sorting itself then there's no reason for us to update + // on our own. + const prevSlicedRooms = this.state.rooms.slice(0, this.numVisibleTiles); + const nextSlicedRooms = nextState.rooms.slice(0, this.numVisibleTiles); + if (arrayHasOrderChange(prevSlicedRooms, nextSlicedRooms)) { + return true; + } + + // Finally, nothing happened so no-op the update + return false; } + public componentWillUnmount() { + defaultDispatcher.unregister(this.dispatcherRef); + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); + } + + private onListsUpdated = () => { + const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer + + if (this.props.extraBadTilesThatShouldntExist) { + const nameCondition = RoomListStore.instance.getFirstNameFilterCondition(); + if (nameCondition) { + stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist + .filter(t => nameCondition.matches(t.props.displayName || "")); + } else if (this.state.filteredExtraTiles) { + stateUpdates.filteredExtraTiles = null; + } + } + + const currentRooms = this.state.rooms; + const newRooms = arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []); + if (arrayHasOrderChange(currentRooms, newRooms)) { + stateUpdates.rooms = newRooms; + } + + const isStillBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition(); + if (isStillBeingFiltered !== this.isBeingFiltered) { + this.isBeingFiltered = isStillBeingFiltered; + if (isStillBeingFiltered) { + stateUpdates.isExpanded = true; + } else { + stateUpdates.isExpanded = !this.layout.isCollapsed; + } + } + + if (Object.keys(stateUpdates).length > 0) { + this.setState(stateUpdates); + } + }; + private onAction = (payload: ActionPayload) => { - if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) { + if (payload.action === "view_room" && payload.show_room_tile && this.state.rooms) { // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change // where we lose the room we are changing from temporarily and then it comes back in an update right after. setImmediate(() => { - const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id); + const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id); if (!this.state.isExpanded && roomIndex > -1) { this.toggleCollapsed(); @@ -302,12 +402,12 @@ export default class RoomSublist extends React.Component { let room; if (this.props.tagId === DefaultTagID.Invite) { // switch to first room as that'll be the top of the list for the user - room = this.props.rooms && this.props.rooms[0]; + room = this.state.rooms && this.state.rooms[0]; } else { // find the first room with a count of the same colour as the badge count - room = this.props.rooms.find((r: Room) => { - const notifState = this.state.notificationState.getForRoom(r); - return notifState.count > 0 && notifState.color === this.state.notificationState.color; + room = this.state.rooms.find((r: Room) => { + const notifState = this.notificationState.getForRoom(r); + return notifState.count > 0 && notifState.color === this.notificationState.color; }); } @@ -399,8 +499,8 @@ export default class RoomSublist extends React.Component { const tiles: React.ReactElement[] = []; - if (this.props.rooms) { - const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); + if (this.state.rooms) { + const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles); for (const room of visibleRooms) { tiles.push( { } } - if (this.props.extraBadTilesThatShouldntExist) { - tiles.push(...this.props.extraBadTilesThatShouldntExist); + if (this.extraTiles) { + // HACK: We break typing here, but this 'extra tiles' property shouldn't exist. + (tiles as any[]).push(...this.extraTiles); } // We only have to do this because of the extra tiles. We do it conditionally @@ -522,7 +623,7 @@ export default class RoomSublist extends React.Component { const badge = ( ; interface IState { - hover: boolean; - notificationState: NotificationState; selected: boolean; notificationsMenuPosition: PartialDOMRect; generalMenuPosition: PartialDOMRect; + messagePreview?: string; } const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`; @@ -111,25 +104,42 @@ const NotifOption: React.FC = ({active, onClick, iconClassNam ); }; -export default class RoomTile extends React.Component { +export default class RoomTile extends React.PureComponent { private dispatcherRef: string; private roomTileRef = createRef(); + private notificationState: NotificationState; + private roomProps: RoomEchoChamber; constructor(props: IProps) { super(props); this.state = { - hover: false, - notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, notificationsMenuPosition: null, generalMenuPosition: null, + + // generatePreview() will return nothing if the user has previews disabled + messagePreview: this.generatePreview(), }; ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); this.dispatcherRef = defaultDispatcher.register(this.onAction); + MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged); + this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); + this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + this.roomProps = EchoChamber.forRoom(this.props.room); + this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate); } + private onNotificationUpdate = () => { + this.forceUpdate(); // notification state changed - update + }; + + private onRoomPropertyUpdate = (property: CachedRoomKey) => { + if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate(); + // else ignore - not important for this tile + }; + private get showContextMenu(): boolean { return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite; } @@ -150,6 +160,8 @@ export default class RoomTile extends React.Component { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); } defaultDispatcher.unregister(this.dispatcherRef); + MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged); + this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); } private onAction = (payload: ActionPayload) => { @@ -160,6 +172,21 @@ export default class RoomTile extends React.Component { } }; + private onRoomPreviewChanged = (room: Room) => { + if (this.props.room && room.roomId === this.props.room.roomId) { + // generatePreview() will return nothing if the user has previews disabled + this.setState({messagePreview: this.generatePreview()}); + } + }; + + private generatePreview(): string | null { + if (!this.showMessagePreview) { + return null; + } + + return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + } + private scrollIntoView = () => { if (!this.roomTileRef.current) return; this.roomTileRef.current.scrollIntoView({ @@ -168,14 +195,6 @@ export default class RoomTile extends React.Component { }); }; - private onTileMouseEnter = () => { - this.setState({hover: true}); - }; - - private onTileMouseLeave = () => { - this.setState({hover: false}); - }; - private onTileClick = (ev: React.KeyboardEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -292,17 +311,9 @@ export default class RoomTile extends React.Component { ev.stopPropagation(); if (MatrixClientPeg.get().isGuest()) return; - // get key before we go async and React discards the nativeEvent - const key = (ev as React.KeyboardEvent).key; - try { - // TODO add local echo - https://github.com/vector-im/riot-web/issues/14280 - await setRoomNotifsState(this.props.room.roomId, newState); - } catch (error) { - // TODO: some form of error notification to the user to inform them that their state change failed. - // See https://github.com/vector-im/riot-web/issues/14281 - console.error(error); - } + this.roomProps.notificationVolume = newState; + const key = (ev as React.KeyboardEvent).key; if (key === Key.ENTER) { // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 this.setState({notificationsMenuPosition: null}); // hide the menu @@ -320,7 +331,7 @@ export default class RoomTile extends React.Component { return null; } - const state = getRoomNotifsState(this.props.room.roomId); + const state = this.roomProps.notificationVolume; let contextMenu = null; if (this.state.notificationsMenuPosition) { @@ -482,7 +493,7 @@ export default class RoomTile extends React.Component { badge = (