diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 0b8fcfa5c82..999f19c2589 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -52,6 +52,8 @@ jobs: - "--noImplicitAny" steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Install Deps run: "scripts/ci/layered.sh" diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd402f79ee..3b8428ec020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28) +===================================================================================================== + +## ✨ Features + * Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner. + * Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald. + * Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt. + * Poll history: detail screen ([\#10172](https://github.com/matrix-org/matrix-react-sdk/pull/10172)). Contributed by @kerryarchibald. + * Provide a more detailed error message than "No known servers" ([\#6048](https://github.com/matrix-org/matrix-react-sdk/pull/6048)). Fixes vector-im/element-web#13247. Contributed by @aaronraimist. + * Say when a call was answered from a different device ([\#10224](https://github.com/matrix-org/matrix-react-sdk/pull/10224)). + * Widget permissions customizations using module api ([\#10121](https://github.com/matrix-org/matrix-react-sdk/pull/10121)). Contributed by @maheichyk. + * Fix copy button icon overlapping with copyable text ([\#10227](https://github.com/matrix-org/matrix-react-sdk/pull/10227)). Contributed by @Adesh-Pandey. + * Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk. + * The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)). + * Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)). + * Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros. + * Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald. + * Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald. + * Poll history - ended polls list items ([\#10119](https://github.com/matrix-org/matrix-react-sdk/pull/10119)). Contributed by @kerryarchibald. + * Remove threads labs flag and the ability to disable threads ([\#9878](https://github.com/matrix-org/matrix-react-sdk/pull/9878)). Fixes vector-im/element-web#24365. + * Show a success dialog after setting up the key backup ([\#10177](https://github.com/matrix-org/matrix-react-sdk/pull/10177)). Fixes vector-im/element-web#24487. + * Release Sign in with QR out of labs ([\#10066](https://github.com/matrix-org/matrix-react-sdk/pull/10066)). Contributed by @hughns. + * Hide indent button in rte ([\#10149](https://github.com/matrix-org/matrix-react-sdk/pull/10149)). Contributed by @alunturner. + * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). + * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. + +## 🐛 Bug Fixes + * Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)). + * Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)). + * Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33. + * Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009. + * Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul. + * Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt. + * Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald. + * Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne. + * Re-focus the composer on dialogue quit ([\#10007](https://github.com/matrix-org/matrix-react-sdk/pull/10007)). Fixes vector-im/element-web#22832. Contributed by @Ashu999. + * Try to resolve emails before creating a DM ([\#10164](https://github.com/matrix-org/matrix-react-sdk/pull/10164)). + * Disable poll response loading test ([\#10168](https://github.com/matrix-org/matrix-react-sdk/pull/10168)). Contributed by @justjanne. + * Fix email lookup in invite dialog ([\#10150](https://github.com/matrix-org/matrix-react-sdk/pull/10150)). Fixes vector-im/element-web#23353. + * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. + * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. + * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. + * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. + * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. + * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. + * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. + Changes in [3.66.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0) (2023-02-14) ===================================================================================================== diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 306e94cb97d..7ffd290862a 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -183,6 +183,10 @@ describe("Cryptography", function () { cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click(); cy.contains(".mx_Dialog_title", "Setting up keys").should("exist"); cy.contains(".mx_Dialog_title", "Setting up keys").should("not.exist"); + + cy.contains("Secure Backup successful").should("exist"); + cy.contains("Done").click(); + cy.contains("Secure Backup successful").should("not.exist"); }); return; }); diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index b9e3265b767..13e3c56abab 100644 --- a/cypress/e2e/crypto/decryption-failure.spec.ts +++ b/cypress/e2e/crypto/decryption-failure.spec.ts @@ -181,12 +181,14 @@ describe("Decryption Failure Bar", () => { cy.contains(".mx_DecryptionFailureBar_button", "Reset").click(); + // Set up key backup cy.get(".mx_Dialog").within(() => { cy.contains(".mx_Dialog_primary", "Continue").click(); cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey"); // Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851 cy.contains(".mx_AccessibleButton", "Download").click(); cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click(); + cy.contains("Done").click(); }); cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should( diff --git a/cypress/e2e/location/location.spec.ts b/cypress/e2e/location/location.spec.ts index 0d512705a08..b716fe543b4 100644 --- a/cypress/e2e/location/location.spec.ts +++ b/cypress/e2e/location/location.spec.ts @@ -27,7 +27,7 @@ describe("Location sharing", () => { }; const submitShareLocation = (): void => { - cy.get('[data-test-id="location-picker-submit-button"]').click(); + cy.get('[data-testid="location-picker-submit-button"]').click(); }; beforeEach(() => { diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index 51d169d61bd..07a14533c70 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 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. @@ -54,12 +54,12 @@ describe("Polls", () => { }; const getPollOption = (pollId: string, optionText: string): Chainable => { - return getPollTile(pollId).contains(".mx_MPollBody_option .mx_StyledRadioButton", optionText); + return getPollTile(pollId).contains(".mx_PollOption .mx_StyledRadioButton", optionText); }; const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => { getPollOption(pollId, optionText).within(() => { - cy.get(".mx_MPollBody_optionVoteCount").should("contain", `${votes} vote`); + cy.get(".mx_PollOption_optionVoteCount").should("contain", `${votes} vote`); }); }; @@ -83,7 +83,6 @@ describe("Polls", () => { }; beforeEach(() => { - cy.enableLabsFeature("feature_threadenabled"); cy.window().then((win) => { win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests }); diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 84778d4be76..d946ad34dac 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 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. @@ -19,17 +19,10 @@ limitations under the License. import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { MatrixClient } from "../../global"; -function markWindowBeforeReload(): void { - // mark our window object to "know" when it gets reloaded - cy.window().then((w) => (w.beforeReload = true)); -} - describe("Threads", () => { let homeserver: HomeserverInstance; beforeEach(() => { - // Default threads to ON for this spec - cy.enableLabsFeature("feature_threadenabled"); cy.window().then((win) => { win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests }); @@ -44,35 +37,6 @@ describe("Threads", () => { cy.stopHomeserver(homeserver); }); - it("should reload when enabling threads beta", () => { - markWindowBeforeReload(); - - // Turn off - cy.openUserSettings("Labs").within(() => { - // initially the new property is there - cy.window().should("have.prop", "beforeReload", true); - - cy.leaveBeta("Threaded messages"); - cy.wait(1000); - // after reload the property should be gone - cy.window().should("not.have.prop", "beforeReload"); - }); - - cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app - markWindowBeforeReload(); - - // Turn on - cy.openUserSettings("Labs").within(() => { - // initially the new property is there - cy.window().should("have.prop", "beforeReload", true); - - cy.joinBeta("Threaded messages"); - cy.wait(1000); - // after reload the property should be gone - cy.window().should("not.have.prop", "beforeReload"); - }); - }); - it("should be usable for a conversation", () => { let bot: MatrixClient; cy.getBot(homeserver, { diff --git a/package.json b/package.json index 2c717e99f1d..3642474ee7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.66.0", + "version": "3.67.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.4.0", - "@matrix-org/matrix-wysiwyg": "^0.23.0", + "@matrix-org/matrix-wysiwyg": "^1.1.1", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", @@ -93,7 +93,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "23.3.0", + "matrix-js-sdk": "23.4.0", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -112,7 +112,7 @@ "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "^2.3.2", + "sanitize-html": "2.8.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", "url": "^0.11.0", @@ -153,22 +153,24 @@ "@types/escape-html": "^1.0.1", "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", + "@types/glob-to-regexp": "^0.4.1", "@types/jest": "^29.2.1", - "@types/katex": "^0.14.0", + "@types/katex": "^0.16.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^16", + "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "17.0.49", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "17.0.17", - "@types/react-test-renderer": "^17.0.1", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "^2.3.1", + "@types/sanitize-html": "2.8.0", + "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^5.35.1", @@ -210,7 +212,6 @@ "postcss-scss": "^4.0.4", "prettier": "2.8.0", "raw-loader": "^4.0.2", - "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.4", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 671de3ed868..f1f15287d5e 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -18,7 +18,9 @@ @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss"; +@import "./components/views/dialogs/polls/_PollListItemEnded.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; +@import "./components/views/elements/_FilterTabGroup.pcss"; @import "./components/views/elements/_LearnMore.pcss"; @import "./components/views/location/_EnableLiveShare.pcss"; @import "./components/views/location/_LiveDurationDropdown.pcss"; @@ -32,6 +34,7 @@ @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/pips/_WidgetPip.pcss"; +@import "./components/views/polls/_PollOption.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; @import "./components/views/settings/devices/_DeviceDetailHeading.pcss"; @import "./components/views/settings/devices/_DeviceDetails.pcss"; @@ -48,6 +51,7 @@ @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./components/views/typography/_Caption.pcss"; @import "./compound/_Icon.pcss"; +@import "./compound/_SuccessDialog.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @import "./structures/_AutocompleteInput.pcss"; @import "./structures/_BackdropPanel.pcss"; @@ -236,6 +240,7 @@ @import "./views/messages/_MLocationBody.pcss"; @import "./views/messages/_MNoticeBody.pcss"; @import "./views/messages/_MPollBody.pcss"; +@import "./views/messages/_MPollEndBody.pcss"; @import "./views/messages/_MStickerBody.pcss"; @import "./views/messages/_MTextBody.pcss"; @import "./views/messages/_MVideoBody.pcss"; diff --git a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss new file mode 100644 index 00000000000..6518052ab61 --- /dev/null +++ b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss @@ -0,0 +1,60 @@ +/* +Copyright 2023 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_PollListItemEnded { + width: 100%; + display: flex; + flex-direction: column; + color: $primary-content; +} + +.mx_PollListItemEnded_title { + display: grid; + justify-content: left; + align-items: center; + grid-gap: $spacing-8; + grid-template-columns: min-content 1fr min-content; + grid-template-rows: auto; +} + +.mx_PollListItemEnded_icon { + height: 14px; + width: 14px; + color: $quaternary-content; + padding-left: $spacing-8; +} + +.mx_PollListItemEnded_date { + font-size: $font-12px; + color: $secondary-content; +} + +.mx_PollListItemEnded_question { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mx_PollListItemEnded_answers { + display: grid; + grid-gap: $spacing-8; + margin-top: $spacing-12; +} + +.mx_PollListItemEnded_voteCount { + // 6px to match PollOption padding + margin: $spacing-8 0 0 6px; +} diff --git a/res/css/components/views/elements/_FilterTabGroup.pcss b/res/css/components/views/elements/_FilterTabGroup.pcss new file mode 100644 index 00000000000..bbf1a279ad4 --- /dev/null +++ b/res/css/components/views/elements/_FilterTabGroup.pcss @@ -0,0 +1,46 @@ +/* +Copyright 2023 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_FilterTabGroup { + color: $primary-content; + label { + margin-right: $spacing-12; + cursor: pointer; + span { + display: inline-block; + line-height: $font-24px; + } + } + input[type="radio"] { + appearance: none; + margin: 0; + padding: 0; + + &:focus, + &:hover { + & + span { + color: $secondary-content; + } + } + + &:checked + span { + color: $accent; + font-weight: $font-semi-bold; + // underline + box-shadow: 0 1.5px 0 0 currentColor; + } + } +} diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss new file mode 100644 index 00000000000..da4c66d6cf1 --- /dev/null +++ b/res/css/components/views/polls/_PollOption.pcss @@ -0,0 +1,108 @@ +/* +Copyright 2023 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_PollOption { + border: 1px solid $quinary-content; + border-radius: 8px; + padding: 6px 12px; + background-color: $background; + + .mx_StyledRadioButton_content, + .mx_PollOption_endedOption { + padding-top: 2px; + margin-right: 0px; + } + + .mx_StyledRadioButton_spacer { + display: none; + } +} + +.mx_PollOption, +/* label has cursor: default in user-agent stylesheet */ +/* override */ +.mx_PollOption_live-option { + cursor: pointer; +} + +.mx_PollOption_content { + display: flex; + justify-content: space-between; +} + +.mx_PollOption_optionVoteCount { + color: $secondary-content; + font-size: $font-12px; + white-space: nowrap; +} + +.mx_PollOption_winnerIcon { + height: 12px; + width: 12px; + color: $accent; + margin-right: $spacing-4; + vertical-align: middle; +} + +.mx_PollOption_checked { + border-color: $accent; + + .mx_PollOption_popularityBackground { + .mx_PollOption_popularityAmount { + background-color: $accent; + } + } + + // override checked radio button styling + // to show checkmark instead + .mx_StyledRadioButton_checked { + input[type="radio"] + div { + border-width: 2px; + border-color: $accent; + background-color: $accent; + background-image: url("$(res)/img/element-icons/check-white.svg"); + background-size: 12px; + background-repeat: no-repeat; + background-position: center; + + div { + visibility: hidden; + } + } + } +} + +/* options not actionable in these states */ +.mx_PollOption_checked, +.mx_PollOption_ended { + pointer-events: none; +} + +.mx_PollOption_popularityBackground { + width: 100%; + height: 8px; + margin-right: 12px; + border-radius: 8px; + background-color: $system; + margin-top: $spacing-8; + + .mx_PollOption_popularityAmount { + width: 0%; + height: 8px; + border-radius: 8px; + background-color: $quaternary-content; + } +} diff --git a/res/css/compound/_Icon.pcss b/res/css/compound/_Icon.pcss index 4a1d832675d..e12006a32e3 100644 --- a/res/css/compound/_Icon.pcss +++ b/res/css/compound/_Icon.pcss @@ -29,10 +29,22 @@ limitations under the License. color: $accent; } +.mx_Icon_bg-accent-light { + background-color: rgba($accent, 0.1); +} + .mx_Icon_alert { color: $alert; } +.mx_Icon_circle-40 { + border-radius: 20px; + flex: 0 0 40px; + height: 40px; + padding: 0 12px; + width: 40px; +} + .mx_Icon_8 { flex: 0 0 8px; height: 8px; diff --git a/res/css/compound/_SuccessDialog.pcss b/res/css/compound/_SuccessDialog.pcss new file mode 100644 index 00000000000..61f98a97df7 --- /dev/null +++ b/res/css/compound/_SuccessDialog.pcss @@ -0,0 +1,48 @@ +/* +Copyright 2023 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_SuccessDialog { + text-align: center; + + .mx_Icon { + mask-border: $spacing-16; + } + + .mx_Dialog_header { + margin: 0 0 $spacing-16; + padding: 0; + } + + .mx_Dialog_title { + margin: 0; + } + + .mx_Dialog_content { + color: $secondary-content; + margin: 0 0 $spacing-40; + } + + .mx_Dialog_buttons { + .mx_Dialog_buttons_row { + justify-content: center; + + button.mx_Dialog_primary { + height: 48px; + min-width: 328px; + } + } + } +} diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss index 40969bf0c2c..0c826bc5b1c 100644 --- a/res/css/structures/_RoomStatusBar.pcss +++ b/res/css/structures/_RoomStatusBar.pcss @@ -115,6 +115,7 @@ limitations under the License. padding-left: 30px; /* 18px for the icon, 2px margin to text, 10px regular padding */ display: inline-block; position: relative; + user-select: none; &:nth-child(2) { border-left: 1px solid $resend-button-divider-color; diff --git a/res/css/views/dialogs/polls/_PollHistoryList.pcss b/res/css/views/dialogs/polls/_PollHistoryList.pcss index 6a0a003ce1e..ee6f0254f71 100644 --- a/res/css/views/dialogs/polls/_PollHistoryList.pcss +++ b/res/css/views/dialogs/polls/_PollHistoryList.pcss @@ -32,6 +32,10 @@ limitations under the License. grid-gap: $spacing-20; padding-right: $spacing-64; margin: $spacing-32 0; + + &.mx_PollHistoryList_list_ENDED { + grid-gap: $spacing-32; + } } .mx_PollHistoryList_noResults { @@ -42,3 +46,14 @@ limitations under the License. justify-content: center; color: $secondary-content; } + +.mx_PollHistoryList_loading { + color: $secondary-content; + text-align: center; + + // center in all free space + // when there are no results + &.mx_PollHistoryList_noResultsYet { + margin: auto auto; + } +} diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss index 2c624e835a2..5dc40898623 100644 --- a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss @@ -23,6 +23,14 @@ limitations under the License. /* never asked. */ width: 560px; + &.mx_SuccessDialog { + padding: 56px; /* 80px from design - 24px wrapper padding */ + + .mx_Dialog_title { + margin-bottom: $spacing-16; + } + } + .mx_SettingsFlag { display: flex; } diff --git a/res/css/views/elements/_CopyableText.pcss b/res/css/views/elements/_CopyableText.pcss index e6b3b1ebf92..8e1d3f3cfd7 100644 --- a/res/css/views/elements/_CopyableText.pcss +++ b/res/css/views/elements/_CopyableText.pcss @@ -38,9 +38,12 @@ limitations under the License. cursor: pointer; margin-left: 20px; display: block; + /* If the copy button is used within a scrollable div, make it stick to the right while scrolling */ + position: sticky; + right: 0; /* center to first line */ - position: relative; top: 0.15em; + background-color: $background; &::before { content: ""; diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index ed355be103c..e7f3118d571 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -47,109 +47,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/composer/poll.svg"); } - .mx_MPollBody_option { - border: 1px solid $quinary-content; - border-radius: 8px; - margin-bottom: 16px; - padding: 6px 12px; - max-width: 550px; - background-color: $background; - - .mx_StyledRadioButton, - .mx_MPollBody_endedOption { - margin-bottom: 8px; - } - - .mx_StyledRadioButton_content, - .mx_MPollBody_endedOption { - padding-top: 2px; - margin-right: 0px; - } - - .mx_StyledRadioButton_spacer { - display: none; - } - - .mx_MPollBody_optionDescription { - display: flex; - justify-content: space-between; - - .mx_MPollBody_optionVoteCount { - color: $secondary-content; - font-size: $font-12px; - white-space: nowrap; - margin-left: 8px; - } - } - - .mx_MPollBody_popularityBackground { - width: 100%; - height: 8px; - margin-right: 12px; - border-radius: 8px; - background-color: $system; - - .mx_MPollBody_popularityAmount { - width: 0%; - height: 8px; - border-radius: 8px; - background-color: $quaternary-content; - } - } - } - - .mx_MPollBody_option:last-child { - margin-bottom: 8px; - } - - .mx_MPollBody_option_checked { - border-color: $accent; - - .mx_MPollBody_popularityBackground { - .mx_MPollBody_popularityAmount { - background-color: $accent; - } - } - } - - /* options not actionable in these states */ - .mx_MPollBody_option_checked, - .mx_MPollBody_option_ended { - pointer-events: none; - } - - .mx_StyledRadioButton_checked, - .mx_MPollBody_endedOptionWinner { - input[type="radio"] + div { - border-width: 2px; - border-color: $accent; - background-color: $accent; - background-image: url("$(res)/img/element-icons/check-white.svg"); - background-size: 12px; - background-repeat: no-repeat; - background-position: center; - - div { - visibility: hidden; - } - } - } - - .mx_MPollBody_endedOptionWinner .mx_MPollBody_optionDescription .mx_MPollBody_optionVoteCount::before { - content: ""; - position: relative; - display: inline-block; - margin-right: 4px; - top: 2px; - height: 12px; - width: 12px; - background-color: $accent; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url("$(res)/img/element-icons/trophy.svg"); - } - .mx_MPollBody_totalVotes { display: flex; flex-direction: inline; @@ -169,9 +66,9 @@ limitations under the License. pointer-events: none; } -.mx_MPollBody_option, -/* label has cursor: default in user-agent stylesheet */ -/* override */ -.mx_MPollBody_live-option { - cursor: pointer; +.mx_MPollBody_allOptions { + display: grid; + grid-gap: $spacing-16; + margin-bottom: $spacing-8; + max-width: 550px; } diff --git a/res/css/views/messages/_MPollEndBody.pcss b/res/css/views/messages/_MPollEndBody.pcss new file mode 100644 index 00000000000..db302655043 --- /dev/null +++ b/res/css/views/messages/_MPollEndBody.pcss @@ -0,0 +1,22 @@ +/* +Copyright 2023 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_MPollEndBody_icon { + height: 14px; + margin-right: $spacing-8; + vertical-align: middle; + color: $secondary-content; +} diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss index c03de9f36ce..5a61bcd2dab 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss @@ -28,4 +28,9 @@ limitations under the License. margin-bottom: $spacing-16; } } + + /* prevent the access token from overflowing the text box */ + div .mx_CopyableText { + overflow: scroll; + } } diff --git a/res/img/element-icons/check.svg b/res/img/element-icons/check.svg new file mode 100644 index 00000000000..afbd40cf109 --- /dev/null +++ b/res/img/element-icons/check.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/res/img/element-icons/room/composer/poll.svg b/res/img/element-icons/room/composer/poll.svg index e843e36c70a..75e74fd60aa 100644 --- a/res/img/element-icons/room/composer/poll.svg +++ b/res/img/element-icons/room/composer/poll.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/res/img/element-icons/trophy.svg b/res/img/element-icons/trophy.svg index e392cb0a79c..99f4831b573 100644 --- a/res/img/element-icons/trophy.svg +++ b/res/img/element-icons/trophy.svg @@ -1,3 +1,3 @@ - + diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index 7121e7a36ce..544045c7ed4 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -11,6 +11,7 @@ set -ex scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk +[ -n "$JS_SDK_GITHUB_BASE_REF" ] && git fetch --depth 1 origin $JS_SDK_GITHUB_BASE_REF && git checkout $JS_SDK_GITHUB_BASE_REF yarn link yarn install --pure-lockfile $@ popd diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh index bb002bd3abf..cd3dc7442cb 100755 --- a/scripts/ci/layered.sh +++ b/scripts/ci/layered.sh @@ -16,6 +16,7 @@ set -ex # Set up the js-sdk first scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk +[ -n "$JS_SDK_GITHUB_BASE_REF" ] && git fetch --depth 1 origin $JS_SDK_GITHUB_BASE_REF && git checkout $JS_SDK_GITHUB_BASE_REF yarn link yarn install --pure-lockfile popd diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f1e72ca2fc4..9d3b64fd6af 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -218,7 +218,7 @@ declare global { processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & { parameterDescriptors?: AudioParamDescriptor[]; }, - ); + ): void; // eslint-disable-next-line no-var var grecaptcha: diff --git a/src/@types/opus-recorder.d.ts b/src/@types/opus-recorder.d.ts new file mode 100644 index 00000000000..a964278aa1d --- /dev/null +++ b/src/@types/opus-recorder.d.ts @@ -0,0 +1,65 @@ +/* +Copyright 2023 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. +*/ + +declare module "opus-recorder/dist/recorder.min.js" { + export default class Recorder { + public static isRecordingSupported(): boolean; + + public constructor(config: { + bufferLength?: number; + encoderApplication?: number; + encoderFrameSize?: number; + encoderPath?: string; + encoderSampleRate?: number; + encoderBitRate?: number; + maxFramesPerPage?: number; + mediaTrackConstraints?: boolean; + monitorGain?: number; + numberOfChannels?: number; + recordingGain?: number; + resampleQuality?: number; + streamPages?: boolean; + wavBitDepth?: number; + sourceNode?: MediaStreamAudioSourceNode; + encoderComplexity?: number; + }); + + public ondataavailable?(data: ArrayBuffer): void; + + public readonly encodedSamplePosition: number; + + public start(): Promise; + + public stop(): Promise; + + public close(): void; + } +} + +declare module "opus-recorder/dist/encoderWorker.min.js" { + const path: string; + export default path; +} + +declare module "opus-recorder/dist/waveWorker.min.js" { + const path: string; + export default path; +} + +declare module "opus-recorder/dist/decoderWorker.min.js" { + const path: string; + export default path; +} diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index b6ed0d57387..5d8d947854d 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; +import { IAuthData, IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "./MatrixClientPeg"; import Modal from "./Modal"; @@ -29,6 +29,12 @@ function getIdServerDomain(): string { return MatrixClientPeg.get().idBaseUrl.split("://")[1]; } +export type Binding = { + bind: boolean; + label: string; + errorTitle: string; +}; + /** * Allows a user to add a third party identifier to their homeserver and, * optionally, the identity servers. @@ -178,7 +184,7 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async checkEmailLinkClicked(): Promise { + public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> { try { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (this.bind) { @@ -220,16 +226,19 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("Add Email Address"), - matrixClient: MatrixClientPeg.get(), - authData: e.data, - makeRequest: this.makeAddThreepidOnlyRequest, - aestheticsForStagePhases: { - [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, - [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + const { finished } = Modal.createDialog<[boolean, IAuthData | Error | null]>( + InteractiveAuthDialog, + { + title: _t("Add Email Address"), + matrixClient: MatrixClientPeg.get(), + authData: e.data, + makeRequest: this.makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }, - }); + ); return finished; } } diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 226f5b692bc..e4e12bedfe5 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -42,10 +42,7 @@ interface IState { export default class AsyncWrapper extends React.Component { private unmounted = false; - public state = { - component: null, - error: null, - }; + public state: IState = {}; public componentDidMount(): void { // XXX: temporary logging to try to diagnose @@ -77,7 +74,7 @@ export default class AsyncWrapper extends React.Component { this.props.onFinished(false); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.component) { const Component = this.state.component; return ; diff --git a/src/Avatar.ts b/src/Avatar.ts index d02e4b8e281..5036b8f2561 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -138,7 +138,7 @@ export function getInitialLetter(name: string): string | undefined { } export function avatarUrlForRoom( - room: Room, + room: Room | null, width: number, height: number, resizeMethod?: ResizeMethod, diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 46f964995af..7280b6bb3d7 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -193,11 +193,11 @@ export default abstract class BasePlatform { public displayNotification( title: string, msg: string, - avatarUrl: string, + avatarUrl: string | null, room: Room, ev?: MatrixEvent, ): Notification { - const notifBody = { + const notifBody: NotificationOptions = { body: msg, silent: true, // we play our own sounds }; diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 85ca90067d5..e05858bb166 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -89,7 +89,7 @@ async function loadImageElement(imageFile: File): Promise<{ // check for hi-dpi PNGs and fudge display resolution as needed. // this is mainly needed for macOS screencaps - let parsePromise: Promise; + let parsePromise = Promise.resolve(false); if (imageFile.type === "image/png") { // in practice macOS happens to order the chunks so they fall in // the first 0x1000 bytes (thanks to a massive ICC header). @@ -101,7 +101,7 @@ async function loadImageElement(imageFile: File): Promise<{ const chunks = extractPngChunks(buffer); for (const chunk of chunks) { if (chunk.name === "pHYs") { - if (chunk.data.byteLength !== PHYS_HIDPI.length) return; + if (chunk.data.byteLength !== PHYS_HIDPI.length) return false; return chunk.data.every((val, i) => val === PHYS_HIDPI[i]); } } @@ -199,10 +199,10 @@ function loadVideoElement(videoFile: File): Promise { reject(e); }; - let dataUrl = ev.target.result as string; + let dataUrl = ev.target?.result as string; // Chrome chokes on quicktime but likes mp4, and `file.type` is // read only, so do this horrible hack to unbreak quicktime - if (dataUrl.startsWith("data:video/quicktime;")) { + if (dataUrl?.startsWith("data:video/quicktime;")) { dataUrl = dataUrl.replace("data:video/quicktime;", "data:video/mp4;"); } @@ -258,7 +258,7 @@ function readFileAsArrayBuffer(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function (e): void { - resolve(e.target.result as ArrayBuffer); + resolve(e.target?.result as ArrayBuffer); }; reader.onerror = function (e): void { reject(e); @@ -329,7 +329,7 @@ export async function uploadFile( export default class ContentMessages { private inprogress: RoomUpload[] = []; - private mediaConfig: IMediaConfig = null; + private mediaConfig: IMediaConfig | null = null; public sendStickerContentToRoom( url: string, @@ -377,8 +377,8 @@ export default class ContentMessages { modal.close(); } - const tooBigFiles = []; - const okFiles = []; + const tooBigFiles: File[] = []; + const okFiles: File[] = []; for (const file of files) { if (this.isFileSizeAcceptable(file)) { @@ -420,7 +420,14 @@ export default class ContentMessages { } promBefore = doMaybeLocalRoomAction(roomId, (actualRoomId) => - this.sendContentToRoom(file, actualRoomId, relation, matrixClient, replyToEvent, loopPromiseBefore), + this.sendContentToRoom( + file, + actualRoomId, + relation, + matrixClient, + replyToEvent ?? undefined, + loopPromiseBefore, + ), ); } @@ -584,7 +591,7 @@ export default class ContentMessages { } private ensureMediaConfigFetched(matrixClient: MatrixClient): Promise { - if (this.mediaConfig !== null) return; + if (this.mediaConfig !== null) return Promise.resolve(); logger.log("[Media Config] Fetching"); return matrixClient diff --git a/src/DateUtils.ts b/src/DateUtils.ts index c279c1ad1b2..a6a8caa9468 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -175,7 +175,10 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { return prevDate.getFullYear() === nextDate.getFullYear(); } -export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean { +export function wantsDateSeparator( + prevEventDate: Date | null | undefined, + nextEventDate: Date | null | undefined, +): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 7329c665bc2..256fe245eef 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -138,7 +138,7 @@ export class DecryptionFailureTracker { return; } if (err) { - this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); + this.addDecryptionFailure(new DecryptionFailure(e.getId()!, err.code)); } else { // Could be an event in the failures, remove it this.removeDecryptionFailuresForEvent(e); @@ -146,7 +146,7 @@ export class DecryptionFailureTracker { } public addVisibleEvent(e: MatrixEvent): void { - const eventId = e.getId(); + const eventId = e.getId()!; if (this.trackedEvents.has(eventId)) { return; @@ -154,7 +154,7 @@ export class DecryptionFailureTracker { this.visibleEvents.add(eventId); if (this.failures.has(eventId) && !this.visibleFailures.has(eventId)) { - this.visibleFailures.set(eventId, this.failures.get(eventId)); + this.visibleFailures.set(eventId, this.failures.get(eventId)!); } } @@ -172,7 +172,7 @@ export class DecryptionFailureTracker { } public removeDecryptionFailuresForEvent(e: MatrixEvent): void { - const eventId = e.getId(); + const eventId = e.getId()!; this.failures.delete(eventId); this.visibleFailures.delete(eventId); } @@ -193,8 +193,8 @@ export class DecryptionFailureTracker { * Clear state and stop checking for and tracking failures. */ public stop(): void { - clearInterval(this.checkInterval); - clearInterval(this.trackInterval); + if (this.checkInterval) clearInterval(this.checkInterval); + if (this.trackInterval) clearInterval(this.trackInterval); this.failures = new Map(); this.visibleEvents = new Set(); diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index eccfc1c0e3e..039adc27cd1 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -51,7 +51,7 @@ import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulk const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; export default class DeviceListener { - private dispatcherRef: string; + private dispatcherRef: string | null; // device IDs for which the user has dismissed the verify toast ('Later') private dismissed = new Set(); // has the user dismissed any of the various nag toasts to setup encryption on this device? @@ -152,7 +152,7 @@ export default class DeviceListener { private ensureDeviceIdsAtStartPopulated(): void { if (this.ourDeviceIdsAtStart === null) { const cli = MatrixClientPeg.get(); - this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()).map((d) => d.deviceId)); + this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()!).map((d) => d.deviceId)); } } @@ -162,7 +162,7 @@ export default class DeviceListener { // devicesAtStart list to the devices that we see after the fetch. if (initialFetch) return; - const myUserId = MatrixClientPeg.get().getUserId(); + const myUserId = MatrixClientPeg.get().getUserId()!; if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated(); // No need to do a recheck here: we just need to get a snapshot of our devices @@ -170,7 +170,7 @@ export default class DeviceListener { }; private onDevicesUpdated = (users: string[]): void => { - if (!users.includes(MatrixClientPeg.get().getUserId())) return; + if (!users.includes(MatrixClientPeg.get().getUserId()!)) return; this.recheck(); }; @@ -225,7 +225,7 @@ export default class DeviceListener { // The server doesn't tell us when key backup is set up, so we poll // & cache the result - private async getKeyBackupInfo(): Promise { + private async getKeyBackupInfo(): Promise { const now = new Date().getTime(); if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); @@ -265,10 +265,10 @@ export default class DeviceListener { this.checkKeyBackupStatus(); } else if (this.shouldShowSetupEncryptionToast()) { // make sure our keys are finished downloading - await cli.downloadKeys([cli.getUserId()]); + await cli.downloadKeys([cli.getUserId()!]); // cross signing isn't enabled - nag to enable it // There are 3 different toasts for: - if (!cli.getCrossSigningId() && cli.getStoredCrossSigningForUser(cli.getUserId())) { + if (!cli.getCrossSigningId() && cli.getStoredCrossSigningForUser(cli.getUserId()!)) { // Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); @@ -310,13 +310,13 @@ export default class DeviceListener { // as long as cross-signing isn't ready, // you can't see or dismiss any device toasts if (crossSigningReady) { - const devices = cli.getStoredDevicesForUser(cli.getUserId()); + const devices = cli.getStoredDevicesForUser(cli.getUserId()!); for (const device of devices) { if (device.deviceId === cli.deviceId) continue; const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!); if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { - if (this.ourDeviceIdsAtStart.has(device.deviceId)) { + if (this.ourDeviceIdsAtStart?.has(device.deviceId)) { oldUnverifiedDeviceIds.add(device.deviceId); } else { newUnverifiedDeviceIds.add(device.deviceId); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index cf7cb285394..b41f39cec3b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -204,7 +204,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { attribs.style += "height: 100%;"; } - attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height); + attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!; return { tagName, attribs }; }, "code": function (tagName: string, attribs: sanitizeHtml.Attributes) { @@ -228,7 +228,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS // equivalents - const customCSSMapper = { + const customCSSMapper: Record = { "data-mx-color": "color", "data-mx-bg-color": "background-color", // $customAttributeKey: $cssAttributeKey @@ -352,7 +352,7 @@ const topicSanitizeHtmlParams: IExtendedSanitizeOptions = { }; abstract class BaseHighlighter { - public constructor(public highlightClass: string, public highlightLink: string) {} + public constructor(public highlightClass: string, public highlightLink?: string) {} /** * apply the highlights to a section of text @@ -504,7 +504,7 @@ function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | s export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnString): string; export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnNode): ReactNode; export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}): ReactNode | string { - const isFormattedBody = content.format === "org.matrix.custom.html" && !!content.formatted_body; + const isFormattedBody = content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string"; let bodyHasEmoji = false; let isHtmlMessage = false; @@ -514,7 +514,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } let strippedBody: string; - let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext + let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext let isAllHtmlEmoji = false; try { @@ -530,7 +530,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody); strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; - bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody : plainBody); + bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody! : plainBody); const highlighter = safeHighlights?.length ? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink) @@ -544,11 +544,11 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function (safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join(""); + return highlighter.applyHighlights(safeText, safeHighlights!).join(""); }; } - safeBody = sanitizeHtml(formattedBody, sanitizeParams); + safeBody = sanitizeHtml(formattedBody!, sanitizeParams); const phtml = cheerio.load(safeBody, { // @ts-ignore: The `_useHtmlParser2` internal option is the // simplest way to both parse and render using `htmlparser2`. @@ -594,7 +594,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op safeBody = formatEmojis(safeBody, true).join(""); } } else if (highlighter) { - safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join(""); + safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join(""); } } finally { delete sanitizeParams.textFilter; @@ -616,7 +616,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op contentBodyTrimmed = contentBodyTrimmed.replace(EMOJI_SEPARATOR_REGEX, ""); const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed); - const matched = match && match[0] && match[0].length === contentBodyTrimmed.length; + const matched = match?.[0]?.length === contentBodyTrimmed.length; emojiBody = (matched || isAllHtmlEmoji) && (strippedBody === safeBody || // replies have the html fallbacks, account for that here @@ -630,7 +630,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "markdown-body": isHtmlMessage && !emojiBody, }); - let emojiBodyElements: JSX.Element[]; + let emojiBodyElements: JSX.Element[] | undefined; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } @@ -659,7 +659,7 @@ export function topicToHtml( allowExtendedHtml = false, ): ReactNode { if (!SettingsStore.getValue("feature_html_topic")) { - htmlTopic = null; + htmlTopic = undefined; } let isFormattedTopic = !!htmlTopic; @@ -667,10 +667,10 @@ export function topicToHtml( let safeTopic = ""; try { - topicHasEmoji = mightContainEmoji(isFormattedTopic ? htmlTopic : topic); + topicHasEmoji = mightContainEmoji(isFormattedTopic ? htmlTopic! : topic); if (isFormattedTopic) { - safeTopic = sanitizeHtml(htmlTopic, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams); + safeTopic = sanitizeHtml(htmlTopic!, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams); if (topicHasEmoji) { safeTopic = formatEmojis(safeTopic, true).join(""); } @@ -679,7 +679,7 @@ export function topicToHtml( isFormattedTopic = false; // Fall back to plain-text topic } - let emojiBodyElements: ReturnType; + let emojiBodyElements: ReturnType | undefined; if (!isFormattedTopic && topicHasEmoji) { emojiBodyElements = formatEmojis(topic, false); } diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 8234f5bc757..1db73cc0745 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -169,10 +169,18 @@ export interface IConfigOptions { inline?: { left?: string; right?: string; + pattern?: { + tex?: string; + latex?: string; + }; }; display?: { left?: string; right?: string; + pattern?: { + tex?: string; + latex?: string; + }; }; }; diff --git a/src/IdentityAuthClient.tsx b/src/IdentityAuthClient.tsx index 293a3c19a6b..12f42a3add1 100644 --- a/src/IdentityAuthClient.tsx +++ b/src/IdentityAuthClient.tsx @@ -67,7 +67,7 @@ export default class IdentityAuthClient { window.localStorage.setItem("mx_is_access_token", this.accessToken); } - private readToken(): string { + private readToken(): string | null { if (this.tempClient) return null; // temporary client: ignore return window.localStorage.getItem("mx_is_access_token"); } @@ -77,13 +77,13 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - public async getAccessToken({ check = true } = {}): Promise { + public async getAccessToken({ check = true } = {}): Promise { if (!this.authEnabled) { // The current IS doesn't support authentication return null; } - let token = this.accessToken; + let token: string | null = this.accessToken; if (!token) { token = this.readToken(); } diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 9d4d3f61521..7b1ea4031be 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -16,6 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; + export const Key = { HOME: "Home", END: "End", @@ -76,7 +78,7 @@ export const Key = { export const IS_MAC = navigator.platform.toUpperCase().includes("MAC"); -export function isOnlyCtrlOrCmdKeyEvent(ev: KeyboardEvent): boolean { +export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean { if (IS_MAC) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; } else { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 82e5cac996c..34fffffc505 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -158,9 +158,9 @@ export default class LegacyCallHandler extends EventEmitter { private transferees = new Map(); // callId (target) -> call (transferee) private audioPromises = new Map>(); private audioElementsWithListeners = new Map(); - private supportsPstnProtocol = null; - private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol - private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native + private supportsPstnProtocol: boolean | null = null; + private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol + private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we @@ -181,20 +181,20 @@ export default class LegacyCallHandler extends EventEmitter { * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room" * if a voip_mxid_translate_pattern is set in the config) */ - public roomIdForCall(call: MatrixCall): string { + public roomIdForCall(call?: MatrixCall): string | null { if (!call) return null; // check asserted identity: if we're not obeying asserted identity, // this map will never be populated, but we check anyway for sanity if (this.shouldObeyAssertedfIdentity()) { - const nativeUser = this.assertedIdentityNativeUsers[call.callId]; + const nativeUser = this.assertedIdentityNativeUsers.get(call.callId); if (nativeUser) { const room = findDMForUser(MatrixClientPeg.get(), nativeUser); if (room) return room.roomId; } } - return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) ?? call.roomId ?? null; } public start(): void { @@ -282,7 +282,7 @@ export default class LegacyCallHandler extends EventEmitter { } public unSilenceCall(callId: string): void { - if (this.isForcedSilent) return; + if (this.isForcedSilent()) return; this.silencedCalls.delete(callId); this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.play(AudioID.Ring); @@ -341,14 +341,14 @@ export default class LegacyCallHandler extends EventEmitter { } private shouldObeyAssertedfIdentity(): boolean { - return SdkConfig.getObject("voip")?.get("obey_asserted_identity"); + return !!SdkConfig.getObject("voip")?.get("obey_asserted_identity"); } - public getSupportsPstnProtocol(): boolean { + public getSupportsPstnProtocol(): boolean | null { return this.supportsPstnProtocol; } - public getSupportsVirtualRooms(): boolean { + public getSupportsVirtualRooms(): boolean | null { return this.supportsSipNativeVirtual; } @@ -414,7 +414,7 @@ export default class LegacyCallHandler extends EventEmitter { cli.prepareToEncrypt(cli.getRoom(call.roomId)); }; - public getCallById(callId: string): MatrixCall { + public getCallById(callId: string): MatrixCall | null { for (const call of this.calls.values()) { if (call.callId === callId) return call; } @@ -435,7 +435,7 @@ export default class LegacyCallHandler extends EventEmitter { } public getAllActiveCalls(): MatrixCall[] { - const activeCalls = []; + const activeCalls: MatrixCall[] = []; for (const call of this.calls.values()) { if (call.state !== CallState.Ended && call.state !== CallState.Ringing) { @@ -446,7 +446,7 @@ export default class LegacyCallHandler extends EventEmitter { } public getAllActiveCallsNotInRoom(notInThisRoomId: string): MatrixCall[] { - const callsNotInThatRoom = []; + const callsNotInThatRoom: MatrixCall[] = []; for (const [roomId, call] of this.calls.entries()) { if (roomId !== notInThisRoomId && call.state !== CallState.Ended) { @@ -466,8 +466,8 @@ export default class LegacyCallHandler extends EventEmitter { return this.getAllActiveCallsNotInRoom(roomId); } - public getTransfereeForCallId(callId: string): MatrixCall { - return this.transferees[callId]; + public getTransfereeForCallId(callId: string): MatrixCall | undefined { + return this.transferees.get(callId); } public play(audioId: AudioID): void { @@ -547,7 +547,7 @@ export default class LegacyCallHandler extends EventEmitter { const mappedRoomId = this.roomIdForCall(call); const callForThisRoom = this.getCallForRoom(mappedRoomId); - return callForThisRoom && call.callId === callForThisRoom.callId; + return !!callForThisRoom && call.callId === callForThisRoom.callId; } private setCallListeners(call: MatrixCall): void { @@ -610,7 +610,7 @@ export default class LegacyCallHandler extends EventEmitter { return; } - const newAssertedIdentity = call.getRemoteAssertedIdentity().id; + const newAssertedIdentity = call.getRemoteAssertedIdentity()?.id; let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); @@ -621,7 +621,7 @@ export default class LegacyCallHandler extends EventEmitter { logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); if (newNativeAssertedIdentity) { - this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; + this.assertedIdentityNativeUsers.set(call.callId, newNativeAssertedIdentity); // If we don't already have a room with this user, make one. This will be slightly odd // if they called us because we'll be inviting them, but there's not much we can do about @@ -642,7 +642,7 @@ export default class LegacyCallHandler extends EventEmitter { }); } - private onCallStateChanged = (newState: CallState, oldState: CallState, call: MatrixCall): void => { + private onCallStateChanged = (newState: CallState, oldState: CallState | null, call: MatrixCall): void => { if (!this.matchesCallForThisRoom(call)) return; const mappedRoomId = this.roomIdForCall(call); @@ -830,7 +830,7 @@ export default class LegacyCallHandler extends EventEmitter { "turn.matrix.org, but this will not be as reliable, and " + "it will share your IP address with that server. You can also manage " + "this in Settings.", - null, + undefined, { code }, )}

@@ -843,7 +843,7 @@ export default class LegacyCallHandler extends EventEmitter { cli.setFallbackICEServerAllowed(allow); }, }, - null, + undefined, true, ); } @@ -882,7 +882,7 @@ export default class LegacyCallHandler extends EventEmitter { title, description, }, - null, + undefined, true, ); } @@ -917,7 +917,7 @@ export default class LegacyCallHandler extends EventEmitter { return; } if (transferee) { - this.transferees[call.callId] = transferee; + this.transferees.set(call.callId, transferee); } this.setCallListeners(call); diff --git a/src/Login.ts b/src/Login.ts index 6475a9f5c93..73e6366956f 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -29,20 +29,17 @@ interface ILoginOptions { } export default class Login { - private hsUrl: string; - private isUrl: string; - private fallbackHsUrl: string; - private flows: Array; - private defaultDeviceDisplayName: string; - private tempClient: MatrixClient; - - public constructor(hsUrl: string, isUrl: string, fallbackHsUrl?: string, opts?: ILoginOptions) { - this.hsUrl = hsUrl; - this.isUrl = isUrl; - this.fallbackHsUrl = fallbackHsUrl; - this.flows = []; + private flows: Array = []; + private readonly defaultDeviceDisplayName?: string; + private tempClient: MatrixClient | null = null; // memoize + + public constructor( + private hsUrl: string, + private isUrl: string, + private fallbackHsUrl: string | null, + opts: ILoginOptions, + ) { this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - this.tempClient = null; // memoize } public getHomeserverUrl(): string { @@ -91,12 +88,12 @@ export default class Login { } public loginViaPassword( - username: string, - phoneCountry: string, - phoneNumber: string, + username: string | undefined, + phoneCountry: string | undefined, + phoneNumber: string | undefined, password: string, ): Promise { - const isEmail = username.indexOf("@") > 0; + const isEmail = !!username && username.indexOf("@") > 0; let identifier; if (phoneCountry && phoneNumber) { @@ -127,7 +124,7 @@ export default class Login { }; const tryFallbackHs = (originalError: Error): Promise => { - return sendLoginRequest(this.fallbackHsUrl, this.isUrl, "m.login.password", loginParams).catch( + return sendLoginRequest(this.fallbackHsUrl!, this.isUrl, "m.login.password", loginParams).catch( (fallbackError) => { logger.log("fallback HS login failed", fallbackError); // throw the original error @@ -136,13 +133,13 @@ export default class Login { ); }; - let originalLoginError = null; + let originalLoginError: Error | null = null; return sendLoginRequest(this.hsUrl, this.isUrl, "m.login.password", loginParams) .catch((error) => { originalLoginError = error; if (error.httpStatus === 403) { if (this.fallbackHsUrl) { - return tryFallbackHs(originalLoginError); + return tryFallbackHs(originalLoginError!); } } throw originalLoginError; diff --git a/src/Markdown.ts b/src/Markdown.ts index b082e659d01..e493649e6d0 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -141,7 +141,7 @@ export default class Markdown { */ private repairLinks(parsed: commonmark.Node): commonmark.Node { const walker = parsed.walker(); - let event: commonmark.NodeWalkingStep = null; + let event: commonmark.NodeWalkingStep | null = null; let text = ""; let isInPara = false; let previousNode: commonmark.Node | null = null; @@ -289,7 +289,7 @@ export default class Markdown { // However, if it's a blockquote, adds a p tag anyway // in order to avoid deviation to commonmark and unexpected // results when parsing the formatted HTML. - if (node.parent.type === "block_quote" || isMultiLine(node)) { + if (node.parent?.type === "block_quote" || isMultiLine(node)) { realParagraph.call(this, node, entering); } }; diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 19a9eb5fde8..69a2263fc59 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. Copyright 2017, 2018, 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2023 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. @@ -218,7 +218,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours - opts.threadSupport = SettingsStore.getValue("feature_threadenabled"); + opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 329741fe511..fcb066e92fe 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -37,7 +37,7 @@ export enum MediaDeviceHandlerEvent { } export default class MediaDeviceHandler extends EventEmitter { - private static internalInstance; + private static internalInstance?: MediaDeviceHandler; public static get instance(): MediaDeviceHandler { if (!MediaDeviceHandler.internalInstance) { @@ -67,7 +67,7 @@ export default class MediaDeviceHandler extends EventEmitter { public static async getDevices(): Promise { try { const devices = await navigator.mediaDevices.enumerateDevices(); - const output = { + const output: Record = { [MediaDeviceKindEnum.AudioOutput]: [], [MediaDeviceKindEnum.AudioInput]: [], [MediaDeviceKindEnum.VideoInput]: [], diff --git a/src/Modal.tsx b/src/Modal.tsx index 1b21f74b5e5..3b21c47d3d0 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -33,7 +33,7 @@ export interface IModal { beforeClosePromise?: Promise; closeReason?: string; onBeforeClose?(reason?: string): Promise; - onFinished(...args: T): void; + onFinished?(...args: T): void; close(...args: T): void; hidden?: boolean; } @@ -68,11 +68,11 @@ export class ModalManager extends TypedEventEmitter = null; + private priorityModal: IModal | null = null; // The modal to keep open underneath other modals if possible. Useful // for cases like Settings where the modal should remain open while the // user is prompted for more information/errors. - private staticModal: IModal = null; + private staticModal: IModal | null = null; // A list of the modals we have stacked up, with the most recent at [0] // Neither the static nor priority modal will be in this list. private modals: IModal[] = []; @@ -144,17 +144,14 @@ export class ModalManager extends TypedEventEmitter["close"]; onFinishedProm: IHandle["finished"]; } { - const modal: IModal = { - onFinished: props ? props.onFinished : null, - onBeforeClose: options.onBeforeClose, - beforeClosePromise: null, - closeReason: null, + const modal = { + onFinished: props?.onFinished, + onBeforeClose: options?.onBeforeClose, className, // these will be set below but we need an object reference to pass to getCloseFn before we can do that elem: null, - close: null, - }; + } as IModal; // never call this from onFinished() otherwise it will loop const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); @@ -173,7 +170,7 @@ export class ModalManager extends TypedEventEmitter( modal: IModal, - props: IProps, + props?: IProps, ): [IHandle["close"], IHandle["finished"]] { const deferred = defer(); return [ @@ -183,13 +180,13 @@ export class ModalManager extends TypedEventEmitter= 0) { this.modals.splice(i, 1); @@ -317,7 +314,7 @@ export class ModalManager extends TypedEventEmitter { diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index 24b4e85ae37..b8c3f855ac4 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactInstance } from "react"; import ReactDom from "react-dom"; interface IChildProps { @@ -41,7 +41,7 @@ interface IProps { * automatic positional animation, look at react-shuffle or similar libraries. */ export default class NodeAnimator extends React.Component { - private nodes = {}; + private nodes: Record = {}; private children: { [key: string]: React.DetailedReactHTMLElement }; public static defaultProps: Partial = { startStyles: [], @@ -65,7 +65,7 @@ export default class NodeAnimator extends React.Component { */ private applyStyles(node: HTMLElement, styles: React.CSSProperties): void { Object.entries(styles).forEach(([property, value]) => { - node.style[property] = value; + node.style[property as keyof Omit] = value; }); } @@ -120,7 +120,7 @@ export default class NodeAnimator extends React.Component { this.nodes[k] = node; } - public render(): JSX.Element { + public render(): React.ReactNode { return <>{Object.values(this.children)}; } } diff --git a/src/Notifier.ts b/src/Notifier.ts index 42909a2632e..4e34083f3f6 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -68,7 +68,7 @@ Override both the content body and the TextForEvent handler for specific msgtype This is useful when the content body contains fallback text that would explain that the client can't handle a particular type of tile. */ -const msgTypeHandlers = { +const msgTypeHandlers: Record string | null> = { [MsgType.KeyVerificationRequest]: (event: MatrixEvent) => { const name = (event.sender || {}).name; return _t("%(name)s is requesting verification", { name }); @@ -95,22 +95,26 @@ const msgTypeHandlers = { }, }; -export const Notifier = { - notifsByRoom: {}, +class NotifierClass { + private notifsByRoom: Record = {}; // A list of event IDs that we've received but need to wait until // they're decrypted until we decide whether to notify for them // or not - pendingEncryptedEventIds: [], + private pendingEncryptedEventIds: string[] = []; - notificationMessageForEvent: function (ev: MatrixEvent): string { + private toolbarHidden?: boolean; + private isSyncing?: boolean; + + public notificationMessageForEvent(ev: MatrixEvent): string { if (msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) { return msgTypeHandlers[ev.getContent().msgtype](ev); } return TextForEvent.textForEvent(ev); - }, + } - _displayPopupNotification: function (ev: MatrixEvent, room: Room): void { + // XXX: exported for tests + public displayPopupNotification(ev: MatrixEvent, room: Room): void { const plaf = PlatformPeg.get(); const cli = MatrixClientPeg.get(); if (!plaf) { @@ -152,7 +156,7 @@ export const Notifier = { msg = ""; } - let avatarUrl = null; + let avatarUrl: string | null = null; if (ev.sender && !SettingsStore.getValue("lowBandwidth")) { avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, "crop"); } @@ -162,12 +166,17 @@ export const Notifier = { // if displayNotification returns non-null, the platform supports // clearing notifications later, so keep track of this. if (notif) { - if (this.notifsByRoom[ev.getRoomId()] === undefined) this.notifsByRoom[ev.getRoomId()] = []; - this.notifsByRoom[ev.getRoomId()].push(notif); + if (this.notifsByRoom[ev.getRoomId()!] === undefined) this.notifsByRoom[ev.getRoomId()!] = []; + this.notifsByRoom[ev.getRoomId()!].push(notif); } - }, - - getSoundForRoom: function (roomId: string) { + } + + public getSoundForRoom(roomId: string): { + url: string; + name: string; + type: string; + size: string; + } | null { // We do no caching here because the SDK caches setting // and the browser will cache the sound. const content = SettingsStore.getValue("notificationSound", roomId); @@ -193,9 +202,10 @@ export const Notifier = { type: content.type, size: content.size, }; - }, + } - _playAudioNotification: async function (ev: MatrixEvent, room: Room): Promise { + // XXX: Exported for tests + public async playAudioNotification(ev: MatrixEvent, room: Room): Promise { const cli = MatrixClientPeg.get(); if (localNotificationsAreSilenced(cli)) { return; @@ -209,7 +219,7 @@ export const Notifier = { sound ? `audio[src='${sound.url}']` : "#messageAudio", ); let audioElement = selector; - if (!selector) { + if (!audioElement) { if (!sound) { logger.error("No audio element or sound to play for notification"); return; @@ -224,39 +234,32 @@ export const Notifier = { } catch (ex) { logger.warn("Caught error when trying to fetch room notification sound:", ex); } - }, + } - start: function (this: typeof Notifier) { - // do not re-bind in the case of repeated call - this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this); - this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this); - this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); - this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); - - MatrixClientPeg.get().on(RoomEvent.Timeline, this.boundOnEvent); - MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt); - MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); - MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange); + public start(): void { + MatrixClientPeg.get().on(RoomEvent.Timeline, this.onEvent); + MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt); + MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().on(ClientEvent.Sync, this.onSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; - }, + } - stop: function (this: typeof Notifier) { + public stop(): void { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.boundOnEvent); - MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt); - MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); - MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange); + MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.onEvent); + MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.onRoomReceipt); + MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onSyncStateChange); } this.isSyncing = false; - }, + } - supportsDesktopNotifications: function () { - const plaf = PlatformPeg.get(); - return plaf && plaf.supportsNotifications(); - }, + public supportsDesktopNotifications(): boolean { + return PlatformPeg.get()?.supportsNotifications() ?? false; + } - setEnabled: function (enable: boolean, callback?: () => void) { + public setEnabled(enable: boolean, callback?: () => void): void { const plaf = PlatformPeg.get(); if (!plaf) return; @@ -320,31 +323,30 @@ export const Notifier = { // set the notifications_hidden flag, as the user has knowingly interacted // with the setting we shouldn't nag them any further this.setPromptHidden(true); - }, + } - isEnabled: function () { + public isEnabled(): boolean { return this.isPossible() && SettingsStore.getValue("notificationsEnabled"); - }, + } - isPossible: function () { + public isPossible(): boolean { const plaf = PlatformPeg.get(); - if (!plaf) return false; - if (!plaf.supportsNotifications()) return false; + if (!plaf?.supportsNotifications()) return false; if (!plaf.maySendNotifications()) return false; return true; // possible, but not necessarily enabled - }, + } - isBodyEnabled: function () { + public isBodyEnabled(): boolean { return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled"); - }, + } - isAudioEnabled: function () { + public isAudioEnabled(): boolean { // We don't route Audio via the HTML Notifications API so it is possible regardless of other things return SettingsStore.getValue("audioNotificationsEnabled"); - }, + } - setPromptHidden: function (this: typeof Notifier, hidden: boolean, persistent = true) { + public setPromptHidden(hidden: boolean, persistent = true): void { this.toolbarHidden = hidden; hideNotificationsToast(); @@ -353,9 +355,9 @@ export const Notifier = { if (persistent && global.localStorage) { global.localStorage.setItem("notifications_hidden", String(hidden)); } - }, + } - shouldShowPrompt: function () { + public shouldShowPrompt(): boolean { const client = MatrixClientPeg.get(); if (!client) { return false; @@ -366,25 +368,21 @@ export const Notifier = { this.supportsDesktopNotifications() && !isPushNotifyDisabled() && !this.isEnabled() && - !this._isPromptHidden() + !this.isPromptHidden() ); - }, + } - _isPromptHidden: function (this: typeof Notifier) { + private isPromptHidden(): boolean { // Check localStorage for any such meta data if (global.localStorage) { return global.localStorage.getItem("notifications_hidden") === "true"; } - return this.toolbarHidden; - }, + return !!this.toolbarHidden; + } - onSyncStateChange: function ( - this: typeof Notifier, - state: SyncState, - prevState?: SyncState, - data?: ISyncStateData, - ) { + // XXX: Exported for tests + public onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: ISyncStateData): void => { if (state === SyncState.Syncing) { this.isSyncing = true; } else if (state === SyncState.Stopped || state === SyncState.Error) { @@ -395,16 +393,15 @@ export const Notifier = { if (![SyncState.Stopped, SyncState.Error].includes(state) && !data?.fromCache) { createLocalNotificationSettingsIfNeeded(MatrixClientPeg.get()); } - }, + }; - onEvent: function ( - this: typeof Notifier, + private onEvent = ( ev: MatrixEvent, room: Room | undefined, toStartOfTimeline: boolean | undefined, removed: boolean, data: IRoomTimelineData, - ) { + ): void => { if (!data.liveEvent) return; // only notify for new things, not old. if (!this.isSyncing) return; // don't alert for any messages initially if (ev.getSender() === MatrixClientPeg.get().getUserId()) return; @@ -414,7 +411,7 @@ export const Notifier = { // If it's an encrypted event and the type is still 'm.room.encrypted', // it hasn't yet been decrypted, so wait until it is. if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - this.pendingEncryptedEventIds.push(ev.getId()); + this.pendingEncryptedEventIds.push(ev.getId()!); // don't let the list fill up indefinitely while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) { this.pendingEncryptedEventIds.shift(); @@ -422,22 +419,22 @@ export const Notifier = { return; } - this._evaluateEvent(ev); - }, + this.evaluateEvent(ev); + }; - onEventDecrypted: function (ev: MatrixEvent) { + private onEventDecrypted = (ev: MatrixEvent): void => { // 'decrypted' means the decryption process has finished: it may have failed, // in which case it might decrypt soon if the keys arrive if (ev.isDecryptionFailure()) return; - const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); + const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()!); if (idx === -1) return; this.pendingEncryptedEventIds.splice(idx, 1); - this._evaluateEvent(ev); - }, + this.evaluateEvent(ev); + }; - onRoomReceipt: function (ev: MatrixEvent, room: Room) { + private onRoomReceipt = (ev: MatrixEvent, room: Room): void => { if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether @@ -453,13 +450,13 @@ export const Notifier = { } delete this.notifsByRoom[room.roomId]; } - }, + }; - _evaluateEvent: function (ev: MatrixEvent) { + // XXX: exported for tests + public evaluateEvent(ev: MatrixEvent): void { // Mute notifications for broadcast info events if (ev.getType() === VoiceBroadcastInfoEventType) return; - - let roomId = ev.getRoomId(); + let roomId = ev.getRoomId()!; if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { // Attempt to translate a virtual room to a native one const nativeRoomId = VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(roomId); @@ -477,7 +474,7 @@ export const Notifier = { const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions?.notify) { - this._performCustomEventHandling(ev); + this.performCustomEventHandling(ev); const store = SdkContextClass.instance.roomViewStore; const isViewingRoom = store.getRoomId() === room.roomId; @@ -492,33 +489,34 @@ export const Notifier = { } if (this.isEnabled()) { - this._displayPopupNotification(ev, room); + this.displayPopupNotification(ev, room); } if (actions.tweaks.sound && this.isAudioEnabled()) { - PlatformPeg.get().loudNotification(ev, room); - this._playAudioNotification(ev, room); + PlatformPeg.get()?.loudNotification(ev, room); + this.playAudioNotification(ev, room); } } - }, + } /** * Some events require special handling such as showing in-app toasts */ - _performCustomEventHandling: function (ev: MatrixEvent) { + private performCustomEventHandling(ev: MatrixEvent): void { if (ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType()) && SettingsStore.getValue("feature_group_calls")) { ToastStore.sharedInstance().addOrReplaceToast({ - key: getIncomingCallToastKey(ev.getStateKey()), + key: getIncomingCallToastKey(ev.getStateKey()!), priority: 100, component: IncomingCallToast, bodyClassName: "mx_IncomingCallToast", props: { callEvent: ev }, }); } - }, -}; + } +} if (!window.mxNotifier) { - window.mxNotifier = Notifier; + window.mxNotifier = new NotifierClass(); } export default window.mxNotifier; +export const Notifier: NotifierClass = window.mxNotifier; diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index 7dbc8a54060..851aa95df13 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -146,7 +146,7 @@ export default class PasswordReset { err.message = _t("Failed to verify email address: make sure you clicked the link in the email"); } else if (err.httpStatus === 404) { err.message = _t( - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", + "Your email address does not appear to be associated with a Matrix ID on this homeserver.", ); } else if (err.httpStatus) { err.message += ` (Status ${err.httpStatus})`; diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index c8a99ab4264..5c5805af938 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -132,8 +132,8 @@ export class PosthogAnalytics { private anonymity = Anonymity.Disabled; // set true during the constructor if posthog config is present, otherwise false private readonly enabled: boolean = false; - private static _instance = null; - private platformSuperProperties = {}; + private static _instance: PosthogAnalytics | null = null; + private platformSuperProperties: Properties = {}; public static readonly ANALYTICS_EVENT_TYPE = "im.vector.analytics"; private propertiesForNextEvent: Partial> = {}; private userPropertyCache: UserProperties = {}; @@ -238,11 +238,11 @@ export class PosthogAnalytics { } } - private static async getPlatformProperties(): Promise { + private static async getPlatformProperties(): Promise> { const platform = PlatformPeg.get(); - let appVersion: string; + let appVersion: string | undefined; try { - appVersion = await platform.getAppVersion(); + appVersion = await platform?.getAppVersion(); } catch (e) { // this happens if no version is set i.e. in dev appVersion = "unknown"; @@ -250,7 +250,7 @@ export class PosthogAnalytics { return { appVersion, - appPlatform: platform.getHumanReadableName(), + appPlatform: platform?.getHumanReadableName(), }; } @@ -411,7 +411,7 @@ export class PosthogAnalytics { // All other scenarios should not track a user before they have given // explicit consent that they are ok with their analytics data being collected const options: IPostHogEventOptions = {}; - const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time"), 10); + const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time")!, 10); if (!isNaN(registrationTime)) { options.timestamp = new Date(registrationTime); } diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index 8a8b02965ce..a82b78c1dd2 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -120,7 +120,7 @@ export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName PosthogTrackers.instance.clearOverride(this.props.screenName); } - public render(): JSX.Element { + public render(): React.ReactNode { return null; // no need to render anything, we just need to hook into the React lifecycle } } diff --git a/src/Presence.ts b/src/Presence.ts index c13cc32b60f..02d2ef0e7e0 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -33,9 +33,9 @@ enum State { } class Presence { - private unavailableTimer: Timer = null; - private dispatcherRef: string = null; - private state: State = null; + private unavailableTimer: Timer | null = null; + private dispatcherRef: string | null = null; + private state: State | null = null; /** * Start listening the user activity to evaluate his presence state. @@ -73,14 +73,14 @@ class Presence { * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - public getState(): State { + public getState(): State | null { return this.state; } private onAction = (payload: ActionPayload): void => { if (payload.action === "user_activity") { this.setState(State.Online); - this.unavailableTimer.restart(); + this.unavailableTimer?.restart(); } }; diff --git a/src/Resend.ts b/src/Resend.ts index bc62c62efab..17e39a7e296 100644 --- a/src/Resend.ts +++ b/src/Resend.ts @@ -46,7 +46,7 @@ export default class Resend { } public static resend(event: MatrixEvent): Promise { - const room = MatrixClientPeg.get().getRoom(event.getRoomId()); + const room = MatrixClientPeg.get().getRoom(event.getRoomId())!; return MatrixClientPeg.get() .resendEvent(event, room) .then( diff --git a/src/RoomAliasCache.ts b/src/RoomAliasCache.ts index c318db2d3f6..f565d8d2d3d 100644 --- a/src/RoomAliasCache.ts +++ b/src/RoomAliasCache.ts @@ -30,6 +30,6 @@ export function storeRoomAliasInCache(alias: string, id: string): void { aliasToIDMap.set(alias, id); } -export function getCachedRoomIDForAlias(alias: string): string { +export function getCachedRoomIDForAlias(alias: string): string | undefined { return aliasToIDMap.get(alias); } diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index 582eb360f81..c92ebcc55e2 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -112,7 +112,7 @@ export function inviteUsersToRoom( ): Promise { return inviteMultipleToRoom(roomId, userIds, sendSharedHistoryKeys, progressCallback) .then((result) => { - const room = MatrixClientPeg.get().getRoom(roomId); + const room = MatrixClientPeg.get().getRoom(roomId)!; showAnyInviteErrors(result.states, room, result.inviter); }) .catch((err) => { @@ -142,7 +142,7 @@ export function showAnyInviteErrors( }); return false; } else { - const errorList = []; + const errorList: string[] = []; for (const addr of failedUsers) { if (states[addr] === "error") { const reason = inviter.getErrorText(addr); @@ -173,16 +173,19 @@ export function showAnyInviteErrors(
{name} - {user.userId} + {user?.userId}
{inviter.getErrorText(addr)} diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index c98f5685083..9e94d0895d6 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -48,7 +48,7 @@ export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNo } // for everything else, look at the room rule. - let roomRule = null; + let roomRule: IPushRule | undefined; try { roomRule = client.getRoomPushRule("global", roomId); } catch (err) { @@ -108,7 +108,7 @@ export function getUnreadNotificationCount(room: Room, type: NotificationCountTy function setRoomNotifsStateMuted(roomId: string): Promise { const cli = MatrixClientPeg.get(); - const promises = []; + const promises: Promise[] = []; // delete the room rule const roomRule = cli.getRoomPushRule("global", roomId); @@ -139,7 +139,7 @@ function setRoomNotifsStateMuted(roomId: string): Promise { function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise { const cli = MatrixClientPeg.get(); - const promises = []; + const promises: Promise[] = []; const overrideMuteRule = findOverrideMuteRule(roomId); if (overrideMuteRule) { diff --git a/src/Rooms.ts b/src/Rooms.ts index 12fed5aac1a..25bbb0e1778 100644 --- a/src/Rooms.ts +++ b/src/Rooms.ts @@ -30,13 +30,13 @@ import AliasCustomisations from "./customisations/Alias"; * @param {Object} room The room object * @returns {string} A display alias for the given room */ -export function getDisplayAliasForRoom(room: Room): string | undefined { +export function getDisplayAliasForRoom(room: Room): string | null { return getDisplayAliasForAliasSet(room.getCanonicalAlias(), room.getAltAliases()); } // The various display alias getters should all feed through this one path so // there's a single place to change the logic. -export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string { +export function getDisplayAliasForAliasSet(canonicalAlias: string | null, altAliases: string[]): string | null { if (AliasCustomisations.getDisplayAliasForAliasSet) { return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases); } @@ -46,7 +46,7 @@ export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: s export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise { let newTarget; if (isDirect) { - const guessedUserId = guessDMRoomTargetId(room, MatrixClientPeg.get().getUserId()); + const guessedUserId = guessDMRoomTargetId(room, MatrixClientPeg.get().getUserId()!); newTarget = guessedUserId; } else { newTarget = null; @@ -119,7 +119,7 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string { if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; - oldestTs = user.events.member.getTs(); + oldestTs = user.events.member?.getTs(); } } if (oldestUser) return oldestUser.userId; @@ -130,7 +130,7 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string { if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; - oldestTs = user.events.member.getTs(); + oldestTs = user.events.member?.getTs(); } } diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index f65d03ab5cb..8400a8f03fe 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -32,8 +32,8 @@ const imApiVersion = "1.1"; // TODO: Generify the name of this class and all components within - it's not just for Scalar. export default class ScalarAuthClient { - private scalarToken: string; - private termsInteractionCallback: TermsInteractionCallback; + private scalarToken: string | null; + private termsInteractionCallback?: TermsInteractionCallback; private isDefaultManager: boolean; public constructor(private apiUrl: string, private uiUrl: string) { @@ -59,7 +59,7 @@ export default class ScalarAuthClient { } } - private readTokenFromStore(): string { + private readTokenFromStore(): string | null { let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); if (!token && this.isDefaultManager) { token = window.localStorage.getItem("mx_scalar_token"); @@ -67,7 +67,7 @@ export default class ScalarAuthClient { return token; } - private readToken(): string { + private readToken(): string | null { if (this.scalarToken) return this.scalarToken; return this.readTokenFromStore(); } @@ -256,7 +256,7 @@ export default class ScalarAuthClient { } } - public getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string { + public getScalarInterfaceUrlForRoom(room: Room, screen?: string, id?: string): string { const roomId = room.roomId; const roomName = room.name; let url = this.uiUrl; diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index fec671eab43..aebffad18b7 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -358,7 +358,7 @@ function inviteUser(event: MessageEvent, roomId: string, userId: string): v if (room) { // if they are already invited or joined we can resolve immediately. const member = room.getMember(userId); - if (member && ["join", "invite"].includes(member.membership)) { + if (member && ["join", "invite"].includes(member.membership!)) { sendResponse(event, { success: true, }); @@ -389,7 +389,7 @@ function kickUser(event: MessageEvent, roomId: string, userId: string): voi if (room) { // if they are already not in the room we can resolve immediately. const member = room.getMember(userId); - if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { + if (!member || getEffectiveMembership(member.membership!) === EffectiveMembership.Leave) { sendResponse(event, { success: true, }); @@ -472,7 +472,7 @@ function setWidget(event: MessageEvent, roomId: string | null): void { } else { // Room widget if (!roomId) { - sendError(event, _t("Missing roomId."), null); + sendError(event, _t("Missing roomId.")); return; } WidgetUtils.setRoomWidget( @@ -675,7 +675,7 @@ function canSendEvent(event: MessageEvent, roomId: string): void { sendError(event, _t("You are not in this room.")); return; } - const me = client.credentials.userId; + const me = client.credentials.userId!; let canSend = false; if (isState) { diff --git a/src/Searching.ts b/src/Searching.ts index df54695326e..90798f480ed 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -34,7 +34,7 @@ const SEARCH_LIMIT = 10; async function serverSideSearch( term: string, - roomId: string = undefined, + roomId?: string, abortSignal?: AbortSignal, ): Promise<{ response: ISearchResponse; query: ISearchRequestBody }> { const client = MatrixClientPeg.get(); @@ -67,7 +67,7 @@ async function serverSideSearch( async function serverSideSearchProcess( term: string, - roomId: string = undefined, + roomId?: string, abortSignal?: AbortSignal, ): Promise { const client = MatrixClientPeg.get(); @@ -158,7 +158,7 @@ async function combinedSearch(searchTerm: string, abortSignal?: AbortSignal): Pr async function localSearch( searchTerm: string, - roomId: string = undefined, + roomId?: string, processResult = true, ): Promise<{ response: IResultRoomEvents; query: ISearchArgs }> { const eventIndex = EventIndexPeg.get(); @@ -195,7 +195,7 @@ export interface ISeshatSearchResults extends ISearchResults { serverSideNextBatch?: string; } -async function localSearchProcess(searchTerm: string, roomId: string = undefined): Promise { +async function localSearchProcess(searchTerm: string, roomId?: string): Promise { const emptyResult = { results: [], highlights: [], @@ -244,7 +244,7 @@ async function localPagination(searchResult: ISeshatSearchResults): Promise { +function eventIndexSearch(term: string, roomId?: string, abortSignal?: AbortSignal): Promise { let searchPromise: Promise; if (roomId !== undefined) { @@ -643,11 +639,7 @@ export function searchPagination(searchResult: ISearchResults): Promise { +export default function eventSearch(term: string, roomId?: string, abortSignal?: AbortSignal): Promise { const eventIndex = EventIndexPeg.get(); if (eventIndex === null) { diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 20db6594b01..2a56de87513 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -102,14 +102,14 @@ async function getSecretStorageKey({ }): Promise<[string, Uint8Array]> { const cli = MatrixClientPeg.get(); let keyId = await cli.getDefaultSecretStorageKeyId(); - let keyInfo: ISecretStorageKeyInfo; + let keyInfo!: ISecretStorageKeyInfo; if (keyId) { // use the default SSSS key if set keyInfo = keyInfos[keyId]; if (!keyInfo) { // if the default key is not available, pretend the default key // isn't set - keyId = undefined; + keyId = null; } } if (!keyId) { @@ -156,7 +156,7 @@ async function getSecretStorageKey({ return MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo); }, }, - /* className= */ null, + /* className= */ undefined, /* isPriorityModal= */ false, /* isStaticModal= */ false, /* options= */ { @@ -182,7 +182,7 @@ async function getSecretStorageKey({ export async function getDehydrationKey( keyInfo: ISecretStorageKeyInfo, - checkFunc: (Uint8Array) => void, + checkFunc: (data: Uint8Array) => void, ): Promise { const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); if (keyFromCustomisations) { @@ -196,7 +196,7 @@ export async function getDehydrationKey( /* props= */ { keyInfo, - checkPrivateKey: async (input): Promise => { + checkPrivateKey: async (input: KeyParams): Promise => { const key = await inputToKey(input); try { checkFunc(key); @@ -206,7 +206,7 @@ export async function getDehydrationKey( } }, }, - /* className= */ null, + /* className= */ undefined, /* isPriorityModal= */ false, /* isStaticModal= */ false, /* options= */ { @@ -243,7 +243,7 @@ async function onSecretRequested( requestId: string, name: string, deviceTrust: DeviceTrustLevel, -): Promise { +): Promise { logger.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); const client = MatrixClientPeg.get(); if (userId !== client.getUserId()) { @@ -259,19 +259,19 @@ async function onSecretRequested( name === "m.cross_signing.user_signing" ) { const callbacks = client.getCrossSigningCacheCallbacks(); - if (!callbacks.getCrossSigningKeyCache) return; + if (!callbacks?.getCrossSigningKeyCache) return; const keyId = name.replace("m.cross_signing.", ""); const key = await callbacks.getCrossSigningKeyCache(keyId); if (!key) { logger.log(`${keyId} requested by ${deviceId}, but not found in cache`); } - return key && encodeBase64(key); + return key ? encodeBase64(key) : undefined; } else if (name === "m.megolm_backup.v1") { - const key = await client.crypto.getSessionBackupPrivateKey(); + const key = await client.crypto?.getSessionBackupPrivateKey(); if (!key) { logger.log(`session backup key requested by ${deviceId}, but not found in cache`); } - return key && encodeBase64(key); + return key ? encodeBase64(key) : undefined; } logger.warn("onSecretRequested didn't recognise the secret named ", name); } @@ -284,15 +284,15 @@ export const crossSigningCallbacks: ICryptoCallbacks = { }; export async function promptForBackupPassphrase(): Promise { - let key: Uint8Array; + let key!: Uint8Array; const { finished } = Modal.createDialog( RestoreKeyBackupDialog, { showSummary: false, - keyCallback: (k) => (key = k), + keyCallback: (k: Uint8Array) => (key = k), }, - null, + undefined, /* priority = */ false, /* static = */ true, ); @@ -338,7 +338,7 @@ export async function accessSecretStorage(func = async (): Promise => {}, { forceReset, }, - null, + undefined, /* priority = */ false, /* static = */ true, /* options = */ { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 3434090d8ea..66f673445dd 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -81,7 +81,8 @@ const singleMxcUpload = async (): Promise => { const fileSelector = document.createElement("input"); fileSelector.setAttribute("type", "file"); fileSelector.onchange = (ev: HTMLInputEvent) => { - const file = ev.target.files[0]; + const file = ev.target.files?.[0]; + if (!file) return; Modal.createDialog(UploadConfirmDialog, { file, @@ -111,7 +112,7 @@ export const CommandCategories = { export type RunResult = XOR<{ error: Error | ITranslatableError }, { promise: Promise }>; -type RunFn = (this: Command, roomId: string, args: string) => RunResult; +type RunFn = (this: Command, roomId: string, args?: string) => RunResult; interface ICommandOpts { command: string; @@ -159,7 +160,7 @@ export class Command { return this.getCommand() + " " + this.args; } - public run(roomId: string, threadId: string, args: string): RunResult { + public run(roomId: string, threadId: string | null, args?: string): RunResult { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` if (!this.runFn) { return reject(newTranslatableError("Command error: Unable to handle slash command.")); @@ -304,7 +305,7 @@ export const Commands = [ if (args) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - if (!room.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { + if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { return reject( newTranslatableError("You do not have the required permissions to use this command."), ); @@ -313,7 +314,7 @@ export const Commands = [ const { finished } = Modal.createDialog( RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, - /*className=*/ null, + /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, ); @@ -395,12 +396,12 @@ export const Commands = [ runFn: function (roomId, args) { if (args) { const cli = MatrixClientPeg.get(); - const ev = cli.getRoom(roomId).currentState.getStateEvents("m.room.member", cli.getUserId()); + const ev = cli.getRoom(roomId)?.currentState.getStateEvents("m.room.member", cli.getUserId()!); const content = { ...(ev ? ev.getContent() : { membership: "join" }), displayname: args, }; - return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getUserId())); + return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getUserId()!)); } return reject(this.getUsage()); }, @@ -413,7 +414,7 @@ export const Commands = [ description: _td("Changes the avatar of the current room"), isEnabled: () => !isCurrentLocalRoom(), runFn: function (roomId, args) { - let promise = Promise.resolve(args); + let promise = Promise.resolve(args ?? null); if (!args) { promise = singleMxcUpload(); } @@ -436,9 +437,9 @@ export const Commands = [ runFn: function (roomId, args) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - const userId = cli.getUserId(); + const userId = cli.getUserId()!; - let promise = Promise.resolve(args); + let promise = Promise.resolve(args ?? null); if (!args) { promise = singleMxcUpload(); } @@ -446,7 +447,7 @@ export const Commands = [ return success( promise.then((url) => { if (!url) return; - const ev = room.currentState.getStateEvents("m.room.member", userId); + const ev = room?.currentState.getStateEvents("m.room.member", userId); const content = { ...(ev ? ev.getContent() : { membership: "join" }), avatar_url: url, @@ -463,7 +464,7 @@ export const Commands = [ args: "[]", description: _td("Changes your avatar in all rooms"), runFn: function (roomId, args) { - let promise = Promise.resolve(args); + let promise = Promise.resolve(args ?? null); if (!args) { promise = singleMxcUpload(); } @@ -496,7 +497,7 @@ export const Commands = [ ); } - const content: MRoomTopicEventContent = room.currentState.getStateEvents("m.room.topic", "")?.getContent(); + const content = room.currentState.getStateEvents("m.room.topic", "")?.getContent(); const topic = !!content ? ContentHelpers.parseTopicContent(content) : { text: _t("This room has no topic.") }; @@ -697,11 +698,8 @@ export const Commands = [ } if (viaServers) { - // For the join - dispatch["opts"] = { - // These are passed down to the js-sdk's /join call - viaServers: viaServers, - }; + // For the join, these are passed down to the js-sdk's /join call + dispatch["opts"] = { viaServers }; // For if the join fails (rejoin button) dispatch["via_servers"] = viaServers; @@ -877,7 +875,8 @@ export const Commands = [ const cli = MatrixClientPeg.get(); const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()); return ( - room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()) && !isLocalRoom(room) + !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()!) && + !isLocalRoom(room) ); }, runFn: function (roomId, args) { @@ -919,7 +918,8 @@ export const Commands = [ const cli = MatrixClientPeg.get(); const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()); return ( - room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()) && !isLocalRoom(room) + !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()!) && + !isLocalRoom(room) ); }, runFn: function (roomId, args) { @@ -935,7 +935,7 @@ export const Commands = [ } const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent.getContent().users[args]) { + if (!powerLevelEvent?.getContent().users[args]) { return reject(newTranslatableError("Could not find user in room")); } return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); @@ -1042,7 +1042,7 @@ export const Commands = [ throw newTranslatableError("Session already verified!"); } else { throw newTranslatableError( - "WARNING: Session already verified, but keys do NOT MATCH!", + "WARNING: session already verified, but keys do NOT MATCH!", ); } } @@ -1116,9 +1116,9 @@ export const Commands = [ MatrixClientPeg.get().forceDiscardSession(roomId); return success( - room.getEncryptionTargetMembers().then((members) => { + room?.getEncryptionTargetMembers().then((members) => { // noinspection JSIgnoredPromiseFromCall - MatrixClientPeg.get().crypto.ensureOlmSessionsForUsers( + MatrixClientPeg.get().crypto?.ensureOlmSessionsForUsers( members.map((m) => m.userId), true, ); @@ -1170,7 +1170,7 @@ export const Commands = [ return reject(this.getUsage()); } - const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); + const member = MatrixClientPeg.get().getRoom(roomId)?.getMember(userId); dis.dispatch({ action: Action.ViewUser, // XXX: We should be using a real member object and not assuming what the receiver wants. @@ -1200,7 +1200,7 @@ export const Commands = [ description: _td("Switches to this room's virtual room, if it has one"), category: CommandCategories.advanced, isEnabled(): boolean { - return LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom(); + return !!LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom(); }, runFn: (roomId) => { return success( @@ -1390,7 +1390,7 @@ export function parseCommandString(input: string): { cmd?: string; args?: string const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); let cmd: string; - let args: string; + let args: string | undefined; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[2]; @@ -1415,7 +1415,7 @@ interface ICmd { export function getCommand(input: string): ICmd { const { cmd, args } = parseCommandString(input); - if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { + if (cmd && CommandMap.has(cmd) && CommandMap.get(cmd)!.isEnabled()) { return { cmd: CommandMap.get(cmd), args, diff --git a/src/Terms.ts b/src/Terms.ts index bb18a18cf7e..f66f543887c 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -52,11 +52,13 @@ export type Policies = { [policy: string]: Policy; }; +export type ServicePolicyPair = { + policies: Policies; + service: Service; +}; + export type TermsInteractionCallback = ( - policiesAndServicePairs: { - service: Service; - policies: Policies; - }[], + policiesAndServicePairs: ServicePolicyPair[], agreedUrls: string[], extraClassNames?: string, ) => Promise; @@ -117,9 +119,9 @@ export async function startTermsFlow( // but then they'd assume they can un-check the boxes to un-agree to a policy, // but that is not a thing the API supports, so probably best to just show // things they've not agreed to yet. - const unagreedPoliciesAndServicePairs = []; + const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = []; for (const { service, policies } of policiesAndServicePairs) { - const unagreedPolicies = {}; + const unagreedPolicies: Policies = {}; for (const [policyName, policy] of Object.entries(policies)) { let policyAgreed = false; for (const lang of Object.keys(policy)) { diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 7f874f8a898..790ce2571b1 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -33,7 +33,7 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import defaultDispatcher from "./dispatcher/dispatcher"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; -import AccessibleButton from "./components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; @@ -43,12 +43,12 @@ import { getSenderName } from "./utils/event/getSenderName"; function getRoomMemberDisplayname(event: MatrixEvent, userId = event.getSender()): string { const client = MatrixClientPeg.get(); const roomId = event.getRoomId(); - const member = client.getRoom(roomId)?.getMember(userId); + const member = client.getRoom(roomId)?.getMember(userId!); return member?.name || member?.rawDisplayName || userId || _t("Someone"); } function textForCallEvent(event: MatrixEvent): () => string { - const roomName = MatrixClientPeg.get().getRoom(event.getRoomId()!).name; + const roomName = MatrixClientPeg.get().getRoom(event.getRoomId()!)?.name; const isSupported = MatrixClientPeg.get().supportsVoip(); return isSupported @@ -60,7 +60,7 @@ function textForCallEvent(event: MatrixEvent): () => string { // any text to display at all. For this reason they return deferred values // to avoid the expense of looking up translations when they're not needed. -function textForCallInviteEvent(event: MatrixEvent): () => string | null { +function textForCallInviteEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); // FIXME: Find a better way to determine this from the event? const isVoice = !event.getContent().offer?.sdp?.includes("m=video"); @@ -78,9 +78,11 @@ function textForCallInviteEvent(event: MatrixEvent): () => string | null { } else if (!isVoice && !isSupported) { return () => _t("%(senderName)s placed a video call. (not supported by this browser)", { senderName }); } + + return null; } -function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null { +function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): (() => string) | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender?.name || getRoomMemberDisplayname(ev); const targetName = ev.target?.name || getRoomMemberDisplayname(ev, ev.getStateKey()); @@ -118,20 +120,20 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents // We're taking the display namke directly from the event content here so we need // to strip direction override chars which the js-sdk would normally do when // calculating the display name - oldDisplayName: removeDirectionOverrideChars(prevContent.displayname), - displayName: removeDirectionOverrideChars(content.displayname), + oldDisplayName: removeDirectionOverrideChars(prevContent.displayname!), + displayName: removeDirectionOverrideChars(content.displayname!), }); } else if (!prevContent.displayname && content.displayname) { return () => _t("%(senderName)s set their display name to %(displayName)s", { senderName: ev.getSender(), - displayName: removeDirectionOverrideChars(content.displayname), + displayName: removeDirectionOverrideChars(content.displayname!), }); } else if (prevContent.displayname && !content.displayname) { return () => _t("%(senderName)s removed their display name (%(oldDisplayName)s)", { senderName, - oldDisplayName: removeDirectionOverrideChars(prevContent.displayname), + oldDisplayName: removeDirectionOverrideChars(prevContent.displayname!), }); } else if (prevContent.avatar_url && !content.avatar_url) { return () => _t("%(senderName)s removed their profile picture", { senderName }); @@ -187,9 +189,11 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents return null; } } + + return null; } -function textForTopicEvent(ev: MatrixEvent): () => string | null { +function textForTopicEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { @@ -198,12 +202,12 @@ function textForTopicEvent(ev: MatrixEvent): () => string | null { }); } -function textForRoomAvatarEvent(ev: MatrixEvent): () => string | null { +function textForRoomAvatarEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev?.sender?.name || ev.getSender(); return () => _t("%(senderDisplayName)s changed the room avatar.", { senderDisplayName }); } -function textForRoomNameEvent(ev: MatrixEvent): () => string | null { +function textForRoomNameEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { @@ -224,7 +228,7 @@ function textForRoomNameEvent(ev: MatrixEvent): () => string | null { }); } -function textForTombstoneEvent(ev: MatrixEvent): () => string | null { +function textForTombstoneEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t("%(senderDisplayName)s upgraded this room.", { senderDisplayName }); } @@ -281,7 +285,7 @@ function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => Render } } -function textForGuestAccessEvent(ev: MatrixEvent): () => string | null { +function textForGuestAccessEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().guest_access) { case GuestAccess.CanJoin: @@ -298,7 +302,7 @@ function textForGuestAccessEvent(ev: MatrixEvent): () => string | null { } } -function textForServerACLEvent(ev: MatrixEvent): () => string | null { +function textForServerACLEvent(ev: MatrixEvent): (() => string) | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); const current = ev.getContent(); @@ -308,7 +312,7 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null { allow_ip_literals: prevContent.allow_ip_literals !== false, }; - let getText = null; + let getText: () => string; if (prev.deny.length === 0 && prev.allow.length === 0) { getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", { senderDisplayName }); } else { @@ -328,7 +332,7 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null { return getText; } -function textForMessageEvent(ev: MatrixEvent): () => string | null { +function textForMessageEvent(ev: MatrixEvent): (() => string) | null { if (isLocationEvent(ev)) { return textForLocationEvent(ev); } @@ -354,14 +358,14 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null { }; } -function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { +function textForCanonicalAliasEvent(ev: MatrixEvent): (() => string) | null { const senderName = getSenderName(ev); const oldAlias = ev.getPrevContent().alias; const oldAltAliases = ev.getPrevContent().alt_aliases || []; const newAlias = ev.getContent().alias; const newAltAliases = ev.getContent().alt_aliases || []; - const removedAltAliases = oldAltAliases.filter((alias) => !newAltAliases.includes(alias)); - const addedAltAliases = newAltAliases.filter((alias) => !oldAltAliases.includes(alias)); + const removedAltAliases = oldAltAliases.filter((alias: string) => !newAltAliases.includes(alias)); + const addedAltAliases = newAltAliases.filter((alias: string) => !oldAltAliases.includes(alias)); if (!removedAltAliases.length && !addedAltAliases.length) { if (newAlias) { @@ -414,7 +418,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { }); } -function textForThreePidInviteEvent(event: MatrixEvent): () => string | null { +function textForThreePidInviteEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); if (!isValid3pidInvite(event)) { @@ -432,7 +436,7 @@ function textForThreePidInviteEvent(event: MatrixEvent): () => string | null { }); } -function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null { +function textForHistoryVisibilityEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); switch (event.getContent().history_visibility) { case HistoryVisibility.Invited: @@ -463,7 +467,7 @@ function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null } // Currently will only display a change if a user's power level is changed -function textForPowerEvent(event: MatrixEvent): () => string | null { +function textForPowerEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); if (!event.getPrevContent()?.users || !event.getContent()?.users) { return null; @@ -528,20 +532,20 @@ const onPinnedMessagesClick = (): void => { RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); }; -function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Renderable { +function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): (() => Renderable) | null { if (!SettingsStore.getValue("feature_pinning")) return null; const senderName = getSenderName(event); - const roomId = event.getRoomId(); + const roomId = event.getRoomId()!; - const pinned = event.getContent().pinned ?? []; - const previouslyPinned = event.getPrevContent().pinned ?? []; + const pinned = event.getContent<{ pinned: string[] }>().pinned ?? []; + const previouslyPinned: string[] = event.getPrevContent().pinned ?? []; const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0); const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0); if (newlyPinned.length === 1 && newlyUnpinned.length === 0) { // A single message was pinned, include a link to that message. if (allowJSX) { - const messageId = newlyPinned.pop(); + const messageId = newlyPinned.pop()!; return () => ( @@ -550,7 +554,10 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render { senderName }, { a: (sub) => ( - highlightEvent(roomId, messageId)}> + highlightEvent(roomId, messageId)} + > {sub} ), @@ -571,7 +578,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render if (newlyUnpinned.length === 1 && newlyPinned.length === 0) { // A single message was unpinned, include a link to that message. if (allowJSX) { - const messageId = newlyUnpinned.pop(); + const messageId = newlyUnpinned.pop()!; return () => ( @@ -580,7 +587,10 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render { senderName }, { a: (sub) => ( - highlightEvent(roomId, messageId)}> + highlightEvent(roomId, messageId)} + > {sub} ), @@ -619,7 +629,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName }); } -function textForWidgetEvent(event: MatrixEvent): () => string | null { +function textForWidgetEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent(); const { name, type, url } = event.getContent() || {}; @@ -655,12 +665,12 @@ function textForWidgetEvent(event: MatrixEvent): () => string | null { } } -function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null { +function textForWidgetLayoutEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); return () => _t("%(senderName)s has updated the room layout", { senderName }); } -function textForMjolnirEvent(event: MatrixEvent): () => string | null { +function textForMjolnirEvent(event: MatrixEvent): (() => string) | null { const senderName = getSenderName(event); const { entity: prevEntity } = event.getPrevContent(); const { entity, recommendation, reason } = event.getContent(); @@ -789,7 +799,7 @@ function textForMjolnirEvent(event: MatrixEvent): () => string | null { ); } -export function textForLocationEvent(event: MatrixEvent): () => string | null { +export function textForLocationEvent(event: MatrixEvent): () => string { return () => _t("%(senderName)s has shared their location", { senderName: getSenderName(event), @@ -811,7 +821,7 @@ function textForRedactedPollAndMessageEvent(ev: MatrixEvent): string { return message; } -function textForPollStartEvent(event: MatrixEvent): () => string | null { +function textForPollStartEvent(event: MatrixEvent): (() => string) | null { return () => { let message = ""; @@ -830,7 +840,7 @@ function textForPollStartEvent(event: MatrixEvent): () => string | null { }; } -function textForPollEndEvent(event: MatrixEvent): () => string | null { +function textForPollEndEvent(event: MatrixEvent): (() => string) | null { return () => _t("%(senderName)s has ended a poll", { senderName: getSenderName(event), @@ -840,7 +850,7 @@ function textForPollEndEvent(event: MatrixEvent): () => string | null { type Renderable = string | React.ReactNode | null; interface IHandlers { - [type: string]: (ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) => () => Renderable; + [type: string]: (ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) => (() => Renderable) | null; } const handlers: IHandlers = { diff --git a/src/Unread.ts b/src/Unread.ts index 6e7218cad12..1fd7ac449fa 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2023 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. diff --git a/src/UserActivity.ts b/src/UserActivity.ts index 9217aca3c0e..ae6417d4f4d 100644 --- a/src/UserActivity.ts +++ b/src/UserActivity.ts @@ -168,7 +168,7 @@ export default class UserActivity { return this.activeRecentlyTimeout.isRunning(); } - private onPageVisibilityChanged = (e): void => { + private onPageVisibilityChanged = (e: Event): void => { if (this.document.visibilityState === "hidden") { this.activeNowTimeout.abort(); this.activeRecentlyTimeout.abort(); @@ -182,11 +182,12 @@ export default class UserActivity { this.activeRecentlyTimeout.abort(); }; - private onUserActivity = (event: MouseEvent): void => { + // XXX: exported for tests + public onUserActivity = (event: Event): void => { // ignore anything if the window isn't focused if (!this.document.hasFocus()) return; - if (event.screenX && event.type === "mousemove") { + if (event.type === "mousemove" && this.isMouseEvent(event)) { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { // mouse hasn't actually moved return; @@ -223,4 +224,8 @@ export default class UserActivity { } attachedTimers.forEach((t) => t.abort()); } + + private isMouseEvent(event: Event): event is MouseEvent { + return event.type.startsWith("mouse"); + } } diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 7a163350db8..0214ab9cbed 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -38,7 +38,7 @@ export default class VoipUserMapper { return window.mxVoipUserMapper; } - private async userToVirtualUser(userId: string): Promise { + private async userToVirtualUser(userId: string): Promise { const results = await LegacyCallHandler.instance.sipVirtualLookup(userId); if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; @@ -59,11 +59,11 @@ export default class VoipUserMapper { if (!virtualUser) return null; const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); - MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + MatrixClientPeg.get().setRoomAccountData(virtualRoomId!, VIRTUAL_ROOM_EVENT_TYPE, { native_room: roomId, }); - this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId); + this.virtualToNativeRoomIdCache.set(virtualRoomId!, roomId); return virtualRoomId; } @@ -72,9 +72,9 @@ export default class VoipUserMapper { * Gets the ID of the virtual room for a room, or null if the room has no * virtual room */ - public async getVirtualRoomForRoom(roomId: string): Promise { + public async getVirtualRoomForRoom(roomId: string): Promise { const virtualUser = await this.getVirtualUserForRoom(roomId); - if (!virtualUser) return null; + if (!virtualUser) return undefined; return findDMForUser(MatrixClientPeg.get(), virtualUser); } @@ -121,8 +121,12 @@ export default class VoipUserMapper { if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); + if (!inviterId) { + logger.error("Could not find DM inviter for room id: " + invitedRoom.roomId); + } + logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); - const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId); + const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId!); if (result.length === 0) { return; } @@ -141,11 +145,11 @@ export default class VoipUserMapper { // (possibly we should only join if we've also joined the native room, then we'd also have // to make sure we joined virtual rooms on joining a native one) MatrixClientPeg.get().joinRoom(invitedRoom.roomId); - } - // also put this room in the virtual room ID cache so isVirtualRoom return the right answer - // in however long it takes for the echo of setAccountData to come down the sync - this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); + // also put this room in the virtual room ID cache so isVirtualRoom return the right answer + // in however long it takes for the echo of setAccountData to come down the sync + this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); + } } } } diff --git a/src/WhoIsTyping.ts b/src/WhoIsTyping.ts index 01e7b2e4f7d..d4a43636cea 100644 --- a/src/WhoIsTyping.ts +++ b/src/WhoIsTyping.ts @@ -21,11 +21,11 @@ import { MatrixClientPeg } from "./MatrixClientPeg"; import { _t } from "./languageHandler"; export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] { - return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers())); + return usersTyping(room, [MatrixClientPeg.get().getUserId()!].concat(MatrixClientPeg.get().getIgnoredUsers())); } export function usersTypingApartFromMe(room: Room): RoomMember[] { - return usersTyping(room, [MatrixClientPeg.get().getUserId()]); + return usersTyping(room, [MatrixClientPeg.get().getUserId()!]); } /** @@ -36,7 +36,7 @@ export function usersTypingApartFromMe(room: Room): RoomMember[] { * @returns {RoomMember[]} list of user objects who are typing. */ export function usersTyping(room: Room, exclude: string[] = []): RoomMember[] { - const whoIsTyping = []; + const whoIsTyping: RoomMember[] = []; const memberKeys = Object.keys(room.currentState.members); for (const userId of memberKeys) { diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index bb42f7c1ce9..8ba866be3fa 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -27,6 +27,7 @@ import { KEYBOARD_SHORTCUTS, MAC_ONLY_SHORTCUTS, } from "./KeyboardShortcuts"; +import { IBaseSetting } from "../settings/Settings"; /** * This function gets the keyboard shortcuts that should be presented in the UI @@ -103,7 +104,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { return true; }) .reduce((o, key) => { - o[key] = KEYBOARD_SHORTCUTS[key]; + o[key as KeyBindingAction] = KEYBOARD_SHORTCUTS[key as KeyBindingAction]; return o; }, {} as IKeyboardShortcuts); }; @@ -112,7 +113,10 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { * Gets keyboard shortcuts that should be presented to the user in the UI. */ export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { - const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())]; + const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [ + KeyBindingAction, + IBaseSetting, + ][]; return entries.reduce((acc, [key, value]) => { acc[key] = value; @@ -120,11 +124,11 @@ export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { }, {} as IKeyboardShortcuts); }; -export const getKeyboardShortcutValue = (name: string): KeyCombo | undefined => { +export const getKeyboardShortcutValue = (name: KeyBindingAction): KeyCombo | undefined => { return getKeyboardShortcutsForUI()[name]?.default; }; -export const getKeyboardShortcutDisplayName = (name: string): string | undefined => { +export const getKeyboardShortcutDisplayName = (name: KeyBindingAction): string | undefined => { const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; - return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName as string); }; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 0e536ac1497..3011a5b5bd7 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -156,10 +156,8 @@ export enum KeyBindingAction { type KeyboardShortcutSetting = IBaseSetting; -export type IKeyboardShortcuts = { - // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager - [k in KeyBindingAction]?: KeyboardShortcutSetting; -}; +// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager +export type IKeyboardShortcuts = Partial>; export interface ICategory { categoryLabel?: string; diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 605ffb1f5b5..b449b10710f 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -25,6 +25,7 @@ import React, { Reducer, Dispatch, RefObject, + ReactNode, } from "react"; import { getKeyBindingsManager } from "../KeyBindingsManager"; @@ -158,8 +159,8 @@ interface IProps { handleHomeEnd?: boolean; handleUpDown?: boolean; handleLeftRight?: boolean; - children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent) }); - onKeyDown?(ev: React.KeyboardEvent, state: IState); + children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent): void }): ReactNode; + onKeyDown?(ev: React.KeyboardEvent, state: IState): void; } export const findSiblingElement = ( diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index ee3a0e4d368..e8e69865d78 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -25,7 +25,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; - onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton } diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index 2fe87384340..7a394a3d1f9 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -25,7 +25,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; - onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent onClose(): void; // gets called after onChange on KeyBindingAction.Enter } diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 3968ef6d6bd..71818c6cda1 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -30,7 +30,7 @@ export const RovingAccessibleButton: React.FC = ({ inputRef, onFocus, .. return ( { + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index f30225f0f72..f06cc934bbc 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -31,7 +31,7 @@ export const RovingAccessibleTooltipButton: React.FC = ({ inputRef, onFo return ( { + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index b549f18119e..4208d47499f 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { useRovingTabIndex } from "../RovingTabIndex"; import { FocusHandler, Ref } from "./types"; interface IProps { inputRef?: Ref; - children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref }); + children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref }): ReactElement; } // Wrapper to allow use of useRovingTabIndex outside of React Functional Components. diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index 637b57071e4..49351757ca7 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -54,7 +54,7 @@ export default class RoomListActions { oldIndex: number | null, newIndex: number | null, ): AsyncActionPayload { - let metaData = null; + let metaData: Parameters[2] | null = null; // Is the tag ordered manually? const store = RoomListStore.instance; @@ -81,7 +81,7 @@ export default class RoomListActions { return asyncAction( "RoomListActions.tagRoom", () => { - const promises = []; + const promises: Promise[] = []; const roomId = room.roomId; // Evil hack to get DMs behaving @@ -120,7 +120,7 @@ export default class RoomListActions { if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData)) { // metaData is the body of the PUT to set the tag, so it must // at least be an empty object. - metaData = metaData || {}; + metaData = metaData || ({} as typeof metaData); const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { logger.error("Failed to add tag " + newTag + " to room: " + err); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 63a132077fa..5393ae3fc6e 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../../languageHandler"; import SdkConfig from "../../../../SdkConfig"; @@ -27,6 +28,7 @@ import Field from "../../../../components/views/elements/Field"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import { IIndexStats } from "../../../../indexing/BaseEventIndexManager"; interface IProps extends IDialogProps {} @@ -43,7 +45,7 @@ interface IState { * Allows the user to introspect the event index state and disable it. */ export default class ManageEventIndexDialog extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -56,9 +58,9 @@ export default class ManageEventIndexDialog extends React.Component => { + public updateCurrentRoom = async (room: Room): Promise => { const eventIndex = EventIndexPeg.get(); - let stats; + let stats: IIndexStats; try { stats = await eventIndex.getStats(); @@ -136,12 +138,12 @@ export default class ManageEventIndexDialog extends React.Component { - this.setState({ crawlerSleepTime: e.target.value }); + private onCrawlerSleepTimeChange = (e: ChangeEvent): void => { + this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) }); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; - public render(): JSX.Element { + public render(): React.ReactNode { const brand = SdkConfig.get().brand; let crawlerState; diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index a75b41f602b..a9327f2a467 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -239,7 +239,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

{_t( - "Warning: You should only set up key backup from a trusted computer.", + "Warning: you should only set up key backup from a trusted computer.", {}, { b: (sub) => {sub} }, )} @@ -327,7 +327,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

@@ -451,7 +451,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { return f.stages.length === 1 && f.stages[0] === "m.login.password"; }); this.setState({ @@ -361,7 +364,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent

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

@@ -785,6 +791,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent +

{_t("Your keys are now being backed up from this device.")}

+ this.props.onFinished(true)} + hasCancel={false} + /> + + ); + } + private renderPhaseLoadError(): JSX.Element { return (
@@ -837,12 +856,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent; + } + + return null; + } + + private get classNames(): string { + return classNames("mx_CreateSecretStorageDialog", { + mx_SuccessDialog: this.state.phase === Phase.Stored, + }); + } + + public render(): React.ReactNode { let content; if (this.state.error) { content = ( @@ -884,6 +919,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent } as Pick); }; - public render(): JSX.Element { + public render(): React.ReactNode { const disableForm = this.state.phase === Phase.Exporting; return ( @@ -163,7 +163,9 @@ export default class ExportE2eKeysDialog extends React.Component this.onPassphraseChange(e, "passphrase1")} + onChange={(e: ChangeEvent) => + this.onPassphraseChange(e, "passphrase1") + } autoFocus={true} size={64} type="password" @@ -174,7 +176,9 @@ export default class ExportE2eKeysDialog extends React.Component this.onPassphraseChange(e, "passphrase2")} + onChange={(e: ChangeEvent) => + this.onPassphraseChange(e, "passphrase2") + } size={64} type="password" disabled={disableForm} diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index 079271b0213..5d3864e811c 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -73,9 +73,9 @@ export default class ImportE2eKeysDialog extends React.Component } private onFormChange = (): void => { - const files = this.file.current.files || []; + const files = this.file.current.files; this.setState({ - enableSubmit: this.state.passphrase !== "" && files.length > 0, + enableSubmit: this.state.passphrase !== "" && !!files?.length, }); }; @@ -127,7 +127,7 @@ export default class ImportE2eKeysDialog extends React.Component return false; }; - public render(): JSX.Element { + public render(): React.ReactNode { const disableForm = this.state.phase !== Phase.Edit; return ( diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index fbe94cb8e60..fbb823dd8ec 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -48,13 +48,13 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onFinished: this.props.onFinished, }, - null, + undefined, /* priority = */ false, /* static = */ true, ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const title = {_t("New Recovery Method")}; const newMethodDetected =

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

; diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index 1a7e79b9d20..f795a3e534e 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -37,14 +37,14 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent>, - null, + undefined, null, /* priority = */ false, /* static = */ true, ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const title = {_t("Recovery Method Removed")}; return ( diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 0828a3df1d8..939d76d0a5f 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -91,7 +91,7 @@ export class PlaybackQueue { public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback): void { // We don't ever detach our listeners: we expect the Playback to clean up for us - this.playbacks.set(mxEvent.getId(), playback); + this.playbacks.set(mxEvent.getId()!, playback); playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state)); playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock)); } @@ -99,12 +99,12 @@ export class PlaybackQueue { private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState): void { // Remember where the user got to in playback const wasLastPlaying = this.currentPlaybackId === mxEvent.getId(); - if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) { + if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()!) && !wasLastPlaying) { // noinspection JSIgnoredPromiseFromCall - playback.skipTo(this.clockStates.get(mxEvent.getId())!); + playback.skipTo(this.clockStates.get(mxEvent.getId()!)!); } else if (newState === PlaybackState.Stopped) { // Remove the now-useless clock for some space savings - this.clockStates.delete(mxEvent.getId()); + this.clockStates.delete(mxEvent.getId()!); if (wasLastPlaying) { this.recentFullPlays.add(this.currentPlaybackId); @@ -133,7 +133,7 @@ export class PlaybackQueue { // timeline is already most recent last, so we can iterate down that. const timeline = arrayFastClone(this.room.getLiveTimeline().getEvents()); let scanForVoiceMessage = false; - let nextEv: MatrixEvent; + let nextEv: MatrixEvent | undefined; for (const event of timeline) { if (event.getId() === mxEvent.getId()) { scanForVoiceMessage = true; @@ -149,8 +149,8 @@ export class PlaybackQueue { break; // Stop automatic playback: next useful event is not a voice message } - const havePlayback = this.playbacks.has(event.getId()); - const isRecentlyCompleted = this.recentFullPlays.has(event.getId()); + const havePlayback = this.playbacks.has(event.getId()!); + const isRecentlyCompleted = this.recentFullPlays.has(event.getId()!); if (havePlayback && !isRecentlyCompleted) { nextEv = event; break; @@ -164,7 +164,7 @@ export class PlaybackQueue { } else { this.playbackIdOrder = orderClone; - const instance = this.playbacks.get(nextEv.getId()); + const instance = this.playbacks.get(nextEv.getId()!); PlaybackManager.instance.pauseAllExcept(instance); // This should cause a Play event, which will re-populate our playback order @@ -196,7 +196,7 @@ export class PlaybackQueue { } } - this.currentPlaybackId = mxEvent.getId(); + this.currentPlaybackId = mxEvent.getId()!; if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { order.push(this.currentPlaybackId); } @@ -214,7 +214,7 @@ export class PlaybackQueue { if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values if (playback.currentState !== PlaybackState.Stopped) { - this.clockStates.set(mxEvent.getId(), clocks[0]); // [0] is the current seek position + this.clockStates.set(mxEvent.getId()!, clocks[0]); // [0] is the current seek position } } } diff --git a/src/audio/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts index 0c0cc56cd68..3cb9cf03d28 100644 --- a/src/audio/RecorderWorklet.ts +++ b/src/audio/RecorderWorklet.ts @@ -43,7 +43,11 @@ class MxVoiceWorklet extends AudioWorkletProcessor { private nextAmplitudeSecond = 0; private amplitudeIndex = 0; - public process(inputs, outputs, parameters): boolean { + public process( + inputs: Float32Array[][], + outputs: Float32Array[][], + parameters: Record, + ): boolean { const currentSecond = roundTimeToTargetFreq(currentTime); // We special case the first ping because there's a fairly good chance that we'll miss the zeroth // update. Firefox for instance takes 0.06 seconds (roughly) to call this function for the first diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index 32fcb5a97ab..f4d7905c333 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// @ts-ignore import Recorder from "opus-recorder/dist/recorder.min.js"; import encoderPath from "opus-recorder/dist/encoderWorker.min.js"; import { SimpleObservable } from "matrix-widget-api"; @@ -78,7 +77,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private targetMaxLength: number | null = TARGET_MAX_LENGTH; public amplitudes: number[] = []; // at each second mark, generated private liveWaveform = new FixedRollingArray(RECORDING_PLAYBACK_SAMPLES, 0); - public onDataAvailable: (data: ArrayBuffer) => void; + public onDataAvailable?: (data: ArrayBuffer) => void; public get contentType(): string { return "audio/ogg"; @@ -182,7 +181,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }); // not using EventEmitter here because it leads to detached bufferes - this.recorder.ondataavailable = (data: ArrayBuffer) => this?.onDataAvailable(data); + this.recorder.ondataavailable = (data: ArrayBuffer) => this.onDataAvailable?.(data); } catch (e) { logger.error("Error starting recording: ", e); if (e instanceof DOMException) { diff --git a/src/audio/compat.ts b/src/audio/compat.ts index ab63f644a19..ce0fc30816d 100644 --- a/src/audio/compat.ts +++ b/src/audio/compat.ts @@ -69,7 +69,7 @@ export function decodeOgg(audioBuffer: ArrayBuffer): Promise { command: "encode", buffers: ev.data, }, - ev.data.map((b) => b.buffer), + ev.data.map((b: Float32Array) => b.buffer), ); }; diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 32c2d80c6fc..0b1fe2dd9e8 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -70,7 +70,7 @@ export default abstract class AutocompleteProvider { * @param {boolean} force True if the user is forcing completion * @return {object} { command, range } where both objects fields are null if no match */ - public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand | null { + public getCurrentCommand(query: string, selection: ISelectionRange, force = false): Partial { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { @@ -78,7 +78,7 @@ export default abstract class AutocompleteProvider { } if (!commandRegex) { - return null; + return {}; } commandRegex.lastIndex = 0; diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index 113c9287901..995760f4b33 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -95,7 +95,7 @@ export default class CommandProvider extends AutocompleteProvider { description={_t(result.description)} /> ), - range, + range: range!, }; }); } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 3d76a366d4b..7257a7a1ee5 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -59,7 +59,11 @@ const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => { _orderBy: index, })); -function score(query: string, space: string): number { +function score(query: string, space: string[] | string): number { + if (Array.isArray(space)) { + return Math.min(...space.map((s) => score(query, s))); + } + const index = space.indexOf(query); if (index === -1) { return Infinity; @@ -122,7 +126,7 @@ export default class EmojiProvider extends AutocompleteProvider { shouldMatchWordsOnly: true, }); - this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); + this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))) as (IEmoji | ICustomEmoji)[]; } public async getCompletions( @@ -189,7 +193,7 @@ export default class EmojiProvider extends AutocompleteProvider { {c.emoji.unicode} ), - range, + range: range!, }; } else { let mediaUrl; @@ -210,7 +214,7 @@ export default class EmojiProvider extends AutocompleteProvider { {c.emoji.shortcodes[0]} ), - range, + range: range!, } as const; } }) diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 5efe0e86f60..d4a4793ab51 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -40,12 +40,13 @@ export default class NotifProvider extends AutocompleteProvider { ): Promise { const client = MatrixClientPeg.get(); - if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId)) return []; + if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId!)) return []; const { command, range } = this.getCurrentCommand(query, selection, force); if ( - command?.[0].length > 1 && - ["@room", "@channel", "@everyone", "@here"].some((c) => c.startsWith(command[0])) + command?.[0] && + command[0].length > 1 && + ["@room", "@channel", "@everyone", "@here"].some((c) => c.startsWith(command![0])) ) { return [ { @@ -58,7 +59,7 @@ export default class NotifProvider extends AutocompleteProvider { ), - range, + range: range!, }, ]; } diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 1f7b5a5a7f9..031f0b0122f 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -88,7 +88,7 @@ export default class QueryMatcher { if (!this._items.has(key)) { this._items.set(key, []); } - this._items.get(key).push({ + this._items.get(key)!.push({ keyWeight: Number(index), object, }); @@ -104,7 +104,11 @@ export default class QueryMatcher { if (query.length === 0) { return []; } - const matches = []; + const matches: { + index: number; + object: T; + keyWeight: number; + }[] = []; // Iterate through the map & check each key. // ES6 Map iteration order is defined to be insertion order, so results // here will come out in the order they were put in. diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 9cde5013650..3b86f16f1c3 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -39,12 +39,12 @@ function canonicalScore(displayedAlias: string, room: Room): number { function matcherObject( room: Room, - displayedAlias: string | null, + displayedAlias: string, matchName = "", ): { room: Room; matchName: string; - displayedAlias: string | null; + displayedAlias: string; } { return { room, @@ -81,7 +81,7 @@ export default class RoomProvider extends AutocompleteProvider { // the only reason we need to do this is because Fuse only matches on properties let matcherObjects = this.getRooms().reduce[]>((aliases, room) => { if (room.getCanonicalAlias()) { - aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); + aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias()!, room.name)); } if (room.getAltAliases().length) { const altAliases = room.getAltAliases().map((alias) => matcherObject(room, alias)); @@ -122,7 +122,7 @@ export default class RoomProvider extends AutocompleteProvider { ), - range, + range: range!, }), ) .filter((completion) => !!completion.completion && completion.completion.length > 0); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 65de4b1bb4f..0ba5f656d8f 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -44,7 +44,7 @@ const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g; export default class UserProvider extends AutocompleteProvider { public matcher: QueryMatcher; - public users: RoomMember[]; + public users: RoomMember[] | null; public room: Room; public constructor(room: Room, renderingType?: TimelineRenderingType) { @@ -54,7 +54,7 @@ export default class UserProvider extends AutocompleteProvider { renderingType, }); this.room = room; - this.matcher = new QueryMatcher([], { + this.matcher = new QueryMatcher([], { keys: ["name"], funcs: [(obj) => obj.userId.slice(1)], // index by user id minus the leading '@' shouldMatchWordsOnly: false, @@ -73,7 +73,7 @@ export default class UserProvider extends AutocompleteProvider { private onRoomTimeline = ( ev: MatrixEvent, - room: Room | null, + room: Room | undefined, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData, @@ -110,18 +110,15 @@ export default class UserProvider extends AutocompleteProvider { // lazy-load user list into matcher if (!this.users) this.makeUsers(); - let completions = []; const { command, range } = this.getCurrentCommand(rawQuery, selection, force); - if (!command) return completions; - - const fullMatch = command[0]; + const fullMatch = command?.[0]; // Don't search if the query is a single "@" if (fullMatch && fullMatch !== "@") { // Don't include the '@' in our search query - it's only used as a way to trigger completion const query = fullMatch.startsWith("@") ? fullMatch.substring(1) : fullMatch; - completions = this.matcher.match(query, limit).map((user) => { - const description = UserIdentifierCustomisations.getDisplayUserIdentifier(user.userId, { + return this.matcher.match(query, limit).map((user) => { + const description = UserIdentifierCustomisations.getDisplayUserIdentifier?.(user.userId, { roomId: this.room.roomId, withDisplayName: true, }); @@ -132,18 +129,18 @@ export default class UserProvider extends AutocompleteProvider { completion: user.rawDisplayName, completionId: user.userId, type: "user", - suffix: selection.beginning && range.start === 0 ? ": " : " ", + suffix: selection.beginning && range!.start === 0 ? ": " : " ", href: makeUserPermalink(user.userId), component: ( ), - range, + range: range!, }; }); } - return completions; + return []; } public getName(): string { @@ -152,10 +149,10 @@ export default class UserProvider extends AutocompleteProvider { private makeUsers(): void { const events = this.room.getLiveTimeline().getEvents(); - const lastSpoken = {}; + const lastSpoken: Record = {}; for (const event of events) { - lastSpoken[event.getSender()] = event.getTs(); + lastSpoken[event.getSender()!] = event.getTs(); } const currentUserId = MatrixClientPeg.get().credentials.userId; @@ -167,7 +164,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - public onUserSpoke(user: RoomMember): void { + public onUserSpoke(user: RoomMember | null): void { if (!this.users) return; if (!user) return; if (user.userId === MatrixClientPeg.get().credentials.userId) return; diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 90fda3fe215..efbbbccb55a 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -55,7 +55,7 @@ export default class AutoHideScrollbar ex } } - public render(): JSX.Element { + public render(): React.ReactNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 978dd07be91..5e66c883e78 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -99,13 +99,13 @@ export interface IProps extends MenuProps { closeOnInteraction?: boolean; // Function to be called on menu close - onFinished(); + onFinished(): void; // on resize callback - windowResize?(); + windowResize?(): void; } interface IState { - contextMenuElem: HTMLDivElement; + contextMenuElem?: HTMLDivElement; } // Generic ContextMenu Portal wrapper @@ -119,12 +119,10 @@ export default class ContextMenu extends React.PureComponent { managed: true, }; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); - this.state = { - contextMenuElem: null, - }; + this.state = {}; // persist what had focus when we got initialized so we can return it after this.initialFocus = document.activeElement as HTMLElement; @@ -181,7 +179,7 @@ export default class ContextMenu extends React.PureComponent { button: 0, // Left relatedTarget: null, }); - document.elementFromPoint(x, y).dispatchEvent(clickEvent); + document.elementFromPoint(x, y)?.dispatchEvent(clickEvent); }); } }; @@ -239,7 +237,7 @@ export default class ContextMenu extends React.PureComponent { // MessageActionBar), we should close any ContextMenu that is open. KeyBindingAction.ArrowLeft, KeyBindingAction.ArrowRight, - ].includes(action) + ].includes(action!) ) { this.props.onFinished(); } @@ -312,12 +310,12 @@ export default class ContextMenu extends React.PureComponent { position.top = Math.min(position.top, maxTop); // Adjust the chevron if necessary if (chevronOffset.top !== undefined) { - chevronOffset.top = propsChevronOffset + top - position.top; + chevronOffset.top = propsChevronOffset! + top! - position.top; } } else if (position.bottom !== undefined) { position.bottom = Math.min(position.bottom, windowHeight - contextMenuRect.height - WINDOW_PADDING); if (chevronOffset.top !== undefined) { - chevronOffset.top = propsChevronOffset + position.bottom - bottom; + chevronOffset.top = propsChevronOffset! + position.bottom - bottom!; } } if (position.left !== undefined) { @@ -327,12 +325,12 @@ export default class ContextMenu extends React.PureComponent { } position.left = Math.min(position.left, maxLeft); if (chevronOffset.left !== undefined) { - chevronOffset.left = propsChevronOffset + left - position.left; + chevronOffset.left = propsChevronOffset! + left! - position.left; } } else if (position.right !== undefined) { position.right = Math.min(position.right, windowWidth - contextMenuRect.width - WINDOW_PADDING); if (chevronOffset.left !== undefined) { - chevronOffset.left = propsChevronOffset + position.right - right; + chevronOffset.left = propsChevronOffset! + position.right - right!; } } } @@ -387,13 +385,13 @@ export default class ContextMenu extends React.PureComponent { menuStyle["paddingRight"] = menuPaddingRight; } - const wrapperStyle = {}; + const wrapperStyle: CSSProperties = {}; if (!isNaN(Number(zIndex))) { - menuStyle["zIndex"] = zIndex + 1; + menuStyle["zIndex"] = zIndex! + 1; wrapperStyle["zIndex"] = zIndex; } - let background; + let background: JSX.Element; if (hasBackground) { background = (
, ): { close: (...args: any[]) => void } { - const onFinished = function (...args): void { + const onFinished = function (...args: any[]): void { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); props?.onFinished?.apply(null, args); }; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index e3cacf01146..cbd7d885c7e 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -118,7 +118,7 @@ export default class EmbeddedPage extends React.PureComponent { } }; - public render(): JSX.Element { + public render(): React.ReactNode { // HACK: Workaround for the context's MatrixClient not updating. const client = this.context || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 03ec6eb4335..ccbb7621357 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -45,7 +45,7 @@ interface IProps { } interface IState { - timelineSet: EventTimelineSet; + timelineSet: EventTimelineSet | null; narrow: boolean; } @@ -61,7 +61,7 @@ class FilePanel extends React.Component { public noRoom: boolean; private card = createRef(); - public state = { + public state: IState = { timelineSet: null, narrow: false, }; @@ -225,7 +225,7 @@ class FilePanel extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { if (MatrixClientPeg.get().isGuest()) { return ( diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 6b727c5140d..98dfbf0851f 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -79,6 +79,12 @@ export function GenericDropdownMenuGroup({ ); } +function isGenericDropdownMenuGroupArray( + items: readonly GenericDropdownMenuItem[], +): items is GenericDropdownMenuGroup[] { + return isGenericDropdownMenuGroup(items[0]); +} + function isGenericDropdownMenuGroup(item: GenericDropdownMenuItem): item is GenericDropdownMenuGroup { return "options" in item; } @@ -123,19 +129,19 @@ export function GenericDropdownMenu({ .flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it])) .find((option) => (toKey ? toKey(option.key) === toKey(value) : option.key === value)); let contextMenuOptions: JSX.Element; - if (options && isGenericDropdownMenuGroup(options[0])) { + if (options && isGenericDropdownMenuGroupArray(options)) { contextMenuOptions = ( <> {options.map((group) => ( {group.options.map((option) => ( { @@ -156,7 +162,7 @@ export function GenericDropdownMenu({ <> {options.map((option) => ( { diff --git a/src/components/structures/GenericErrorPage.tsx b/src/components/structures/GenericErrorPage.tsx index 4261d9b2f43..4f348daf014 100644 --- a/src/components/structures/GenericErrorPage.tsx +++ b/src/components/structures/GenericErrorPage.tsx @@ -22,7 +22,7 @@ interface IProps { } export default class GenericErrorPage extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return (
diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 1476198239a..c7cde582d3f 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -177,7 +177,7 @@ export default class IndicatorScrollbar e } }; - public render(): JSX.Element { + public render(): React.ReactNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props; diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 99be8705a4d..fd83f0aea24 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -80,7 +80,7 @@ interface IProps { // Called when the stage changes, or the stage's phase changes. First // argument is the stage, second is the phase. Some stages do not have // phases and will be counted as 0 (numeric). - onStagePhaseChange?(stage: string, phase: string | number): void; + onStagePhaseChange?(stage: AuthType, phase: number): void; } interface IState { @@ -99,7 +99,7 @@ export default class InteractiveAuthComponent extends React.Component { private listContainerRef = createRef(); private roomListRef = createRef(); - private focusedElement = null; + private focusedElement: Element | null = null; private isDoingStickyHeaders = false; public constructor(props: IProps) { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 807a9222122..165957e09c1 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -141,8 +141,8 @@ class LoggedInView extends React.Component { protected backgroundImageWatcherRef: string; protected resizer: Resizer; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { syncErrorData: undefined, @@ -241,8 +241,8 @@ class LoggedInView extends React.Component { }; private createResizer(): Resizer { - let panelSize; - let panelCollapsed; + let panelSize: number; + let panelCollapsed: boolean; const collapseConfig: ICollapseConfig = { // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel toggleSize: 206 - 50, @@ -364,7 +364,7 @@ class LoggedInView extends React.Component { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; if (!serverNoticeList) return; - const events = []; + const events: MatrixEvent[] = []; let pinnedEventTs = 0; for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); @@ -392,7 +392,7 @@ class LoggedInView extends React.Component { e.getContent()["server_notice_type"] === "m.server_notice.usage_limit_reached" ); }); - const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); + const usageLimitEventContent = usageLimitEvent?.getContent(); this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); this.setState({ usageLimitEventContent, @@ -445,13 +445,13 @@ class LoggedInView extends React.Component { We also listen with a native listener on the document to get keydown events when no element is focused. Bubbling is irrelevant here as the target is the body element. */ - private onReactKeyDown = (ev): void => { + private onReactKeyDown = (ev: React.KeyboardEvent): void => { // events caught while bubbling up on the root element // of this component, so something must be focused. this.onKeyDown(ev); }; - private onNativeKeyDown = (ev): void => { + private onNativeKeyDown = (ev: KeyboardEvent): void => { // only pass this if there is no focused element. // if there is, onKeyDown will be called by the // react keydown handler that respects the react bubbling order. @@ -460,7 +460,7 @@ class LoggedInView extends React.Component { } }; - private onKeyDown = (ev): void => { + private onKeyDown = (ev: React.KeyboardEvent | KeyboardEvent): void => { let handled = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); @@ -594,7 +594,7 @@ class LoggedInView extends React.Component { ) { dis.dispatch({ action: Action.SwitchSpace, - num: ev.code.slice(5), // Cut off the first 5 characters - "Digit" + num: parseInt(ev.code.slice(5), 10), // Cut off the first 5 characters - "Digit" }); handled = true; } @@ -638,13 +638,11 @@ class LoggedInView extends React.Component { * dispatch a page-up/page-down/etc to the appropriate component * @param {Object} ev The key event */ - private onScrollKeyPressed = (ev): void => { - if (this._roomView.current) { - this._roomView.current.handleScrollKey(ev); - } + private onScrollKeyPressed = (ev: React.KeyboardEvent | KeyboardEvent): void => { + this._roomView.current?.handleScrollKey(ev); }; - public render(): JSX.Element { + public render(): React.ReactNode { let pageElement; switch (this.props.page_type) { diff --git a/src/components/structures/MainSplit.tsx b/src/components/structures/MainSplit.tsx index 1254fbc6f33..e9454963118 100644 --- a/src/components/structures/MainSplit.tsx +++ b/src/components/structures/MainSplit.tsx @@ -47,7 +47,7 @@ export default class MainSplit extends React.Component { }; private loadSidePanelSize(): { height: string | number; width: number } { - let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10); + let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size")!, 10); if (isNaN(rhsSize)) { rhsSize = 350; @@ -59,7 +59,7 @@ export default class MainSplit extends React.Component { }; } - public render(): JSX.Element { + public render(): React.ReactNode { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a4a41a79b7f..d4eb7e28a9b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -420,7 +420,7 @@ export default class MatrixChat extends React.PureComponent { window.addEventListener("resize", this.onWindowResized); } - public componentDidUpdate(prevProps, prevState): void { + public componentDidUpdate(prevProps: IProps, prevState: IState): void { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs); @@ -545,12 +545,11 @@ export default class MatrixChat extends React.PureComponent { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); } - const newState = { - currentUserId: null, + this.setState({ + currentUserId: undefined, justRegistered: false, - }; - Object.assign(newState, state); - this.setState(newState); + ...state, + } as IState); } private onAction = (payload: ActionPayload): void => { @@ -2046,7 +2045,7 @@ export default class MatrixChat extends React.PureComponent { return fragmentAfterLogin; } - public render(): JSX.Element { + public render(): React.ReactNode { const fragmentAfterLogin = this.getFragmentAfterLogin(); let view = null; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 038eab36f36..fb3b713e007 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -1,5 +1,5 @@ /* -Copyright 2016 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from "react"; +import React, { createRef, ReactNode, TransitionEvent } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; @@ -34,7 +34,7 @@ import SettingsStore from "../../settings/SettingsStore"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; -import EventTile, { UnwrappedEventTile, GetRelationsForEvent, IReadReceiptProps } from "../views/rooms/EventTile"; +import EventTile, { GetRelationsForEvent, IReadReceiptProps, UnwrappedEventTile } from "../views/rooms/EventTile"; import { hasText } from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; @@ -76,7 +76,6 @@ export function shouldFormContinuation( prevEvent: MatrixEvent | null, mxEvent: MatrixEvent, showHiddenEvents: boolean, - threadsEnabled: boolean, timelineRenderingType?: TimelineRenderingType, ): boolean { if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false; @@ -106,7 +105,6 @@ export function shouldFormContinuation( // Thread summaries in the main timeline should break up a continuation on both sides if ( - threadsEnabled && (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) && timelineRenderingType !== TimelineRenderingType.Thread ) { @@ -269,7 +267,6 @@ export default class MessagePanel extends React.Component { private readReceiptsByUserId: Record = {}; private readonly _showHiddenEvents: boolean; - private readonly threadsEnabled: boolean; private isMounted = false; private readMarkerNode = createRef(); @@ -282,7 +279,7 @@ export default class MessagePanel extends React.Component { // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. public grouperKeyMap = new WeakMap(); - public constructor(props, context) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { @@ -297,7 +294,6 @@ export default class MessagePanel extends React.Component { // and we check this in a hot code path. This is also cached in our // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.threadsEnabled = SettingsStore.getValue("feature_threadenabled"); this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( "showTypingNotifications", @@ -318,7 +314,7 @@ export default class MessagePanel extends React.Component { SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } - public componentDidUpdate(prevProps, prevState): void { + public componentDidUpdate(prevProps: IProps, prevState: IState): void { if (prevProps.layout !== this.props.layout) { this.calculateRoomMembersCount(); } @@ -420,17 +416,13 @@ export default class MessagePanel extends React.Component { /* jump to the top of the content. */ public scrollToTop(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToTop(); - } + this.scrollPanel.current?.scrollToTop(); } /* jump to the bottom of the content. */ public scrollToBottom(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToBottom(); - } + this.scrollPanel.current?.scrollToBottom(); } /** @@ -438,10 +430,8 @@ export default class MessagePanel extends React.Component { * * @param {KeyboardEvent} ev: the keyboard event to handle */ - public handleScrollKey(ev: KeyboardEvent): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.handleScrollKey(ev); - } + public handleScrollKey(ev: React.KeyboardEvent | KeyboardEvent): void { + this.scrollPanel.current?.handleScrollKey(ev); } /* jump to the given event id. @@ -480,7 +470,7 @@ export default class MessagePanel extends React.Component { // TODO: Implement granular (per-room) hide options public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean { - if (this.props.hideThreadedMessages && this.threadsEnabled && this.props.room) { + if (this.props.hideThreadedMessages && this.props.room) { const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); if (!shouldLiveInRoom) { return false; @@ -736,25 +726,13 @@ export default class MessagePanel extends React.Component { willWantDateSeparator || mxEv.getSender() !== nextEv.getSender() || getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage || - !shouldFormContinuation( - mxEv, - nextEv, - this.showHiddenEvents, - this.threadsEnabled, - this.context.timelineRenderingType, - ); + !shouldFormContinuation(mxEv, nextEv, this.showHiddenEvents, this.context.timelineRenderingType); } // is this a continuation of the previous message? const continuation = !wantsDateSeparator && - shouldFormContinuation( - prevEvent, - mxEv, - this.showHiddenEvents, - this.threadsEnabled, - this.context.timelineRenderingType, - ); + shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents, this.context.timelineRenderingType); const eventId = mxEv.getId(); const highlight = eventId === this.props.highlightedEventId; @@ -762,7 +740,7 @@ export default class MessagePanel extends React.Component { const readReceipts = this.readReceiptsByEvent[eventId]; let isLastSuccessful = false; - const isSentState = (s): boolean => !s || s === "sent"; + const isSentState = (s: EventStatus): boolean => !s || s === EventStatus.SENT; const isSent = isSentState(mxEv.getAssociatedStatus()); const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { @@ -882,8 +860,14 @@ export default class MessagePanel extends React.Component { // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. private getReadReceiptsByShownEvent(): Record { - const receiptsByEvent = {}; - const receiptsByUserId = {}; + const receiptsByEvent: Record = {}; + const receiptsByUserId: Record< + string, + { + lastShownEventId: string; + receipt: IReadReceiptProps; + } + > = {}; let lastShownEventId; for (const event of this.props.events) { @@ -995,7 +979,7 @@ export default class MessagePanel extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { let topSpinner; let bottomSpinner; if (this.props.backPaginating) { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 813522ffcb2..f5f5b29d632 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -27,8 +27,8 @@ interface IState { } export default class NonUrgentToastContainer extends React.PureComponent { - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { toasts: NonUrgentToastStore.instance.components, @@ -45,7 +45,7 @@ export default class NonUrgentToastContainer extends React.PureComponent { return (
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 659d804cf14..742bc6d52a0 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -45,7 +45,7 @@ export default class NotificationPanel extends React.PureComponent(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -57,7 +57,7 @@ export default class NotificationPanel extends React.PureComponent

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

diff --git a/src/components/structures/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx index 40c1caee6f9..2456e633030 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -245,7 +245,7 @@ export default class PictureInPictureDragger extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { const style = { transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`, }; diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 0697f4a6da6..d2cde6a76ea 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { MutableRefObject, useContext, useRef } from "react"; +import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -288,7 +288,7 @@ class PipContainerInner extends React.Component { ); } - public render(): JSX.Element { + public render(): ReactNode { const pipMode = true; let pipContent: Array = []; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index d0f1c7b3135..cda85e2a72d 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -65,7 +65,7 @@ export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; public context!: React.ContextType; - public constructor(props, context) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { @@ -151,7 +151,7 @@ export default class RightPanel extends React.Component { this.setState({ searchQuery }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let card =
; const roomId = this.props.room?.roomId; const phase = this.props.overwriteCard?.phase ?? this.state.phase; diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index c55cd2ee897..01a0c0d1ea4 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2023 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. @@ -36,7 +36,6 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import RoomContext from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { UserNameColorMode } from "../../settings/enums/UserNameColorMode"; -import SettingsStore from "../../settings/SettingsStore"; const DEBUG = false; let debuglog = function (msg: string): void {}; @@ -117,23 +116,19 @@ export const RoomSearchView = forwardRef( return b.length - a.length; }); - if (SettingsStore.getValue("feature_threadenabled")) { - // Process all thread roots returned in this batch of search results - // XXX: This won't work for results coming from Seshat which won't include the bundled relationship - for (const result of results.results) { - for (const event of result.context.getTimeline()) { - const bundledRelationship = - event.getServerAggregatedRelation( - THREAD_RELATION_TYPE.name, - ); - if (!bundledRelationship || event.getThread()) continue; - const room = client.getRoom(event.getRoomId()); - const thread = room.findThreadForEvent(event); - if (thread) { - event.setThread(thread); - } else { - room.createThread(event.getId(), event, [], true); - } + for (const result of results.results) { + for (const event of result.context.getTimeline()) { + const bundledRelationship = + event.getServerAggregatedRelation( + THREAD_RELATION_TYPE.name, + ); + if (!bundledRelationship || event.getThread()) continue; + const room = client.getRoom(event.getRoomId()); + const thread = room?.findThreadForEvent(event); + if (thread) { + event.setThread(thread); + } else { + room?.createThread(event.getId()!, event, [], true); } } } @@ -231,7 +226,7 @@ export const RoomSearchView = forwardRef( scrollPanel?.checkScroll(); }; - let lastRoomId: string; + let lastRoomId: string | undefined; let mergedTimeline: MatrixEvent[] = []; let ourEventsIndexes: number[] = []; diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index f370091a8af..ff9d4472d4a 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SyncState, ISyncStateData } from "matrix-js-sdk/src/sync"; import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixError } from "matrix-js-sdk/src/matrix"; import { _t, _td } from "../../languageHandler"; import Resend from "../../Resend"; @@ -192,10 +193,10 @@ export default class RoomStatusBar extends React.PureComponent { private getUnsentMessageContent(): JSX.Element { const unsentMessages = this.state.unsentMessages; - let title; + let title: ReactNode; - let consentError = null; - let resourceLimitError = null; + let consentError: MatrixError | null = null; + let resourceLimitError: MatrixError | null = null; for (const m of unsentMessages) { if (m.error && m.error.errcode === "M_CONSENT_NOT_GIVEN") { consentError = m.error; @@ -212,7 +213,7 @@ export default class RoomStatusBar extends React.PureComponent { {}, { consentLink: (sub) => ( - + {sub} ), @@ -271,7 +272,7 @@ export default class RoomStatusBar extends React.PureComponent { ); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.shouldShowConnectionError()) { return (
diff --git a/src/components/structures/RoomStatusBarUnsentMessages.tsx b/src/components/structures/RoomStatusBarUnsentMessages.tsx index 3ce300f0eb4..38dbae281e7 100644 --- a/src/components/structures/RoomStatusBarUnsentMessages.tsx +++ b/src/components/structures/RoomStatusBarUnsentMessages.tsx @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement } from "react"; +import React, { ReactElement, ReactNode } from "react"; import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../views/rooms/NotificationBadge"; interface RoomStatusBarUnsentMessagesProps { - title: string; + title: ReactNode; description?: string; notificationState: StaticNotificationState; buttons: ReactElement; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 80f65976fdb..a281dd70057 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd -Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2023 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. @@ -33,6 +33,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials"; import { ISearchResults } from "matrix-js-sdk/src/@types/search"; +import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set"; import shouldHideEvent from "../../shouldHideEvent"; import { _t } from "../../languageHandler"; @@ -49,7 +50,7 @@ import RoomScrollStateStore, { ScrollState } from "../../stores/RoomScrollStateS import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; @@ -229,6 +230,7 @@ export interface IRoomState { narrow: boolean; // List of undecryptable events currently visible on-screen visibleDecryptionFailures?: MatrixEvent[]; + msc3946ProcessDynamicPredecessor: boolean; } interface LocalRoomViewProps { @@ -426,6 +428,7 @@ export class RoomView extends React.Component { liveTimeline: undefined, narrow: false, visibleDecryptionFailures: [], + msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"), }; this.dispatcherRef = dis.register(this.onAction); @@ -499,6 +502,9 @@ export class RoomView extends React.Component { ), SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => + this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), + ), ]; } @@ -891,7 +897,7 @@ export class RoomView extends React.Component { this.recalculateUserNameColorMode(); } - public shouldComponentUpdate(nextProps, nextState): boolean { + public shouldComponentUpdate(nextProps: IRoomProps, nextState: IRoomState): boolean { const hasPropsDiff = objectHasDiff(this.props, nextProps); const { upgradeRecommendation, ...state } = this.state; @@ -1000,7 +1006,7 @@ export class RoomView extends React.Component { }); }; - private onPageUnload = (event): string => { + private onPageUnload = (event: BeforeUnloadEvent): string => { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return (event.returnValue = _t("You seem to be uploading files, are you sure you want to quit?")); } else if (this.getCallForRoom() && this.state.callState !== "ended") { @@ -1008,7 +1014,7 @@ export class RoomView extends React.Component { } }; - private onReactKeyDown = (ev): void => { + private onReactKeyDown = (ev: React.KeyboardEvent): void => { let handled = false; const action = getKeyBindingsManager().getRoomAction(ev); @@ -1172,7 +1178,13 @@ export class RoomView extends React.Component { createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } - private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data): void => { + private onRoomTimeline = ( + ev: MatrixEvent, + room: Room | null, + toStartOfTimeline: boolean, + removed: boolean, + data?: IRoomTimelineData, + ): void => { if (this.unmounted) return; // ignore events for other rooms or the notification timeline set @@ -1192,7 +1204,7 @@ export class RoomView extends React.Component { // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. - if (toStartOfTimeline || !data || !data.liveEvent) return; + if (toStartOfTimeline || !data?.liveEvent) return; // no point handling anything while we're waiting for the join to finish: // we'll only be showing a spinner. @@ -1234,7 +1246,7 @@ export class RoomView extends React.Component { CHAT_EFFECTS.forEach((effect) => { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { // For initial threads launch, chat effects are disabled see #19731 - if (!SettingsStore.getValue("feature_threadenabled") || !ev.isRelation(THREAD_RELATION_TYPE.name)) { + if (!ev.isRelation(THREAD_RELATION_TYPE.name)) { dis.dispatch({ action: `effects.${effect.command}` }); } } @@ -1786,7 +1798,7 @@ export class RoomView extends React.Component { }; // update the read marker to match the read-receipt - private forgetReadMarker = (ev): void => { + private forgetReadMarker = (ev: ButtonEvent): void => { ev.stopPropagation(); this.messagePanel.forgetReadMarker(); }; @@ -1878,7 +1890,7 @@ export class RoomView extends React.Component { * * We pass it down to the scroll panel. */ - public handleScrollKey = (ev): void => { + public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { let panel: ScrollPanel | TimelinePanel; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; @@ -1901,15 +1913,13 @@ export class RoomView extends React.Component { // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. - private gatherTimelinePanelRef = (r): void => { + private gatherTimelinePanelRef = (r?: TimelinePanel): void => { this.messagePanel = r; }; private getOldRoom(): Room | null { - const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, ""); - if (!createEvent || !createEvent.getContent()["predecessor"]) return null; - - return this.context.client.getRoom(createEvent.getContent()["predecessor"]["room_id"]); + const { roomId } = this.state.room?.findPredecessor(this.state.msc3946ProcessDynamicPredecessor) || {}; + return this.context.client?.getRoom(roomId) || null; } public getHiddenHighlightCount(): number { @@ -1979,7 +1989,7 @@ export class RoomView extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { return this.renderLocalRoomCreateLoader(); diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index f51cba66a32..7779f97a547 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react"; +import React, { createRef, CSSProperties, ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -195,8 +195,8 @@ export default class ScrollPanel extends React.Component { private heightUpdateInProgress: boolean; private divScroll: HTMLDivElement; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); @@ -440,9 +440,9 @@ export default class ScrollPanel extends React.Component { // pagination. // // If backwards is true, we unpaginate (remove) tiles from the back (top). - let tile; + let tile: HTMLElement; for (let i = 0; i < tiles.length; i++) { - tile = tiles[backwards ? i : tiles.length - 1 - i]; + tile = tiles[backwards ? i : tiles.length - 1 - i] as HTMLElement; // Subtract height of tile as if it were unpaginated excessHeight -= tile.clientHeight; //If removing the tile would lead to future pagination, break before setting scroll token @@ -587,7 +587,7 @@ export default class ScrollPanel extends React.Component { * Scroll up/down in response to a scroll key * @param {object} ev the keyboard event */ - public handleScrollKey = (ev: KeyboardEvent): void => { + public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case KeyBindingAction.ScrollUp: diff --git a/src/components/structures/SearchBox.tsx b/src/components/structures/SearchBox.tsx index a0777f2d524..16c2df31738 100644 --- a/src/components/structures/SearchBox.tsx +++ b/src/components/structures/SearchBox.tsx @@ -24,7 +24,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; interface IProps extends HTMLProps { - onSearch?: (query: string) => void; + onSearch: (query: string) => void; onCleared?: (source?: string) => void; onKeyDown?: (ev: React.KeyboardEvent) => void; onFocus?: (ev: React.FocusEvent) => void; @@ -62,7 +62,7 @@ export default class SearchBox extends React.Component { private onSearch = throttle( (): void => { - this.props.onSearch(this.search.current.value); + this.props.onSearch(this.search.current?.value); }, 200, { trailing: true, leading: true }, @@ -101,7 +101,7 @@ export default class SearchBox extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { onSearch, diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 58565bd4316..238eaa18611 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -280,7 +280,7 @@ const Tile: React.FC = ({ ); if (showChildren) { - const onChildrenKeyDown = (e): void => { + const onChildrenKeyDown = (e: React.KeyboardEvent): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.ArrowLeft: @@ -654,7 +654,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set }; } - let buttonText = _t("Saving..."); + let buttonText = _t("Saving…"); if (!saving) { buttonText = selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"); } @@ -694,7 +694,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set kind="danger_outline" disabled={disabled} > - {removing ? _t("Removing...") : _t("Remove")} + {removing ? _t("Removing…") : _t("Remove")}