diff --git a/.eslintrc.js b/.eslintrc.js index 30e01f86b5..a65f20893b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -100,8 +100,12 @@ module.exports = { files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"], extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"], rules: { - // temporary disabled - "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-function-return-type": [ + "error", + { + allowExpressions: true, + }, + ], // Things we do that break the ideal style "prefer-promise-reject-errors": "off", diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 641a4a0a41..d114217f5c 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -84,14 +84,16 @@ jobs: actions: read issues: read pull-requests: read - environment: - Cypress - #strategy: - # fail-fast: false - # matrix: - # # Run 4 instances in Parallel - # runner: [1, 2, 3, 4] + environment: Cypress + strategy: + fail-fast: false + matrix: + # Run 4 instances in Parallel + runner: [1, 2, 3, 4] steps: + - uses: browser-actions/setup-chrome@latest + - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV + - uses: tecolicom/actions-use-apt-tools@v1 with: # Our test suite includes some screenshot tests with unusual diacritics, which are @@ -121,14 +123,12 @@ jobs: with: # The built-in Electron runner seems to grind to a halt trying # to run the tests, so use chrome. - browser: chrome + browser: "${{ env.BROWSER_PATH }}" start: npx serve -p 8080 webapp wait-on: "http://localhost:8080" - record: - true - #parallel: true - #command-prefix: 'yarn percy exec --parallel --' - command-prefix: "yarn percy exec --" + record: true + parallel: true + command-prefix: "yarn percy exec --parallel --" config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" } }' ci-build-id: ${{ needs.prepare.outputs.uuid }} env: @@ -151,6 +151,8 @@ jobs: COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }} COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }} COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }} + CYPRESS_PULL_REQUEST_ID: ${{ needs.prepare.outputs.pr_id }} + CYPRESS_PULL_REQUEST_URL: https://github.com/${{ github.repository }}/pull/${{ needs.prepare.outputs.pr_id }} # pass the Percy token as an environment variable PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} @@ -159,9 +161,8 @@ jobs: # tell Percy more details about the context of this run PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }} PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }} - PERCY_PULL_REQUEST: - ${{ needs.prepare.outputs.pr_id }} - #PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }} + PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }} + PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }} PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} - name: Upload Artifact diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index 8cb1daf662..d29d01b507 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -12,7 +12,7 @@ jobs: - name: "Get modified files" id: changed_files if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' - uses: tj-actions/changed-files@v34 + uses: tj-actions/changed-files@v35 with: files: | src/i18n/strings/* diff --git a/cypress.config.ts b/cypress.config.ts index 253857e375..f9bc521bdd 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,12 +27,11 @@ export default defineConfig({ return require("./cypress/plugins/index.ts").default(on, config); }, baseUrl: "http://localhost:8080", - experimentalSessionAndOrigin: true, specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}", }, env: { - // Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image. - SLIDING_SYNC_PROXY_TAG: "v0.6.0", + // Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. + SLIDING_SYNC_PROXY_TAG: "v0.99.0-rc1", HOMESERVER: "synapse", }, retries: { diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index 6d2879ff10..289d865ba6 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -120,13 +120,27 @@ describe("Composer", () => { // Type another cy.get("div[contenteditable=true]").type("my message 1"); - // Press enter. Would be nice to just use {enter} but we can't because Cypress - // does not trigger an insertParagraph when you do that. - cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" }); + // Send message + cy.get("div[contenteditable=true]").type("{enter}"); // It was sent cy.contains(".mx_EventTile_body", "my message 1"); }); + it("sends only one message when you press Enter multiple times", () => { + // Type a message + cy.get("div[contenteditable=true]").type("my message 0"); + // It has not been sent yet + cy.contains(".mx_EventTile_body", "my message 0").should("not.exist"); + + // Click send + cy.get("div[contenteditable=true]").type("{enter}"); + cy.get("div[contenteditable=true]").type("{enter}"); + cy.get("div[contenteditable=true]").type("{enter}"); + // It has been sent + cy.contains(".mx_EventTile_body", "my message 0"); + cy.get(".mx_EventTile_body").should("have.length", 1); + }); + it("can write formatted text", () => { cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message"); cy.get('div[aria-label="Send message"]').click(); @@ -141,7 +155,7 @@ describe("Composer", () => { it("only sends when you press Ctrl+Enter", () => { // Type a message and press Enter cy.get("div[contenteditable=true]").type("my message 3"); - cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" }); + cy.get("div[contenteditable=true]").type("{enter}"); // It has not been sent yet cy.contains(".mx_EventTile_body", "my message 3").should("not.exist"); diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 716eff8ddb..306e94cb97 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -20,6 +20,7 @@ import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; import type { CypressBot } from "../../support/bot"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import Chainable = Cypress.Chainable; +import { UserCredentials } from "../../support/login"; type EmojiMapping = [emoji: string, name: string]; interface CryptoTestContext extends Mocha.Context { @@ -154,11 +155,15 @@ const verify = function (this: CryptoTestContext) { }; describe("Cryptography", function () { + let aliceCredentials: UserCredentials; + beforeEach(function () { cy.startHomeserver("default") .as("homeserver") .then((homeserver: HomeserverInstance) => { - cy.initTestUser(homeserver, "Alice", undefined, "alice_"); + cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => { + aliceCredentials = credentials; + }); cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob"); }); }); @@ -183,7 +188,7 @@ describe("Cryptography", function () { }); it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) { - cy.bootstrapCrossSigning(); + cy.bootstrapCrossSigning(aliceCredentials); startDMWithBob.call(this); // send first message cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}"); @@ -194,7 +199,7 @@ describe("Cryptography", function () { }); it("should allow verification when there is no existing DM", function (this: CryptoTestContext) { - cy.bootstrapCrossSigning(); + cy.bootstrapCrossSigning(aliceCredentials); autoJoin(this.bob); // we need to have a room with the other user present, so we can open the verification panel @@ -212,7 +217,7 @@ describe("Cryptography", function () { }); it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) { - cy.bootstrapCrossSigning(); + cy.bootstrapCrossSigning(aliceCredentials); // bob has a second, not cross-signed, device cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice"); diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index 6cc0a69e3c..5f0a9056ad 100644 --- a/cypress/e2e/crypto/decryption-failure.spec.ts +++ b/cypress/e2e/crypto/decryption-failure.spec.ts @@ -105,15 +105,9 @@ describe("Decryption Failure Bar", () => { "and there are other verified devices or backups", () => { let otherDevice: MatrixClient | undefined; - cy.loginBot(homeserver, testUser.username, testUser.password, {}) + cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true }) .then(async (cli) => { otherDevice = cli; - await otherDevice.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (makeRequest) => { - await makeRequest({}); - }, - setupNewCrossSigning: true, - }); }) .then(() => { cy.botSendMessage(bot, roomId, "test"); @@ -169,15 +163,11 @@ describe("Decryption Failure Bar", () => { "should prompt the user to reset keys, if this device isn't verified " + "and there are no other verified devices or backups", () => { - cy.loginBot(homeserver, testUser.username, testUser.password, {}).then(async (cli) => { - await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (makeRequest) => { - await makeRequest({}); - }, - setupNewCrossSigning: true, - }); - await cli.logout(true); - }); + cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true }).then( + async (cli) => { + await cli.logout(true); + }, + ); cy.botSendMessage(bot, roomId, "test"); cy.wait(5000); diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index f8d7dd1e3f..e056329a7b 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -16,9 +16,9 @@ limitations under the License. /// -import type { MsgType } from "matrix-js-sdk/src/@types/event"; +import type { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; -import type { EventType } from "matrix-js-sdk/src/@types/event"; +import type { IContent } from "matrix-js-sdk/src/models/event"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import Chainable = Cypress.Chainable; @@ -29,6 +29,16 @@ const sendEvent = (roomId: string): Chainable => { }); }; +/** generate a message event which will take up some room on the page. */ +function mkPadding(n: number): IContent { + return { + msgtype: "m.text" as MsgType, + body: `padding ${n}`, + format: "org.matrix.custom.html", + formatted_body: `

Test event ${n}

\n`.repeat(10), + }; +} + describe("Editing", () => { let homeserver: HomeserverInstance; @@ -37,7 +47,6 @@ describe("Editing", () => { homeserver = data; cy.initTestUser(homeserver, "Edith").then(() => { cy.injectAxe(); - return cy.createRoom({ name: "Test room" }).as("roomId"); }); }); }); @@ -47,6 +56,8 @@ describe("Editing", () => { }); it("should close the composer when clicking save after making a change and undoing it", () => { + cy.createRoom({ name: "Test room" }).as("roomId"); + cy.get("@roomId").then((roomId) => { sendEvent(roomId); cy.visit("/#/room/" + roomId); @@ -64,4 +75,77 @@ describe("Editing", () => { // Assert that the edit composer has gone away cy.get(".mx_EditMessageComposer").should("not.exist"); }); + + it("should correctly display events which are edited, where we lack the edit event", () => { + // This tests the behaviour when a message has been edited some time after it has been sent, and we + // jump back in room history to view the event, but do not have the actual edit event. + // + // In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on + // the bundled edit event (post-MSC3925). + // + // To test it, we need to have a room with lots of events in, so we can jump around the timeline without + // paginating in the event itself. Hence, we create a bot user which creates the room and populates it before + // we join. + + let testRoomId: string; + let originalEventId: string; + let editEventId: string; + + // create a second user + const bobChainable = cy.getBot(homeserver, { displayName: "Bob", userIdPrefix: "bob_" }); + + cy.all([cy.window({ log: false }), bobChainable]).then(async ([win, bob]) => { + // "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on + // the js-sdk rather than Cypress commands, so uses regular async/await. + + const room = await bob.createRoom({ name: "TestRoom", visibility: win.matrixcs.Visibility.Public }); + testRoomId = room.room_id; + cy.log(`Bot user created room ${room.room_id}`); + + originalEventId = (await bob.sendMessage(room.room_id, { body: "original", msgtype: "m.text" })).event_id; + cy.log(`Bot user sent original event ${originalEventId}`); + + // send a load of padding events. We make them large, so that they fill the whole screen + // and the client doesn't end up paginating into the event we want. + let i = 0; + while (i < 10) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + + // ... then the edit ... + editEventId = ( + await bob.sendMessage(room.room_id, { + "m.new_content": { body: "Edited body", msgtype: "m.text" }, + "m.relates_to": { + rel_type: "m.replace", + event_id: originalEventId, + }, + "body": "* edited", + "msgtype": "m.text", + }) + ).event_id; + cy.log(`Bot user sent edit event ${editEventId}`); + + // ... then a load more padding ... + while (i < 20) { + await bob.sendMessage(room.room_id, mkPadding(i++)); + } + }); + + cy.getClient().then((cli) => { + // now have the cypress user join the room, jump to the original event, and wait for the event to be + // visible + cy.joinRoom(testRoomId); + cy.viewRoomByName("TestRoom"); + cy.visit(`#/room/${testRoomId}/${originalEventId}`); + cy.get(`[data-event-id="${originalEventId}"]`).should((messageTile) => { + // at this point, the edit event should still be unknown + expect(cli.getRoom(testRoomId).getTimelineForEvent(editEventId)).to.be.null; + + // nevertheless, the event should be updated + expect(messageTile.find(".mx_EventTile_body").text()).to.eq("Edited body"); + expect(messageTile.find(".mx_EventTile_edited")).to.exist; + }); + }); + }); }); diff --git a/cypress/e2e/integration-manager/kick.spec.ts b/cypress/e2e/integration-manager/kick.spec.ts index 2cd66fa51b..79e1db1ef9 100644 --- a/cypress/e2e/integration-manager/kick.spec.ts +++ b/cypress/e2e/integration-manager/kick.spec.ts @@ -82,9 +82,27 @@ function sendActionFromIntegrationManager(integrationManagerUrl: string, targetR }); } +function clickUntilGone(selector: string, attempt = 0) { + if (attempt === 11) { + throw new Error("clickUntilGone attempt count exceeded"); + } + + cy.get(selector) + .last() + .click() + .then(($button) => { + const exists = Cypress.$(selector).length > 0; + if (exists) { + clickUntilGone(selector, ++attempt); + } + }); +} + function expectKickedMessage(shouldExist: boolean) { - // Expand any event summaries - cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true }); + // Expand any event summaries, we can't use a click multiple here because clicking one might de-render others + // This is quite horrible but seems the most stable way of clicking 0-N buttons, + // one at a time with a full re-evaluation after each click + clickUntilGone(".mx_GenericEventListSummary_toggle[aria-expanded=false]"); // Check for the event message (or lack thereof) cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should( diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index f73524965c..51d169d61b 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -16,8 +16,6 @@ limitations under the License. /// -import { PollResponseEvent } from "matrix-events-sdk"; - import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { MatrixClient } from "../../global"; import Chainable = Cypress.Chainable; @@ -70,8 +68,16 @@ describe("Polls", () => { cy.get('input[type="radio"]') .invoke("attr", "value") .then((optionId) => { - const pollVote = PollResponseEvent.from([optionId], pollId).serialize(); - bot.sendEvent(roomId, pollVote.type, pollVote.content); + // We can't use the js-sdk types for this stuff directly, so manually construct the event. + bot.sendEvent(roomId, "org.matrix.msc3381.poll.response", { + "m.relates_to": { + rel_type: "m.reference", + event_id: pollId, + }, + "org.matrix.msc3381.poll.response": { + answers: [optionId], + }, + }); }); }); }; diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index 1b2642eeb4..c6d2c298fe 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -21,8 +21,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Interception } from "cypress/types/net-stubbing"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; -import { Layout } from "../../../src/settings/enums/Layout"; import { ProxyInstance } from "../../plugins/sliding-sync"; describe("Sliding Sync", () => { @@ -102,21 +100,6 @@ describe("Sliding Sync", () => { }); }; - // sanity check everything works - it("should correctly render expected messages", () => { - cy.get("@roomId").then((roomId) => cy.visit("/#/room/" + roomId)); - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - - // Wait until configuration is finished - cy.contains( - ".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary", - "created and configured the room.", - ); - - // Click "expand" link button - cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); - }); - it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => { // create rooms and check room names are correct cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple")); diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts new file mode 100644 index 0000000000..d9aeb46062 --- /dev/null +++ b/cypress/e2e/widgets/events.spec.ts @@ -0,0 +1,221 @@ +/* +Copyright 2022 Mikhail Aheichyk +Copyright 2022 Nordeck IT + Consulting GmbH. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; + +import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { UserCredentials } from "../../support/login"; + +const DEMO_WIDGET_ID = "demo-widget-id"; +const DEMO_WIDGET_NAME = "Demo Widget"; +const DEMO_WIDGET_TYPE = "demo"; +const ROOM_NAME = "Demo"; + +const DEMO_WIDGET_HTML = ` + + + Demo Widget + + + + + + +`; + +function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: Room) => boolean): Promise { + const matrixClient = win.mxMatrixClientPeg.get(); + + return new Promise((resolve, reject) => { + const room = matrixClient.getRoom(roomId); + + if (predicate(room)) { + resolve(); + return; + } + + function onEvent(ev: MatrixEvent) { + if (ev.getRoomId() !== roomId) return; + + if (predicate(room)) { + matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent); + resolve(); + } + } + + matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent); + }); +} + +describe("Widget Events", () => { + let homeserver: HomeserverInstance; + let user: UserCredentials; + let bot: MatrixClient; + let demoWidgetUrl: string; + + beforeEach(() => { + cy.startHomeserver("default").then((data) => { + homeserver = data; + + cy.initTestUser(homeserver, "Mike").then((_user) => { + user = _user; + }); + cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => { + bot = _bot; + }); + }); + cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => { + demoWidgetUrl = url; + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + cy.stopWebServers(); + }); + + it("should be updated if user is re-invited into the room with updated state event", () => { + cy.createRoom({ + name: ROOM_NAME, + invite: [bot.getUserId()], + }).then((roomId) => { + // setup widget via state event + cy.getClient() + .then(async (matrixClient) => { + const content: IWidget = { + id: DEMO_WIDGET_ID, + creatorUserId: "somebody", + type: DEMO_WIDGET_TYPE, + name: DEMO_WIDGET_NAME, + url: demoWidgetUrl, + }; + await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID); + }) + .as("widgetEventSent"); + + // set initial layout + cy.getClient() + .then(async (matrixClient) => { + const content = { + widgets: { + [DEMO_WIDGET_ID]: { + container: "top", + index: 1, + width: 100, + height: 0, + }, + }, + }; + await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, ""); + }) + .as("layoutEventSent"); + + // open the room + cy.viewRoomByName(ROOM_NAME); + + // approve capabilities + cy.contains(".mx_WidgetCapabilitiesPromptDialog button", "Approve").click(); + + cy.all([cy.get("@widgetEventSent"), cy.get("@layoutEventSent")]).then(async () => { + // bot creates a new room with 'm.room.topic' + const { room_id: roomNew } = await bot.createRoom({ + name: "New room", + initial_state: [ + { + type: "m.room.topic", + state_key: "", + content: { + topic: "topic initial", + }, + }, + ], + }); + + await bot.invite(roomNew, user.userId); + + // widget should receive 'm.room.topic' event after invite + cy.window().then(async (win) => { + await waitForRoom(win, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "net.widget_echo" && + e.getContent().type === "m.room.topic" && + e.getContent().content.topic === "topic initial", + ); + }); + }); + + // update the topic + await bot.sendStateEvent( + roomNew, + "m.room.topic", + { + topic: "topic updated", + }, + "", + ); + + await bot.invite(roomNew, user.userId, "something changed in the room"); + + // widget should receive updated 'm.room.topic' event after re-invite + cy.window().then(async (win) => { + await waitForRoom(win, roomId, (room) => { + const events = room.getLiveTimeline().getEvents(); + return events.some( + (e) => + e.getType() === "net.widget_echo" && + e.getContent().type === "m.room.topic" && + e.getContent().content.topic === "topic updated", + ); + }); + }); + }); + }); + }); +}); diff --git a/cypress/plugins/sliding-sync/index.ts b/cypress/plugins/sliding-sync/index.ts index 8204fb578d..ab39c7a42b 100644 --- a/cypress/plugins/sliding-sync/index.ts +++ b/cypress/plugins/sliding-sync/index.ts @@ -23,7 +23,7 @@ import { getFreePort } from "../utils/port"; import { HomeserverInstance } from "../utils/homeserver"; // A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync -// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image. +// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. export interface ProxyInstance { containerId: string; @@ -72,7 +72,7 @@ async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Pr const port = await getFreePort(); console.log(new Date(), "starting proxy container...", dockerTag); const containerId = await dockerRun({ - image: "ghcr.io/matrix-org/sliding-sync-proxy:" + dockerTag, + image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag, containerName: "react-sdk-cypress-sliding-sync-proxy", params: [ "--rm", diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index 745ec4002c..9cb5e472de 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -150,7 +150,14 @@ function setupBotClient( if (opts.bootstrapCrossSigning) { await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (func) => { - await func({}); + await func({ + type: "m.login.password", + identifier: { + type: "m.id.user", + user: credentials.userId, + }, + password: credentials.password, + }); }, }); } diff --git a/cypress/support/client.ts b/cypress/support/client.ts index 1195a6f303..c56608fadc 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -22,6 +22,7 @@ import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { Room } from "matrix-js-sdk/src/models/room"; import type { IContent } from "matrix-js-sdk/src/models/event"; import Chainable = Cypress.Chainable; +import { UserCredentials } from "./login"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -119,7 +120,7 @@ declare global { /** * Boostraps cross-signing. */ - bootstrapCrossSigning(): Chainable; + bootstrapCrossSigning(credendtials: UserCredentials): Chainable; /** * Joins the given room by alias or ID * @param roomIdOrAlias the id or alias of the room to join @@ -210,11 +211,18 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => { }); }); -Cypress.Commands.add("bootstrapCrossSigning", () => { +Cypress.Commands.add("bootstrapCrossSigning", (credentials: UserCredentials) => { cy.window({ log: false }).then((win) => { win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (func) => { - await func({}); + await func({ + type: "m.login.password", + identifier: { + type: "m.id.user", + user: credentials.userId, + }, + password: credentials.password, + }); }, }); }); diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index 8510e3640a..3026c94b06 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -76,6 +76,7 @@ export interface Credentials { userId: string; deviceId: string; homeServer: string; + password: string; } function registerUser( @@ -120,6 +121,7 @@ function registerUser( accessToken: response.body.access_token, userId: response.body.user_id, deviceId: response.body.device_id, + password: password, })); } diff --git a/cypress/support/util.ts b/cypress/support/util.ts index b86bbc27d5..6855379bda 100644 --- a/cypress/support/util.ts +++ b/cypress/support/util.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. @@ -16,12 +16,6 @@ limitations under the License. /// -// @see https://github.com/cypress-io/cypress/issues/915#issuecomment-475862672 -// Modified due to changes to `cy.queue` https://github.com/cypress-io/cypress/pull/17448 -// Note: this DOES NOT run Promises in parallel like `Promise.all` due to the nature -// of Cypress promise-like objects and command queue. This only makes it convenient to use the same -// API but runs the commands sequentially. - declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { @@ -31,51 +25,35 @@ declare global { all( commands: T, ): Cypress.Chainable<{ [P in keyof T]: ChainableValue }>; - queue: any; - } - - interface Chainable { - chainerId: string; } } } -const chainStart = Symbol("chainStart"); - /** * @description Returns a single Chainable that resolves when all of the Chainables pass. * @param {Cypress.Chainable[]} commands - List of Cypress.Chainable to resolve. * @returns {Cypress.Chainable} Cypress when all Chainables are resolved. */ cy.all = function all(commands): Cypress.Chainable { - const chain = cy.wrap(null, { log: false }); - const stopCommand = Cypress._.find(cy.queue.get(), { - attributes: { chainerId: chain.chainerId }, + const resultArray = []; + + // as each command completes, store the result in the corresponding location of resultArray. + for (let i = 0; i < commands.length; i++) { + commands[i].then((val) => { + resultArray[i] = val; + }); + } + + // add an entry to the log which, when clicked, will write the results to the console. + Cypress.log({ + name: "all", + consoleProps: () => ({ Results: resultArray }), }); - const startCommand = Cypress._.find(cy.queue.get(), { - attributes: { chainerId: commands[0].chainerId }, - }); - const p = chain.then(() => { - return cy.wrap( - // @see https://lodash.com/docs/4.17.15#lodash - Cypress._(commands) - .map((cmd) => { - return cmd[chainStart] - ? cmd[chainStart].attributes - : Cypress._.find(cy.queue.get(), { - attributes: { chainerId: cmd.chainerId }, - }).attributes; - }) - .concat(stopCommand.attributes) - .slice(1) - .map((cmd) => { - return cmd.prev.get("subject"); - }) - .value(), - ); - }); - p[chainStart] = startCommand; - return p; + + // return a chainable which wraps the resultArray. Although this doesn't have a direct dependency on the input + // commands, cypress won't process it until the commands that precede it on the command queue (which must include + // the input commands) have passed. + return cy.wrap(resultArray, { log: false }); }; // Needed to make this file a module diff --git a/package.json b/package.json index 09682dc292..17d2c761ff 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./lib/index.ts", + "main": "./src/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -56,8 +56,8 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.14.0", + "@matrix-org/analytics-events": "^0.4.0", + "@matrix-org/matrix-wysiwyg": "^0.20.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", @@ -177,7 +177,7 @@ "babel-jest": "^29.0.0", "blob-polyfill": "^7.0.0", "chokidar": "^3.5.1", - "cypress": "^11.0.0", + "cypress": "^12.0.0", "cypress-axe": "^1.0.0", "cypress-multi-reporters": "^1.6.1", "cypress-real-events": "^1.7.1", @@ -259,6 +259,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9cd446ecbc..fe50417c00 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -379,5 +379,6 @@ @import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; +@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss"; @import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/_spacing.pcss b/res/css/_spacing.pcss index 40c470c26b..63197f2321 100644 --- a/res/css/_spacing.pcss +++ b/res/css/_spacing.pcss @@ -16,6 +16,7 @@ limitations under the License. /* 1rem :: 10px */ +$spacing-2: 2px; $spacing-4: 4px; $spacing-8: 8px; $spacing-12: 12px; diff --git a/res/css/views/context_menus/_IconizedContextMenu.pcss b/res/css/views/context_menus/_IconizedContextMenu.pcss index 677d4b2bb0..e6600620ac 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.pcss +++ b/res/css/views/context_menus/_IconizedContextMenu.pcss @@ -84,7 +84,7 @@ limitations under the License. align-items: center; &:hover, - &:focus { + &:focus-visible { background-color: $menu-selected-color; } diff --git a/res/css/views/dialogs/_UserSettingsDialog.pcss b/res/css/views/dialogs/_UserSettingsDialog.pcss index 118c057b83..41d39f8b79 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.pcss +++ b/res/css/views/dialogs/_UserSettingsDialog.pcss @@ -49,6 +49,10 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/security.svg"); } +.mx_UserSettingsDialog_sessionsIcon::before { + mask-image: url("$(res)/img/element-icons/settings/devices.svg"); +} + .mx_UserSettingsDialog_helpIcon::before { mask-image: url("$(res)/img/element-icons/settings/help.svg"); } diff --git a/res/css/views/elements/_ImageView.pcss b/res/css/views/elements/_ImageView.pcss index 969e34924b..f67f8a1262 100644 --- a/res/css/views/elements/_ImageView.pcss +++ b/res/css/views/elements/_ImageView.pcss @@ -67,6 +67,8 @@ $button-gap: 24px; flex-direction: row; align-items: center; color: $lightbox-fg-color; + flex-grow: 1; + flex-basis: 0; } .mx_ImageView_info { @@ -82,6 +84,9 @@ $button-gap: 24px; .mx_ImageView_title { color: $lightbox-fg-color; font-size: $font-12px; + flex-grow: 1; + flex-basis: 0; + text-align: center; } .mx_ImageView_toolbar { @@ -89,6 +94,9 @@ $button-gap: 24px; pointer-events: initial; display: flex; align-items: center; + flex-grow: 1; + flex-basis: 0; + justify-content: flex-end; gap: calc($button-gap - ($button-size - $icon-size)); } diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 1169f51388..5d55e8bf34 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -548,7 +548,19 @@ $left-gutter: 64px; pre, code { font-family: $monospace-font-family !important; - background-color: $codeblock-background-color; + background-color: $system; + } + + code:not(pre *) { + background-color: $inlinecode-background-color; + border: 1px solid $inlinecode-border-color; + border-radius: 4px; + // The horizontal padding is added by gfm.css .markdown-body + padding: $spacing-2 0; + // Avoid inline code blocks to be sticked when on multiple lines + line-height: $font-22px; + // Avoid the border to be glued to the other words + margin-right: $spacing-2; } code { @@ -566,6 +578,8 @@ $left-gutter: 64px; background: transparent; } + border: 1px solid $quinary-content; + code { white-space: pre; /* we want code blocks to be scrollable and not wrap */ @@ -619,6 +633,17 @@ $left-gutter: 64px; ul ol { list-style-type: revert; } + + /* Make list type disc to match rich text editor */ + > ul { + list-style-type: disc; + } + + /* Remove top and bottom margin for better consecutive list display */ + > :is(ol, ul) { + margin-top: 0; + margin-bottom: 0; + } } } @@ -733,6 +758,8 @@ $left-gutter: 64px; .mx_EventTile_collapsedCodeBlock { max-height: 30vh; + padding-top: $spacing-12; + padding-bottom: $spacing-12; } /* Inserted adjacent to
 blocks, (See TextualBody) */
@@ -873,6 +900,7 @@ $left-gutter: 64px;
 
     &::before {
         inset: 0;
+        pointer-events: none; /* ensures the title for the sender name can be correctly displayed */
     }
 
     /* Display notification dot */
@@ -916,8 +944,14 @@ $left-gutter: 64px;
         inset: $padding auto auto $padding;
     }
 
+    .mx_EventTile_details {
+        overflow: hidden;
+    }
+
     .mx_DisambiguatedProfile {
         display: inline-flex;
+        align-items: center;
+        flex: 1;
 
         .mx_DisambiguatedProfile_displayName,
         .mx_DisambiguatedProfile_mxid {
@@ -968,8 +1002,11 @@ $left-gutter: 64px;
 
     .mx_MessageTimestamp {
         font-size: $font-12px;
-        max-width: var(--MessageTimestamp-max-width);
+        width: unset; /* Cancel the default width */
+        overflow: hidden; /* ensure correct overflow behavior */
+        text-overflow: ellipsis;
         position: initial;
+        margin-left: auto; /* to ensure it's end-aligned even if it's the only element of its parent */
     }
 
     &:hover {
@@ -1297,7 +1334,7 @@ $left-gutter: 64px;
 
 .mx_EventTile_details {
     display: flex;
-    width: -webkit-fill-available;
+    width: stretch;
     align-items: center;
     justify-content: space-between;
     gap: $spacing-8;
diff --git a/res/css/views/rooms/_ReplyTile.pcss b/res/css/views/rooms/_ReplyTile.pcss
index 1e70b47956..1a2e1a3814 100644
--- a/res/css/views/rooms/_ReplyTile.pcss
+++ b/res/css/views/rooms/_ReplyTile.pcss
@@ -32,11 +32,12 @@ limitations under the License.
         grid-template:
             "sender" auto
             "message" auto
-            / auto;
+            / 100%;
         text-decoration: none;
         color: $secondary-content;
         transition: color ease 0.15s;
         gap: 2px;
+        max-width: 100%; // avoid overflow with wide content
 
         &:hover {
             color: $primary-content;
diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss
index f1636055b1..c6ab2add22 100644
--- a/res/css/views/rooms/_RoomHeader.pcss
+++ b/res/css/views/rooms/_RoomHeader.pcss
@@ -108,7 +108,7 @@ limitations under the License.
     display: flex;
     user-select: none;
 
-    &:not(.mx_RoomHeader_name--textonly):hover {
+    &:hover {
         background-color: $quinary-content;
     }
 
diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
index d48476cfd7..cc805e1ac1 100644
--- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
+++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
@@ -25,6 +25,7 @@ limitations under the License.
     }
 
     .mx_WysiwygComposer_Editor_content {
+        line-height: $font-22px;
         white-space: pre-wrap;
         word-wrap: break-word;
         outline: none;
@@ -35,6 +36,52 @@ limitations under the License.
         .caretNode {
             user-select: all;
         }
+
+        ul,
+        ol {
+            margin-top: 0;
+            margin-bottom: 0;
+            padding-inline-start: $spacing-28;
+        }
+
+        blockquote {
+            color: #777;
+            border-left: 2px solid $blockquote-bar-color;
+            border-radius: 2px;
+            padding: 0 10px;
+
+            margin-block-start: 0;
+            margin-block-end: 0;
+            margin-inline-start: 0;
+            margin-inline-end: 0;
+        }
+
+        // model output always includes a linebreak but we do not want the user
+        // to see it when writing input in lists
+        :is(ol, ul, pre, blockquote) + br:last-of-type {
+            display: none;
+        }
+
+        > pre {
+            font-size: $font-15px;
+            line-height: $font-24px;
+
+            margin-top: 0;
+            margin-bottom: 0;
+            padding: $spacing-8 $spacing-12;
+
+            background-color: $inlinecode-background-color;
+            border: 1px solid $inlinecode-border-color;
+            border-radius: 2px;
+        }
+
+        code {
+            font-family: $monospace-font-family !important;
+            background-color: $inlinecode-background-color;
+            border: 1px solid $inlinecode-border-color;
+            border-radius: 4px;
+            padding: $spacing-2;
+        }
     }
 
     .mx_WysiwygComposer_Editor_content_placeholder::before {
diff --git a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss
index fa8078279f..8e3dd22c99 100644
--- a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss
+++ b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss
@@ -50,6 +50,12 @@ limitations under the License.
         }
     }
 
+    .mx_FormattingButtons_disabled {
+        .mx_FormattingButtons_Icon {
+            color: $quinary-content;
+        }
+    }
+
     .mx_FormattingButtons_Icon {
         --size: 16px;
         height: var(--size);
diff --git a/res/css/views/voip/_CallDuration.pcss b/res/css/views/voip/_CallDuration.pcss
index c8dc07ef67..49524c88c6 100644
--- a/res/css/views/voip/_CallDuration.pcss
+++ b/res/css/views/voip/_CallDuration.pcss
@@ -17,4 +17,5 @@ limitations under the License.
 .mx_CallDuration {
     color: $secondary-content;
     font-size: $font-12px;
+    white-space: nowrap;
 }
diff --git a/res/css/views/voip/_CallView.pcss b/res/css/views/voip/_CallView.pcss
index 3e214a5b7b..72c5dc1839 100644
--- a/res/css/views/voip/_CallView.pcss
+++ b/res/css/views/voip/_CallView.pcss
@@ -160,7 +160,7 @@ limitations under the License.
                             content: "";
                             display: inline-block;
                             mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
-                            mask-size: $size;
+                            mask-size: 20px;
                             mask-position: center;
                             background-color: $call-primary-content;
                             height: 100%;
@@ -181,7 +181,7 @@ limitations under the License.
                         .mx_CallView_deviceButton {
                             &.mx_CallView_deviceButton_audio::before {
                                 mask-image: url("$(res)/img/element-icons/Mic-off.svg");
-                                mask-size: 14px;
+                                mask-size: 18px;
                             }
 
                             &.mx_CallView_deviceButton_video::before {
diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss
new file mode 100644
index 0000000000..bf2e535096
--- /dev/null
+++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss
@@ -0,0 +1,26 @@
+/*
+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_VoiceBroadcastRecordingConnectionError {
+    align-items: center;
+    color: $alert;
+    display: flex;
+    gap: $spacing-12;
+
+    svg path {
+        fill: $alert;
+    }
+}
diff --git a/res/img/element-icons/room/composer/bulleted_list.svg b/res/img/element-icons/room/composer/bulleted_list.svg
new file mode 100644
index 0000000000..828bb8ab03
--- /dev/null
+++ b/res/img/element-icons/room/composer/bulleted_list.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/room/composer/code_block.svg b/res/img/element-icons/room/composer/code_block.svg
new file mode 100644
index 0000000000..dd0be2aefc
--- /dev/null
+++ b/res/img/element-icons/room/composer/code_block.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/room/composer/numbered_list.svg b/res/img/element-icons/room/composer/numbered_list.svg
new file mode 100644
index 0000000000..46a5438f3f
--- /dev/null
+++ b/res/img/element-icons/room/composer/numbered_list.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/room/composer/quote.svg b/res/img/element-icons/room/composer/quote.svg
new file mode 100644
index 0000000000..82cc2d2875
--- /dev/null
+++ b/res/img/element-icons/room/composer/quote.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/res/img/element-icons/settings/devices.svg b/res/img/element-icons/settings/devices.svg
new file mode 100644
index 0000000000..4d2f171993
--- /dev/null
+++ b/res/img/element-icons/settings/devices.svg
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss
index 6fd88d63a9..882ef1f005 100644
--- a/res/themes/dark/css/_dark.pcss
+++ b/res/themes/dark/css/_dark.pcss
@@ -224,6 +224,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
 $breadcrumb-placeholder-bg-color: #272c35;
 $theme-button-bg-color: #e3e8f0;
 $resend-button-divider-color: rgba($header-panel-text-primary-color, 0.74);
+$inlinecode-border-color: $quinary-content;
+$inlinecode-background-color: $system;
 $codeblock-background-color: #2a3039;
 $scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
 $selected-color: $room-highlight-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss
index ed1ff5793b..03088f216b 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.pcss
+++ b/res/themes/legacy-dark/css/_legacy-dark.pcss
@@ -190,6 +190,8 @@ $appearance-tab-border-color: $room-highlight-color;
 $composer-shadow-color: tranparent;
 
 $codeblock-background-color: #2a3039;
+$inlinecode-border-color: #2a3039;
+$inlinecode-background-color: #2a3039;
 
 /* Bubble tiles */
 $eventbubble-self-bg: #14322e;
diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss
index ae52b078a7..dd152f368e 100644
--- a/res/themes/legacy-light/css/_legacy-light.pcss
+++ b/res/themes/legacy-light/css/_legacy-light.pcss
@@ -290,6 +290,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
 $composer-shadow-color: tranparent;
 
 $codeblock-background-color: $header-panel-bg-color;
+$inlinecode-border-color: $header-panel-bg-color;
+$inlinecode-background-color: $header-panel-bg-color;
 
 /* Bubble tiles */
 $eventbubble-self-bg: #f0fbf8;
diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss
index 4a1ae7e53a..6d59801779 100644
--- a/res/themes/light/css/_light.pcss
+++ b/res/themes/light/css/_light.pcss
@@ -295,6 +295,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
 $breadcrumb-placeholder-bg-color: #e8eef5;
 $theme-button-bg-color: $quinary-content;
 $resend-button-divider-color: $input-darker-bg-color;
+$inlinecode-border-color: $quinary-content;
+$inlinecode-background-color: $system;
 $codeblock-background-color: $header-panel-bg-color;
 $scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
 $selected-color: $secondary-accent-color;
diff --git a/src/@types/diff-dom.d.ts b/src/@types/diff-dom.d.ts
index 5998b0e404..bf9150a697 100644
--- a/src/@types/diff-dom.d.ts
+++ b/src/@types/diff-dom.d.ts
@@ -20,10 +20,10 @@ declare module "diff-dom" {
         name: string;
         text?: string;
         route: number[];
-        value: string;
-        element: unknown;
-        oldValue: string;
-        newValue: string;
+        value: HTMLElement | string;
+        element: HTMLElement | string;
+        oldValue: HTMLElement | string;
+        newValue: HTMLElement | string;
     }
 
     interface IOpts {}
diff --git a/src/@types/polyfill.ts b/src/@types/polyfill.ts
index d24d2f4463..6434512e75 100644
--- a/src/@types/polyfill.ts
+++ b/src/@types/polyfill.ts
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 // This is intended to fix re-resizer because of its unguarded `instanceof TouchEvent` checks.
-export function polyfillTouchEvent() {
+export function polyfillTouchEvent(): void {
     // Firefox doesn't have touch events without touch devices being present, so create a fake
     // one we can rely on lying about.
     if (!window.TouchEvent) {
diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx
index f6f7edd2c2..226f5b692b 100644
--- a/src/AsyncWrapper.tsx
+++ b/src/AsyncWrapper.tsx
@@ -47,7 +47,7 @@ export default class AsyncWrapper extends React.Component {
         error: null,
     };
 
-    public componentDidMount() {
+    public componentDidMount(): void {
         // XXX: temporary logging to try to diagnose
         // https://github.com/vector-im/element-web/issues/3148
         logger.log("Starting load of AsyncWrapper for modal");
@@ -69,15 +69,15 @@ export default class AsyncWrapper extends React.Component {
             });
     }
 
-    public componentWillUnmount() {
+    public componentWillUnmount(): void {
         this.unmounted = true;
     }
 
-    private onWrapperCancelClick = () => {
+    private onWrapperCancelClick = (): void => {
         this.props.onFinished(false);
     };
 
-    public render() {
+    public render(): JSX.Element {
         if (this.state.component) {
             const Component = this.state.component;
             return ;
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 30a53e74a0..8a3f10a22c 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -137,7 +137,12 @@ export function getInitialLetter(name: string): string {
     return split(name, "", 1)[0].toUpperCase();
 }
 
-export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
+export function avatarUrlForRoom(
+    room: Room,
+    width: number,
+    height: number,
+    resizeMethod?: ResizeMethod,
+): string | null {
     if (!room) return null; // null-guard
 
     if (room.getMxcAvatarUrl()) {
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 46a406271c..22d274ffb1 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -272,7 +272,7 @@ export default abstract class BasePlatform {
         return null;
     }
 
-    public setLanguage(preferredLangs: string[]) {}
+    public setLanguage(preferredLangs: string[]): void {}
 
     public setSpellCheckEnabled(enabled: boolean): void {}
 
@@ -280,7 +280,7 @@ export default abstract class BasePlatform {
         return null;
     }
 
-    public setSpellCheckLanguages(preferredLangs: string[]) {}
+    public setSpellCheckLanguages(preferredLangs: string[]): void {}
 
     public getSpellCheckLanguages(): Promise | null {
         return null;
diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts
index 56e137cc01..01f84421b6 100644
--- a/src/BlurhashEncoder.ts
+++ b/src/BlurhashEncoder.ts
@@ -40,7 +40,7 @@ export class BlurhashEncoder {
         this.worker.onmessage = this.onMessage;
     }
 
-    private onMessage = (ev: MessageEvent) => {
+    private onMessage = (ev: MessageEvent): void => {
         const { seq, blurhash } = ev.data;
         const deferred = this.pendingDeferredMap.get(seq);
         if (deferred) {
diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts
index a2fe27d0a9..1381e9431e 100644
--- a/src/ContentMessages.ts
+++ b/src/ContentMessages.ts
@@ -68,16 +68,20 @@ interface IMediaConfig {
  * @param {File} imageFile The file to load in an image element.
  * @return {Promise} A promise that resolves with the html image element.
  */
-async function loadImageElement(imageFile: File) {
+async function loadImageElement(imageFile: File): Promise<{
+    width: number;
+    height: number;
+    img: HTMLImageElement;
+}> {
     // Load the file into an html element
     const img = new Image();
     const objectUrl = URL.createObjectURL(imageFile);
     const imgPromise = new Promise((resolve, reject) => {
-        img.onload = function () {
+        img.onload = function (): void {
             URL.revokeObjectURL(objectUrl);
             resolve(img);
         };
-        img.onerror = function (e) {
+        img.onerror = function (e): void {
             reject(e);
         };
     });
@@ -185,13 +189,13 @@ function loadVideoElement(videoFile: File): Promise {
 
         const reader = new FileReader();
 
-        reader.onload = function (ev) {
+        reader.onload = function (ev): void {
             // Wait until we have enough data to thumbnail the first frame.
-            video.onloadeddata = async function () {
+            video.onloadeddata = async function (): Promise {
                 resolve(video);
                 video.pause();
             };
-            video.onerror = function (e) {
+            video.onerror = function (e): void {
                 reject(e);
             };
 
@@ -206,7 +210,7 @@ function loadVideoElement(videoFile: File): Promise {
             video.load();
             video.play();
         };
-        reader.onerror = function (e) {
+        reader.onerror = function (e): void {
             reject(e);
         };
         reader.readAsDataURL(videoFile);
@@ -253,10 +257,10 @@ function infoForVideoFile(
 function readFileAsArrayBuffer(file: File | Blob): Promise {
     return new Promise((resolve, reject) => {
         const reader = new FileReader();
-        reader.onload = function (e) {
+        reader.onload = function (e): void {
             resolve(e.target.result as ArrayBuffer);
         };
-        reader.onerror = function (e) {
+        reader.onerror = function (e): void {
             reject(e);
         };
         reader.readAsArrayBuffer(file);
@@ -461,7 +465,7 @@ export default class ContentMessages {
         matrixClient: MatrixClient,
         replyToEvent: MatrixEvent | undefined,
         promBefore?: Promise,
-    ) {
+    ): Promise {
         const fileName = file.name || _t("Attachment");
         const content: Omit & { info: Partial } = {
             body: fileName,
@@ -491,7 +495,7 @@ export default class ContentMessages {
         this.inprogress.push(upload);
         dis.dispatch({ action: Action.UploadStarted, upload });
 
-        function onProgress(progress: UploadProgress) {
+        function onProgress(progress: UploadProgress): void {
             upload.onProgress(progress);
             dis.dispatch({ action: Action.UploadProgress, upload });
         }
@@ -568,7 +572,7 @@ export default class ContentMessages {
         }
     }
 
-    private isFileSizeAcceptable(file: File) {
+    private isFileSizeAcceptable(file: File): boolean {
         if (
             this.mediaConfig !== null &&
             this.mediaConfig["m.upload.size"] !== undefined &&
@@ -599,7 +603,7 @@ export default class ContentMessages {
             });
     }
 
-    public static sharedInstance() {
+    public static sharedInstance(): ContentMessages {
         if (window.mxContentMessages === undefined) {
             window.mxContentMessages = new ContentMessages();
         }
diff --git a/src/DateUtils.ts b/src/DateUtils.ts
index 1dab03121e..5973a7c5f2 100644
--- a/src/DateUtils.ts
+++ b/src/DateUtils.ts
@@ -188,7 +188,7 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
     return prevEventDate.getDay() !== nextEventDate.getDay();
 }
 
-export function formatFullDateNoDay(date: Date) {
+export function formatFullDateNoDay(date: Date): string {
     return _t("%(date)s at %(time)s", {
         date: date.toLocaleDateString().replace(/\//g, "-"),
         time: date.toLocaleTimeString().replace(/:/g, "-"),
@@ -205,7 +205,7 @@ export function formatFullDateNoDayISO(date: Date): string {
     return date.toISOString();
 }
 
-export function formatFullDateNoDayNoTime(date: Date) {
+export function formatFullDateNoDayNoTime(date: Date): string {
     return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate());
 }
 
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index 3d33ff0fda..be48717415 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -19,6 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
 import { CryptoEvent } from "matrix-js-sdk/src/crypto";
 import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
 import { SyncState } from "matrix-js-sdk/src/sync";
+import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
 
 import { MatrixClientPeg } from "./MatrixClientPeg";
 import dis from "./dispatcher/dispatcher";
@@ -56,7 +57,7 @@ export default class DeviceListener {
     // has the user dismissed any of the various nag toasts to setup encryption on this device?
     private dismissedThisDeviceToast = false;
     // cache of the key backup info
-    private keyBackupInfo: object = null;
+    private keyBackupInfo: IKeyBackupInfo | null = null;
     private keyBackupFetchedAt: number = null;
     private keyBackupStatusChecked = false;
     // We keep a list of our own device IDs so we can batch ones that were already
@@ -70,12 +71,12 @@ export default class DeviceListener {
     private enableBulkUnverifiedSessionsReminder = true;
     private deviceClientInformationSettingWatcherRef: string | undefined;
 
-    public static sharedInstance() {
+    public static sharedInstance(): DeviceListener {
         if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener();
         return window.mxDeviceListener;
     }
 
-    public start() {
+    public start(): void {
         this.running = true;
         MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
         MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
@@ -98,7 +99,7 @@ export default class DeviceListener {
         this.updateClientInformation();
     }
 
-    public stop() {
+    public stop(): void {
         this.running = false;
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
@@ -134,7 +135,7 @@ export default class DeviceListener {
      *
      * @param {String[]} deviceIds List of device IDs to dismiss notifications for
      */
-    public async dismissUnverifiedSessions(deviceIds: Iterable) {
+    public async dismissUnverifiedSessions(deviceIds: Iterable): Promise {
         logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(","));
         for (const d of deviceIds) {
             this.dismissed.add(d);
@@ -143,19 +144,19 @@ export default class DeviceListener {
         this.recheck();
     }
 
-    public dismissEncryptionSetup() {
+    public dismissEncryptionSetup(): void {
         this.dismissedThisDeviceToast = true;
         this.recheck();
     }
 
-    private ensureDeviceIdsAtStartPopulated() {
+    private ensureDeviceIdsAtStartPopulated(): void {
         if (this.ourDeviceIdsAtStart === null) {
             const cli = MatrixClientPeg.get();
             this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()).map((d) => d.deviceId));
         }
     }
 
-    private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
+    private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise => {
         // If we didn't know about *any* devices before (ie. it's fresh login),
         // then they are all pre-existing devices, so ignore this and set the
         // devicesAtStart list to the devices that we see after the fetch.
@@ -168,26 +169,26 @@ export default class DeviceListener {
         // before we download any new ones.
     };
 
-    private onDevicesUpdated = (users: string[]) => {
+    private onDevicesUpdated = (users: string[]): void => {
         if (!users.includes(MatrixClientPeg.get().getUserId())) return;
         this.recheck();
     };
 
-    private onDeviceVerificationChanged = (userId: string) => {
+    private onDeviceVerificationChanged = (userId: string): void => {
         if (userId !== MatrixClientPeg.get().getUserId()) return;
         this.recheck();
     };
 
-    private onUserTrustStatusChanged = (userId: string) => {
+    private onUserTrustStatusChanged = (userId: string): void => {
         if (userId !== MatrixClientPeg.get().getUserId()) return;
         this.recheck();
     };
 
-    private onCrossSingingKeysChanged = () => {
+    private onCrossSingingKeysChanged = (): void => {
         this.recheck();
     };
 
-    private onAccountData = (ev: MatrixEvent) => {
+    private onAccountData = (ev: MatrixEvent): void => {
         // User may have:
         // * migrated SSSS to symmetric
         // * uploaded keys to secret storage
@@ -202,13 +203,13 @@ export default class DeviceListener {
         }
     };
 
-    private onSync = (state: SyncState, prevState?: SyncState) => {
+    private onSync = (state: SyncState, prevState?: SyncState): void => {
         if (state === "PREPARED" && prevState === null) {
             this.recheck();
         }
     };
 
-    private onRoomStateEvents = (ev: MatrixEvent) => {
+    private onRoomStateEvents = (ev: MatrixEvent): void => {
         if (ev.getType() !== EventType.RoomEncryption) return;
 
         // If a room changes to encrypted, re-check as it may be our first
@@ -216,7 +217,7 @@ export default class DeviceListener {
         this.recheck();
     };
 
-    private onAction = ({ action }: ActionPayload) => {
+    private onAction = ({ action }: ActionPayload): void => {
         if (action !== Action.OnLoggedIn) return;
         this.recheck();
         this.updateClientInformation();
@@ -224,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() {
+    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();
@@ -233,7 +234,7 @@ export default class DeviceListener {
         return this.keyBackupInfo;
     }
 
-    private shouldShowSetupEncryptionToast() {
+    private shouldShowSetupEncryptionToast(): boolean {
         // If we're in the middle of a secret storage operation, we're likely
         // modifying the state involved here, so don't add new toasts to setup.
         if (isSecretStorageBeingAccessed()) return false;
@@ -242,7 +243,7 @@ export default class DeviceListener {
         return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId));
     }
 
-    private async recheck() {
+    private async recheck(): Promise {
         if (!this.running) return; // we have been stopped
         const cli = MatrixClientPeg.get();
 
@@ -359,7 +360,7 @@ export default class DeviceListener {
         this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
     }
 
-    private checkKeyBackupStatus = async () => {
+    private checkKeyBackupStatus = async (): Promise => {
         if (this.keyBackupStatusChecked) {
             return;
         }
@@ -388,7 +389,7 @@ export default class DeviceListener {
         }
     };
 
-    private updateClientInformation = async () => {
+    private updateClientInformation = async (): Promise => {
         try {
             if (this.shouldRecordClientInformation) {
                 await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get());
diff --git a/src/Editing.ts b/src/Editing.ts
index 57e58cc2a7..e331a3dca1 100644
--- a/src/Editing.ts
+++ b/src/Editing.ts
@@ -16,5 +16,6 @@ limitations under the License.
 
 import { TimelineRenderingType } from "./contexts/RoomContext";
 
-export const editorRoomKey = (roomId: string, context: TimelineRenderingType) => `mx_edit_room_${roomId}_${context}`;
-export const editorStateKey = (eventId: string) => `mx_edit_state_${eventId}`;
+export const editorRoomKey = (roomId: string, context: TimelineRenderingType): string =>
+    `mx_edit_room_${roomId}_${context}`;
+export const editorStateKey = (eventId: string): string => `mx_edit_state_${eventId}`;
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 19da52f45b..3e67e42256 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -449,9 +449,9 @@ export interface IOptsReturnString extends IOpts {
     returnString: true;
 }
 
-const emojiToHtmlSpan = (emoji: string) =>
+const emojiToHtmlSpan = (emoji: string): string =>
     `${emoji}`;
-const emojiToJsxSpan = (emoji: string, key: number) => (
+const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => (
     
         {emoji}
     
@@ -505,7 +505,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 = {}) {
+export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}): ReactNode | string {
     const isFormattedBody = content.format === "org.matrix.custom.html" && !!content.formatted_body;
     let bodyHasEmoji = false;
     let isHtmlMessage = false;
diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts
index fffa3fbb9f..8234f5bc75 100644
--- a/src/IConfigOptions.ts
+++ b/src/IConfigOptions.ts
@@ -99,7 +99,7 @@ export interface IConfigOptions {
     features?: Record; // 
 
     bug_report_endpoint_url?: string; // omission disables bug reporting
-    uisi_autorageshake_app?: string;
+    uisi_autorageshake_app?: string; // defaults to "element-auto-uisi"
     sentry?: {
         dsn: string;
         environment?: string; // "production", etc
diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts
index 1618aa8f4c..42db71ebab 100644
--- a/src/ImageUtils.ts
+++ b/src/ImageUtils.ts
@@ -28,7 +28,7 @@ limitations under the License.
  * consume in the timeline, when performing scroll offset calculations
  * (e.g. scroll locking)
  */
-export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
+export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number): number {
     if (!fullWidth || !fullHeight) {
         // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
         // log this because it's spammy
diff --git a/src/Keyboard.ts b/src/Keyboard.ts
index 3a425cef31..9d4d3f6152 100644
--- a/src/Keyboard.ts
+++ b/src/Keyboard.ts
@@ -76,7 +76,7 @@ export const Key = {
 
 export const IS_MAC = navigator.platform.toUpperCase().includes("MAC");
 
-export function isOnlyCtrlOrCmdKeyEvent(ev) {
+export function isOnlyCtrlOrCmdKeyEvent(ev: 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 d81bd73661..82e5cac996 100644
--- a/src/LegacyCallHandler.tsx
+++ b/src/LegacyCallHandler.tsx
@@ -169,7 +169,7 @@ export default class LegacyCallHandler extends EventEmitter {
 
     private silencedCalls = new Set(); // callIds
 
-    public static get instance() {
+    public static get instance(): LegacyCallHandler {
         if (!window.mxLegacyCallHandler) {
             window.mxLegacyCallHandler = new LegacyCallHandler();
         }
@@ -456,7 +456,7 @@ export default class LegacyCallHandler extends EventEmitter {
         return callsNotInThatRoom;
     }
 
-    public getAllActiveCallsForPip(roomId: string) {
+    public getAllActiveCallsForPip(roomId: string): MatrixCall[] {
         const room = MatrixClientPeg.get().getRoom(roomId);
         if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
             // This checks if there is space for the call view in the aux panel
@@ -478,7 +478,7 @@ export default class LegacyCallHandler extends EventEmitter {
         const audio = document.getElementById(audioId) as HTMLMediaElement;
         if (audio) {
             this.addEventListenersForAudioElement(audio);
-            const playAudio = async () => {
+            const playAudio = async (): Promise => {
                 try {
                     if (audio.muted) {
                         logger.error(
@@ -524,7 +524,7 @@ export default class LegacyCallHandler extends EventEmitter {
         // TODO: Attach an invisible element for this instead
         // which listens?
         const audio = document.getElementById(audioId) as HTMLMediaElement;
-        const pauseAudio = () => {
+        const pauseAudio = (): void => {
             logger.debug(`${logPrefix} pausing audio`);
             // pause doesn't return a promise, so just do it
             audio.pause();
@@ -600,7 +600,7 @@ export default class LegacyCallHandler extends EventEmitter {
             this.setCallListeners(newCall);
             this.setCallState(newCall, newCall.state);
         });
-        call.on(CallEvent.AssertedIdentityChanged, async () => {
+        call.on(CallEvent.AssertedIdentityChanged, async (): Promise => {
             if (!this.matchesCallForThisRoom(call)) return;
 
             logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
@@ -808,7 +808,7 @@ export default class LegacyCallHandler extends EventEmitter {
 
     private showICEFallbackPrompt(): void {
         const cli = MatrixClientPeg.get();
-        const code = (sub) => {sub};
+        const code = (sub: string): JSX.Element => {sub};
         Modal.createDialog(
             QuestionDialog,
             {
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index db6d15c188..30aab429fb 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -219,7 +219,7 @@ export function attemptTokenLogin(
     })
         .then(function (creds) {
             logger.log("Logged in with token");
-            return clearStorage().then(async () => {
+            return clearStorage().then(async (): Promise => {
                 await persistCredentials(creds);
                 // remember that we just logged in
                 sessionStorage.setItem("mx_fresh_login", String(true));
@@ -406,7 +406,7 @@ async function pickleKeyToAesKey(pickleKey: string): Promise {
     );
 }
 
-async function abortLogin() {
+async function abortLogin(): Promise {
     const signOut = await showStorageEvictedDialog();
     if (signOut) {
         await clearStorage();
diff --git a/src/Livestream.ts b/src/Livestream.ts
index d339045c94..563136983b 100644
--- a/src/Livestream.ts
+++ b/src/Livestream.ts
@@ -20,14 +20,14 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
 import SdkConfig from "./SdkConfig";
 import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
 
-export function getConfigLivestreamUrl() {
+export function getConfigLivestreamUrl(): string | undefined {
     return SdkConfig.get("audio_stream_url");
 }
 
 // Dummy rtmp URL used to signal that we want a special audio-only stream
 const AUDIOSTREAM_DUMMY_URL = "rtmp://audiostream.dummy/";
 
-async function createLiveStream(roomId: string) {
+async function createLiveStream(roomId: string): Promise {
     const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
 
     const url = getConfigLivestreamUrl() + "/createStream";
@@ -47,7 +47,7 @@ async function createLiveStream(roomId: string) {
     return respBody["stream_id"];
 }
 
-export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
+export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string): Promise {
     const streamId = await createLiveStream(roomId);
 
     await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
diff --git a/src/Login.ts b/src/Login.ts
index ec769e8cb3..90f8f5d0eb 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -122,7 +122,7 @@ export default class Login {
             initial_device_display_name: this.defaultDeviceDisplayName,
         };
 
-        const tryFallbackHs = (originalError) => {
+        const tryFallbackHs = (originalError: Error): Promise => {
             return sendLoginRequest(this.fallbackHsUrl, this.isUrl, "m.login.password", loginParams).catch(
                 (fallbackError) => {
                     logger.log("fallback HS login failed", fallbackError);
diff --git a/src/Markdown.ts b/src/Markdown.ts
index 404da0ca8d..a32126117d 100644
--- a/src/Markdown.ts
+++ b/src/Markdown.ts
@@ -56,7 +56,7 @@ function isMultiLine(node: commonmark.Node): boolean {
     return par.firstChild != par.lastChild;
 }
 
-function getTextUntilEndOrLinebreak(node: commonmark.Node) {
+function getTextUntilEndOrLinebreak(node: commonmark.Node): string {
     let currentNode = node;
     let text = "";
     while (currentNode !== null && currentNode.type !== "softbreak" && currentNode.type !== "linebreak") {
@@ -137,7 +137,7 @@ export default class Markdown {
      * See: https://github.com/vector-im/element-web/issues/4674
      * @param parsed
      */
-    private repairLinks(parsed: commonmark.Node) {
+    private repairLinks(parsed: commonmark.Node): commonmark.Node {
         const walker = parsed.walker();
         let event: commonmark.NodeWalkingStep = null;
         let text = "";
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 290c082344..1b21f74b5e 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -77,7 +77,7 @@ export class ModalManager extends TypedEventEmitter[] = [];
 
-    private static getOrCreateContainer() {
+    private static getOrCreateContainer(): HTMLElement {
         let container = document.getElementById(DIALOG_CONTAINER_ID);
 
         if (!container) {
@@ -89,7 +89,7 @@ export class ModalManager extends TypedEventEmitter 0;
+    public hasDialogs(): boolean {
+        return !!this.priorityModal || !!this.staticModal || this.modals.length > 0;
     }
 
     public createDialog(
         Element: React.ComponentType,
         ...rest: ParametersWithoutFirst
-    ) {
+    ): IHandle {
         return this.createDialogAsync(Promise.resolve(Element), ...rest);
     }
 
     public appendDialog(
         Element: React.ComponentType,
         ...rest: ParametersWithoutFirst
-    ) {
+    ): IHandle {
         return this.appendDialogAsync(Promise.resolve(Element), ...rest);
     }
 
-    public closeCurrentModal(reason: string) {
+    public closeCurrentModal(reason: string): void {
         const modal = this.getCurrentModal();
         if (!modal) {
             return;
@@ -139,7 +139,11 @@ export class ModalManager extends TypedEventEmitter,
         className?: string,
         options?: IOptions,
-    ) {
+    ): {
+        modal: IModal;
+        closeDialog: IHandle["close"];
+        onFinishedProm: IHandle["finished"];
+    } {
         const modal: IModal = {
             onFinished: props ? props.onFinished : null,
             onBeforeClose: options.onBeforeClose,
@@ -173,7 +177,7 @@ export class ModalManager extends TypedEventEmitter["close"], IHandle["finished"]] {
         const deferred = defer();
         return [
-            async (...args: T) => {
+            async (...args: T): Promise => {
                 if (modal.beforeClosePromise) {
                     await modal.beforeClosePromise;
                 } else if (modal.onBeforeClose) {
@@ -302,7 +306,7 @@ export class ModalManager extends TypedEventEmitter {
+    private onBackgroundClick = (): void => {
         const modal = this.getCurrentModal();
         if (!modal) {
             return;
@@ -320,7 +324,7 @@ export class ModalManager extends TypedEventEmitter {
         // await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
         await sleep(0);
 
diff --git a/src/Notifier.ts b/src/Notifier.ts
index 7e1f9eb0a4..42909a2632 100644
--- a/src/Notifier.ts
+++ b/src/Notifier.ts
@@ -50,7 +50,8 @@ import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded
 import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
 import ToastStore from "./stores/ToastStore";
 import { ElementCall } from "./models/Call";
-import { VoiceBroadcastChunkEventType } from "./voice-broadcast";
+import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
+import { getSenderName } from "./utils/event/getSenderName";
 
 /*
  * Dispatches:
@@ -80,9 +81,16 @@ const msgTypeHandlers = {
     },
     [MsgType.Audio]: (event: MatrixEvent): string | null => {
         if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
-            // mute broadcast chunks
+            if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
+                // Show a notification for the first broadcast chunk.
+                // At this point a user received something to listen to.
+                return _t("%(senderName)s started a voice broadcast", { senderName: getSenderName(event) });
+            }
+
+            // Mute other broadcast chunks
             return null;
         }
+
         return TextForEvent.textForEvent(event);
     },
 };
@@ -448,6 +456,9 @@ export const Notifier = {
     },
 
     _evaluateEvent: function (ev: MatrixEvent) {
+        // Mute notifications for broadcast info events
+        if (ev.getType() === VoiceBroadcastInfoEventType) return;
+
         let roomId = ev.getRoomId();
         if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
             // Attempt to translate a virtual room to a native one
diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts
index f6661a35f1..7dbc8a5406 100644
--- a/src/PasswordReset.ts
+++ b/src/PasswordReset.ts
@@ -104,6 +104,10 @@ export default class PasswordReset {
         );
     }
 
+    public setLogoutDevices(logoutDevices: boolean): void {
+        this.logoutDevices = logoutDevices;
+    }
+
     public async setNewPassword(password: string): Promise {
         this.password = password;
         await this.checkEmailLinkClicked();
diff --git a/src/PlatformPeg.ts b/src/PlatformPeg.ts
index ab45f1ab1a..cc7bb8dc17 100644
--- a/src/PlatformPeg.ts
+++ b/src/PlatformPeg.ts
@@ -32,13 +32,13 @@ import { PlatformSetPayload } from "./dispatcher/payloads/PlatformSetPayload";
  * object.
  */
 export class PlatformPeg {
-    private platform: BasePlatform = null;
+    private platform: BasePlatform | null = null;
 
     /**
      * Returns the current Platform object for the application.
      * This should be an instance of a class extending BasePlatform.
      */
-    public get() {
+    public get(): BasePlatform | null {
         return this.platform;
     }
 
@@ -46,7 +46,7 @@ export class PlatformPeg {
      * Sets the current platform handler object to use for the application.
      * @param {BasePlatform} platform an instance of a class extending BasePlatform.
      */
-    public set(platform: BasePlatform) {
+    public set(platform: BasePlatform): void {
         this.platform = platform;
         defaultDispatcher.dispatch({
             action: Action.PlatformSet,
diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts
index 3e1773be29..c8a99ab426 100644
--- a/src/PosthogAnalytics.ts
+++ b/src/PosthogAnalytics.ts
@@ -175,7 +175,7 @@ export class PosthogAnalytics {
         this.onLayoutUpdated();
     }
 
-    private onLayoutUpdated = () => {
+    private onLayoutUpdated = (): void => {
         let layout: UserProperties["WebLayout"];
 
         switch (SettingsStore.getValue("layout")) {
@@ -195,7 +195,7 @@ export class PosthogAnalytics {
         this.setProperty("WebLayout", layout);
     };
 
-    private onAction = (payload: ActionPayload) => {
+    private onAction = (payload: ActionPayload): void => {
         if (payload.action !== Action.SettingUpdated) return;
         const settingsPayload = payload as SettingUpdatedPayload;
         if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
@@ -232,7 +232,7 @@ export class PosthogAnalytics {
         return properties;
     };
 
-    private registerSuperProperties(properties: Properties) {
+    private registerSuperProperties(properties: Properties): void {
         if (this.enabled) {
             this.posthog.register(properties);
         }
@@ -255,7 +255,7 @@ export class PosthogAnalytics {
     }
 
     // eslint-disable-nextline no-unused-varsx
-    private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions) {
+    private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions): void {
         if (!this.enabled) {
             return;
         }
diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts
index 09c7225a3d..8a8b02965c 100644
--- a/src/PosthogTrackers.ts
+++ b/src/PosthogTrackers.ts
@@ -107,20 +107,20 @@ export default class PosthogTrackers {
 }
 
 export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> {
-    public componentDidMount() {
+    public componentDidMount(): void {
         PosthogTrackers.instance.trackOverride(this.props.screenName);
     }
 
-    public componentDidUpdate() {
+    public componentDidUpdate(): void {
         // We do not clear the old override here so that we do not send the non-override screen as a transition
         PosthogTrackers.instance.trackOverride(this.props.screenName);
     }
 
-    public componentWillUnmount() {
+    public componentWillUnmount(): void {
         PosthogTrackers.instance.clearOverride(this.props.screenName);
     }
 
-    public render() {
+    public render(): JSX.Element {
         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 3684d6f779..c13cc32b60 100644
--- a/src/Presence.ts
+++ b/src/Presence.ts
@@ -41,7 +41,7 @@ class Presence {
      * Start listening the user activity to evaluate his presence state.
      * Any state change will be sent to the homeserver.
      */
-    public async start() {
+    public async start(): Promise {
         this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
         // the user_activity_start action starts the timer
         this.dispatcherRef = dis.register(this.onAction);
@@ -58,7 +58,7 @@ class Presence {
     /**
      * Stop tracking user activity
      */
-    public stop() {
+    public stop(): void {
         if (this.dispatcherRef) {
             dis.unregister(this.dispatcherRef);
             this.dispatcherRef = null;
@@ -73,11 +73,11 @@ class Presence {
      * Get the current presence state.
      * @returns {string} the presence state (see PRESENCE enum)
      */
-    public getState() {
+    public getState(): State {
         return this.state;
     }
 
-    private onAction = (payload: ActionPayload) => {
+    private onAction = (payload: ActionPayload): void => {
         if (payload.action === "user_activity") {
             this.setState(State.Online);
             this.unavailableTimer.restart();
@@ -89,7 +89,7 @@ class Presence {
      * If the state has changed, the homeserver will be notified.
      * @param {string} newState the new presence state (see PRESENCE enum)
      */
-    private async setState(newState: State) {
+    private async setState(newState: State): Promise {
         if (newState === this.state) {
             return;
         }
diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts
index baa6e6f632..a775eb3d4e 100644
--- a/src/ScalarAuthClient.ts
+++ b/src/ScalarAuthClient.ts
@@ -49,7 +49,7 @@ export default class ScalarAuthClient {
         this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
     }
 
-    private writeTokenToStore() {
+    private writeTokenToStore(): void {
         window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
         if (this.isDefaultManager) {
             // We remove the old token from storage to migrate upwards. This is safe
@@ -72,7 +72,7 @@ export default class ScalarAuthClient {
         return this.readTokenFromStore();
     }
 
-    public setTermsInteractionCallback(callback) {
+    public setTermsInteractionCallback(callback: TermsInteractionCallback): void {
         this.termsInteractionCallback = callback;
     }
 
diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts
index b1912c484a..fec671eab4 100644
--- a/src/ScalarMessaging.ts
+++ b/src/ScalarMessaging.ts
@@ -711,7 +711,7 @@ function returnStateEvent(event: MessageEvent, roomId: string, eventType: s
     sendResponse(event, stateEvent.getContent());
 }
 
-async function getOpenIdToken(event: MessageEvent) {
+async function getOpenIdToken(event: MessageEvent): Promise {
     try {
         const tokenObject = await MatrixClientPeg.get().getOpenIdToken();
         sendResponse(event, tokenObject);
@@ -728,7 +728,7 @@ async function sendEvent(
         content?: IContent;
     }>,
     roomId: string,
-) {
+): Promise {
     const eventType = event.data.type;
     const stateKey = event.data.state_key;
     const content = event.data.content;
@@ -786,7 +786,7 @@ async function readEvents(
         limit?: number;
     }>,
     roomId: string,
-) {
+): Promise {
     const eventType = event.data.type;
     const stateKey = event.data.state_key;
     const limit = event.data.limit;
diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts
index 75cc5ef059..5aaab6e2f4 100644
--- a/src/SdkConfig.ts
+++ b/src/SdkConfig.ts
@@ -27,6 +27,8 @@ export const DEFAULTS: IConfigOptions = {
     integrations_ui_url: "https://scalar.vector.im/",
     integrations_rest_url: "https://scalar.vector.im/api",
     bug_report_endpoint_url: null,
+    uisi_autorageshake_app: "element-auto-uisi",
+
     jitsi: {
         preferred_domain: "meet.element.io",
     },
@@ -56,7 +58,7 @@ export default class SdkConfig {
     private static instance: IConfigOptions;
     private static fallback: SnakedObject;
 
-    private static setInstance(i: IConfigOptions) {
+    private static setInstance(i: IConfigOptions): void {
         SdkConfig.instance = i;
         SdkConfig.fallback = new SnakedObject(i);
 
@@ -90,18 +92,18 @@ export default class SdkConfig {
         return val === undefined ? undefined : null;
     }
 
-    public static put(cfg: Partial) {
+    public static put(cfg: Partial): void {
         SdkConfig.setInstance({ ...DEFAULTS, ...cfg });
     }
 
     /**
      * Resets the config to be completely empty.
      */
-    public static unset() {
+    public static unset(): void {
         SdkConfig.setInstance({}); // safe to cast - defaults will be applied
     }
 
-    public static add(cfg: Partial) {
+    public static add(cfg: Partial): void {
         SdkConfig.put({ ...SdkConfig.get(), ...cfg });
     }
 }
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index ed72417ecc..20db6594b0 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -86,7 +86,7 @@ async function confirmToDismiss(): Promise {
 type KeyParams = { passphrase: string; recoveryKey: string };
 
 function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise {
-    return async ({ passphrase, recoveryKey }) => {
+    return async ({ passphrase, recoveryKey }): Promise => {
         if (passphrase) {
             return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
         } else {
@@ -151,7 +151,7 @@ async function getSecretStorageKey({
         /* props= */
         {
             keyInfo,
-            checkPrivateKey: async (input: KeyParams) => {
+            checkPrivateKey: async (input: KeyParams): Promise => {
                 const key = await inputToKey(input);
                 return MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
             },
@@ -160,7 +160,7 @@ async function getSecretStorageKey({
         /* isPriorityModal= */ false,
         /* isStaticModal= */ false,
         /* options= */ {
-            onBeforeClose: async (reason) => {
+            onBeforeClose: async (reason): Promise => {
                 if (reason === "backgroundClick") {
                     return confirmToDismiss();
                 }
@@ -196,7 +196,7 @@ export async function getDehydrationKey(
         /* props= */
         {
             keyInfo,
-            checkPrivateKey: async (input) => {
+            checkPrivateKey: async (input): Promise => {
                 const key = await inputToKey(input);
                 try {
                     checkFunc(key);
@@ -210,7 +210,7 @@ export async function getDehydrationKey(
         /* isPriorityModal= */ false,
         /* isStaticModal= */ false,
         /* options= */ {
-            onBeforeClose: async (reason) => {
+            onBeforeClose: async (reason): Promise => {
                 if (reason === "backgroundClick") {
                     return confirmToDismiss();
                 }
@@ -324,7 +324,7 @@ export async function promptForBackupPassphrase(): Promise {
  * bootstrapped. Optional.
  * @param {bool} [forceReset] Reset secret storage even if it's already set up
  */
-export async function accessSecretStorage(func = async () => {}, forceReset = false) {
+export async function accessSecretStorage(func = async (): Promise => {}, forceReset = false): Promise {
     const cli = MatrixClientPeg.get();
     secretStorageBeingAccessed = true;
     try {
@@ -342,7 +342,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa
                 /* priority = */ false,
                 /* static = */ true,
                 /* options = */ {
-                    onBeforeClose: async (reason) => {
+                    onBeforeClose: async (reason): Promise => {
                         // If Secure Backup is required, you cannot leave the modal.
                         if (reason === "backgroundClick") {
                             return !isSecureBackupRequired();
@@ -357,7 +357,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa
             }
         } else {
             await cli.bootstrapCrossSigning({
-                authUploadDeviceSigningKeys: async (makeRequest) => {
+                authUploadDeviceSigningKeys: async (makeRequest): Promise => {
                     const { finished } = Modal.createDialog(InteractiveAuthDialog, {
                         title: _t("Setting up keys"),
                         matrixClient: cli,
diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts
index 1fd8f839bd..e95641d4c9 100644
--- a/src/SendHistoryManager.ts
+++ b/src/SendHistoryManager.ts
@@ -60,7 +60,7 @@ export default class SendHistoryManager {
         };
     }
 
-    public save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
+    public save(editorModel: EditorModel, replyEvent?: MatrixEvent): void {
         const item = SendHistoryManager.createItem(editorModel, replyEvent);
         this.history.push(item);
         this.currentIndex = this.history.length;
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 910c077525..9b23bd4138 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -85,7 +85,7 @@ const singleMxcUpload = async (): Promise => {
 
             Modal.createDialog(UploadConfirmDialog, {
                 file,
-                onFinished: async (shouldContinue) => {
+                onFinished: async (shouldContinue): Promise => {
                     if (shouldContinue) {
                         const { content_uri: uri } = await MatrixClientPeg.get().uploadContent(file);
                         resolve(uri);
@@ -151,11 +151,11 @@ export class Command {
         this.analyticsName = opts.analyticsName;
     }
 
-    public getCommand() {
+    public getCommand(): string {
         return `/${this.command}`;
     }
 
-    public getCommandWithArgs() {
+    public getCommandWithArgs(): string {
         return this.getCommand() + " " + this.args;
     }
 
@@ -184,7 +184,7 @@ export class Command {
         return this.runFn(roomId, args);
     }
 
-    public getUsage() {
+    public getUsage(): string {
         return _t("Usage") + ": " + this.getCommandWithArgs();
     }
 
@@ -193,15 +193,15 @@ export class Command {
     }
 }
 
-function reject(error) {
+function reject(error?: any): RunResult {
     return { error };
 }
 
-function success(promise?: Promise) {
+function success(promise?: Promise): RunResult {
     return { promise };
 }
 
-function successSync(value: any) {
+function successSync(value: any): RunResult {
     return success(Promise.resolve(value));
 }
 
@@ -319,7 +319,7 @@ export const Commands = [
                 );
 
                 return success(
-                    finished.then(async ([resp]) => {
+                    finished.then(async ([resp]): Promise => {
                         if (!resp?.continue) return;
                         await upgradeRoom(room, args, resp.invite);
                     }),
@@ -338,7 +338,7 @@ export const Commands = [
         runFn: function (roomId, args) {
             if (args) {
                 return success(
-                    (async () => {
+                    (async (): Promise => {
                         const unixTimestamp = Date.parse(args);
                         if (!unixTimestamp) {
                             throw newTranslatableError(
@@ -501,7 +501,9 @@ export const Commands = [
                 ? ContentHelpers.parseTopicContent(content)
                 : { text: _t("This room has no topic.") };
 
-            const ref = (e) => e && linkifyElement(e);
+            const ref = (e): void => {
+                if (e) linkifyElement(e);
+            };
             const body = topicToHtml(topic.text, topic.html, ref, true);
 
             Modal.createDialog(InfoDialog, {
@@ -1028,7 +1030,7 @@ export const Commands = [
                     const fingerprint = matches[3];
 
                     return success(
-                        (async () => {
+                        (async (): Promise => {
                             const device = cli.getStoredDevice(userId, deviceId);
                             if (!device) {
                                 throw newTranslatableError("Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", {
@@ -1205,7 +1207,7 @@ export const Commands = [
         },
         runFn: (roomId) => {
             return success(
-                (async () => {
+                (async (): Promise => {
                     const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId);
                     if (!room) throw newTranslatableError("No virtual room for this room");
                     dis.dispatch({
@@ -1231,7 +1233,7 @@ export const Commands = [
             }
 
             return success(
-                (async () => {
+                (async (): Promise => {
                     if (isPhoneNumber) {
                         const results = await LegacyCallHandler.instance.pstnLookup(userId);
                         if (!results || results.length === 0 || !results[0].userid) {
@@ -1265,7 +1267,7 @@ export const Commands = [
                     const [userId, msg] = matches.slice(1);
                     if (userId && userId.startsWith("@") && userId.includes(":")) {
                         return success(
-                            (async () => {
+                            (async (): Promise => {
                                 const cli = MatrixClientPeg.get();
                                 const roomId = await ensureDMExists(cli, userId);
                                 dis.dispatch({
diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts
index f9ce5ed2a4..4a6c113253 100644
--- a/src/SlidingSyncManager.ts
+++ b/src/SlidingSyncManager.ts
@@ -119,12 +119,10 @@ export class SlidingSyncManager {
 
     public slidingSync: SlidingSync;
     private client: MatrixClient;
-    private listIdToIndex: Record;
 
     private configureDefer: IDeferred;
 
     public constructor() {
-        this.listIdToIndex = {};
         this.configureDefer = defer();
     }
 
@@ -134,13 +132,18 @@ export class SlidingSyncManager {
 
     public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
         this.client = client;
-        this.listIdToIndex = {};
         // by default use the encrypted subscription as that gets everything, which is a safer
         // default than potentially missing member events.
-        this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
+        this.slidingSync = new SlidingSync(
+            proxyUrl,
+            new Map(),
+            ENCRYPTED_SUBSCRIPTION,
+            client,
+            SLIDING_SYNC_TIMEOUT_MS,
+        );
         this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
         // set the space list
-        this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), {
+        this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
             ranges: [[0, 20]],
             sort: ["by_name"],
             slow_get_all_rooms: true,
@@ -173,47 +176,16 @@ export class SlidingSyncManager {
         return this.slidingSync;
     }
 
-    public listIdForIndex(index: number): string | null {
-        for (const listId in this.listIdToIndex) {
-            if (this.listIdToIndex[listId] === index) {
-                return listId;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces
-     * @param listId A string which represents the list.
-     * @returns The index to use when registering lists or listening for callbacks.
-     */
-    public getOrAllocateListIndex(listId: string): number {
-        let index = this.listIdToIndex[listId];
-        if (index === undefined) {
-            // assign next highest index
-            index = -1;
-            for (const id in this.listIdToIndex) {
-                const listIndex = this.listIdToIndex[id];
-                if (listIndex > index) {
-                    index = listIndex;
-                }
-            }
-            index++;
-            this.listIdToIndex[listId] = index;
-        }
-        return index;
-    }
-
     /**
      * Ensure that this list is registered.
-     * @param listIndex The list index to register
+     * @param listKey The list key to register
      * @param updateArgs The fields to update on the list.
      * @returns The complete list request params
      */
-    public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise {
-        logger.debug("ensureListRegistered:::", listIndex, updateArgs);
+    public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise {
+        logger.debug("ensureListRegistered:::", listKey, updateArgs);
         await this.configureDefer.promise;
-        let list = this.slidingSync.getList(listIndex);
+        let list = this.slidingSync.getListParams(listKey);
         if (!list) {
             list = {
                 ranges: [[0, 20]],
@@ -252,14 +224,14 @@ export class SlidingSyncManager {
         try {
             // if we only have range changes then call a different function so we don't nuke the list from before
             if (updateArgs.ranges && Object.keys(updateArgs).length === 1) {
-                await this.slidingSync.setListRanges(listIndex, updateArgs.ranges);
+                await this.slidingSync.setListRanges(listKey, updateArgs.ranges);
             } else {
-                await this.slidingSync.setList(listIndex, list);
+                await this.slidingSync.setList(listKey, list);
             }
         } catch (err) {
             logger.debug("ensureListRegistered: update failed txn_id=", err);
         }
-        return this.slidingSync.getList(listIndex);
+        return this.slidingSync.getListParams(listKey)!;
     }
 
     public async setRoomVisible(roomId: string, visible: boolean): Promise {
@@ -302,9 +274,8 @@ export class SlidingSyncManager {
      * @param batchSize The number of rooms to return in each request.
      * @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
      */
-    public async startSpidering(batchSize: number, gapBetweenRequestsMs: number) {
+    public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise {
         await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
-        const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
         let startIndex = batchSize;
         let hasMore = true;
         let firstTime = true;
@@ -316,7 +287,7 @@ export class SlidingSyncManager {
                     [startIndex, endIndex],
                 ];
                 if (firstTime) {
-                    await this.slidingSync.setList(listIndex, {
+                    await this.slidingSync.setList(SlidingSyncManager.ListSearch, {
                         // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
                         // any changes to the list whilst spidering are caught.
                         ranges: ranges,
@@ -342,15 +313,17 @@ export class SlidingSyncManager {
                         },
                     });
                 } else {
-                    await this.slidingSync.setListRanges(listIndex, ranges);
+                    await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges);
                 }
-                // gradually request more over time
-                await sleep(gapBetweenRequestsMs);
             } catch (err) {
                 // do nothing, as we reject only when we get interrupted but that's fine as the next
                 // request will include our data
+            } finally {
+                // gradually request more over time, even on errors.
+                await sleep(gapBetweenRequestsMs);
             }
-            hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount;
+            const listData = this.slidingSync.getListData(SlidingSyncManager.ListSearch)!;
+            hasMore = endIndex + 1 < listData.joinedCount;
             startIndex += batchSize;
             firstTime = false;
         }
diff --git a/src/Terms.ts b/src/Terms.ts
index 101a778a94..bb18a18cf7 100644
--- a/src/Terms.ts
+++ b/src/Terms.ts
@@ -75,7 +75,7 @@ export type TermsInteractionCallback = (
 export async function startTermsFlow(
     services: Service[],
     interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
-) {
+): Promise {
     const termsPromises = services.map((s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl));
 
     /*
@@ -176,7 +176,7 @@ export async function startTermsFlow(
             urlsForService,
         );
     });
-    return Promise.all(agreePromises);
+    await Promise.all(agreePromises);
 }
 
 export async function dialogTermsInteractionCallback(
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 8be6cd4a40..7f874f8a89 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -20,7 +20,8 @@ import { logger } from "matrix-js-sdk/src/logger";
 import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
 import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
 import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
-import { M_POLL_START, M_POLL_END, PollStartEvent } from "matrix-events-sdk";
+import { M_POLL_START, M_POLL_END } from "matrix-js-sdk/src/@types/polls";
+import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
 
 import { _t } from "./languageHandler";
 import * as Roles from "./Roles";
@@ -228,7 +229,7 @@ function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
     return () => _t("%(senderDisplayName)s upgraded this room.", { senderDisplayName });
 }
 
-const onViewJoinRuleSettingsClick = () => {
+const onViewJoinRuleSettingsClick = (): void => {
     defaultDispatcher.dispatch({
         action: "open_room_settings",
         initial_tab_id: ROOM_SECURITY_TAB,
diff --git a/src/UserActivity.ts b/src/UserActivity.ts
index 0e163564e0..9217aca3c0 100644
--- a/src/UserActivity.ts
+++ b/src/UserActivity.ts
@@ -50,7 +50,7 @@ export default class UserActivity {
         this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS);
     }
 
-    public static sharedInstance() {
+    public static sharedInstance(): UserActivity {
         if (window.mxUserActivity === undefined) {
             window.mxUserActivity = new UserActivity(window, document);
         }
@@ -66,7 +66,7 @@ export default class UserActivity {
      * later on when the user does become active.
      * @param {Timer} timer the timer to use
      */
-    public timeWhileActiveNow(timer: Timer) {
+    public timeWhileActiveNow(timer: Timer): void {
         this.timeWhile(timer, this.attachedActiveNowTimers);
         if (this.userActiveNow()) {
             timer.start();
@@ -82,14 +82,14 @@ export default class UserActivity {
      * later on when the user does become active.
      * @param {Timer} timer the timer to use
      */
-    public timeWhileActiveRecently(timer: Timer) {
+    public timeWhileActiveRecently(timer: Timer): void {
         this.timeWhile(timer, this.attachedActiveRecentlyTimers);
         if (this.userActiveRecently()) {
             timer.start();
         }
     }
 
-    private timeWhile(timer: Timer, attachedTimers: Timer[]) {
+    private timeWhile(timer: Timer, attachedTimers: Timer[]): void {
         // important this happens first
         const index = attachedTimers.indexOf(timer);
         if (index === -1) {
@@ -113,7 +113,7 @@ export default class UserActivity {
     /**
      * Start listening to user activity
      */
-    public start() {
+    public start(): void {
         this.document.addEventListener("mousedown", this.onUserActivity);
         this.document.addEventListener("mousemove", this.onUserActivity);
         this.document.addEventListener("keydown", this.onUserActivity);
@@ -133,7 +133,7 @@ export default class UserActivity {
     /**
      * Stop tracking user activity
      */
-    public stop() {
+    public stop(): void {
         this.document.removeEventListener("mousedown", this.onUserActivity);
         this.document.removeEventListener("mousemove", this.onUserActivity);
         this.document.removeEventListener("keydown", this.onUserActivity);
@@ -152,7 +152,7 @@ export default class UserActivity {
      * user's attention at any given moment.
      * @returns {boolean} true if user is currently 'active'
      */
-    public userActiveNow() {
+    public userActiveNow(): boolean {
         return this.activeNowTimeout.isRunning();
     }
 
@@ -164,11 +164,11 @@ export default class UserActivity {
      * (or they may have gone to make tea and left the window focused).
      * @returns {boolean} true if user has been active recently
      */
-    public userActiveRecently() {
+    public userActiveRecently(): boolean {
         return this.activeRecentlyTimeout.isRunning();
     }
 
-    private onPageVisibilityChanged = (e) => {
+    private onPageVisibilityChanged = (e): void => {
         if (this.document.visibilityState === "hidden") {
             this.activeNowTimeout.abort();
             this.activeRecentlyTimeout.abort();
@@ -177,12 +177,12 @@ export default class UserActivity {
         }
     };
 
-    private onWindowBlurred = () => {
+    private onWindowBlurred = (): void => {
         this.activeNowTimeout.abort();
         this.activeRecentlyTimeout.abort();
     };
 
-    private onUserActivity = (event: MouseEvent) => {
+    private onUserActivity = (event: MouseEvent): void => {
         // ignore anything if the window isn't focused
         if (!this.document.hasFocus()) return;
 
@@ -214,7 +214,7 @@ export default class UserActivity {
         }
     };
 
-    private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) {
+    private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer): Promise {
         attachedTimers.forEach((t) => t.start());
         try {
             await timeout.finished();
diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx
index beb6eee004..e90aed87a9 100644
--- a/src/accessibility/RovingTabIndex.tsx
+++ b/src/accessibility/RovingTabIndex.tsx
@@ -87,7 +87,7 @@ interface IAction {
     };
 }
 
-export const reducer = (state: IState, action: IAction) => {
+export const reducer: Reducer = (state: IState, action: IAction) => {
     switch (action.type) {
         case Type.Register: {
             if (!state.activeRef) {
diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx
index 22ef018241..3b5fbb0943 100644
--- a/src/accessibility/Toolbar.tsx
+++ b/src/accessibility/Toolbar.tsx
@@ -26,7 +26,7 @@ interface IProps extends Omit, "onKeyDown"> {}
 // https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
 // All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
 const Toolbar: React.FC = ({ children, ...props }) => {
-    const onKeyDown = (ev: React.KeyboardEvent) => {
+    const onKeyDown = (ev: React.KeyboardEvent): void => {
         const target = ev.target as HTMLElement;
         // Don't interfere with input default keydown behaviour
         if (target.tagName === "INPUT") return;
diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
index 867d3aeaab..ee3a0e4d36 100644
--- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
+++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
@@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps {
 export const StyledMenuItemCheckbox: React.FC = ({ children, label, onChange, onClose, ...props }) => {
     const [onFocus, isActive, ref] = useRovingTabIndex();
 
-    const onKeyDown = (e: React.KeyboardEvent) => {
+    const onKeyDown = (e: React.KeyboardEvent): void => {
         let handled = true;
         const action = getKeyBindingsManager().getAccessibilityAction(e);
 
@@ -55,7 +55,7 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh
             e.preventDefault();
         }
     };
-    const onKeyUp = (e: React.KeyboardEvent) => {
+    const onKeyUp = (e: React.KeyboardEvent): void => {
         const action = getKeyBindingsManager().getAccessibilityAction(e);
         switch (action) {
             case KeyBindingAction.Space:
diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx
index 6bbf5a1106..2fe8738434 100644
--- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx
+++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx
@@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps {
 export const StyledMenuItemRadio: React.FC = ({ children, label, onChange, onClose, ...props }) => {
     const [onFocus, isActive, ref] = useRovingTabIndex();
 
-    const onKeyDown = (e: React.KeyboardEvent) => {
+    const onKeyDown = (e: React.KeyboardEvent): void => {
         let handled = true;
         const action = getKeyBindingsManager().getAccessibilityAction(e);
 
@@ -55,7 +55,7 @@ export const StyledMenuItemRadio: React.FC = ({ children, label, onChang
             e.preventDefault();
         }
     };
-    const onKeyUp = (e: React.KeyboardEvent) => {
+    const onKeyUp = (e: React.KeyboardEvent): void => {
         const action = getKeyBindingsManager().getAccessibilityAction(e);
         switch (action) {
             case KeyBindingAction.Enter:
diff --git a/src/actions/actionCreators.ts b/src/actions/actionCreators.ts
index 0341f03cac..b6eb263fb9 100644
--- a/src/actions/actionCreators.ts
+++ b/src/actions/actionCreators.ts
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { AsyncActionPayload } from "../dispatcher/payloads";
+import { AsyncActionFn, AsyncActionPayload } from "../dispatcher/payloads";
 
 /**
  * Create an action thunk that will dispatch actions indicating the current
@@ -45,7 +45,7 @@ import { AsyncActionPayload } from "../dispatcher/payloads";
  *                     `fn`.
  */
 export function asyncAction(id: string, fn: () => Promise, pendingFn: () => any | null): AsyncActionPayload {
-    const helper = (dispatch) => {
+    const helper: AsyncActionFn = (dispatch) => {
         dispatch({
             action: id + ".pending",
             request: typeof pendingFn === "function" ? pendingFn() : undefined,
diff --git a/src/actions/handlers/viewUserDeviceSettings.ts b/src/actions/handlers/viewUserDeviceSettings.ts
index e1dc7b3f26..4525ba104d 100644
--- a/src/actions/handlers/viewUserDeviceSettings.ts
+++ b/src/actions/handlers/viewUserDeviceSettings.ts
@@ -22,7 +22,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
  * Redirect to the correct device manager section
  * Based on the labs setting
  */
-export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean) => {
+export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => {
     defaultDispatcher.dispatch({
         action: Action.ViewUserSettings,
         initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security,
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
index 98bbb55069..63a132077f 100644
--- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
+++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
@@ -56,7 +56,7 @@ export default class ManageEventIndexDialog extends React.Component {
+    public updateCurrentRoom = async (room): Promise => {
         const eventIndex = EventIndexPeg.get();
         let stats;
 
@@ -131,17 +131,17 @@ export default class ManageEventIndexDialog extends React.Component {
+    private onDisable = async (): Promise => {
         const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
         Modal.createDialog(DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true);
     };
 
-    private onCrawlerSleepTimeChange = (e) => {
+    private onCrawlerSleepTimeChange = (e): void => {
         this.setState({ crawlerSleepTime: e.target.value });
         SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
     };
 
-    public render() {
+    public render(): JSX.Element {
         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 f9df9b09a4..a75b41f602 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx
@@ -125,7 +125,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
+                await accessSecretStorage(async (): Promise => {
                     info = await MatrixClientPeg.get().prepareKeyBackupVersion(null /* random key */, {
                         secureSecretStorage: true,
                     });
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
index 0c976ec599..b595a60a2e 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
@@ -350,7 +350,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey,
                     keyBackupInfo: this.state.backupInfo,
                     setupNewKeyBackup: !this.state.backupInfo,
-                    getKeyBackupPassphrase: async () => {
+                    getKeyBackupPassphrase: async (): Promise => {
                         // We may already have the backup key if we earlier went
                         // through the restore backup path, so pass it along
                         // rather than prompting again.
@@ -383,7 +383,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent => {
         // It's possible we'll need the backup key later on for bootstrapping,
         // so let's stash it here, rather than prompting for it twice.
-        const keyCallback = (k) => (this.backupKey = k);
+        const keyCallback = (k: Uint8Array): void => {
+            this.backupKey = k;
+        };
 
         const { finished } = Modal.createDialog(
             RestoreKeyBackupDialog,
@@ -420,7 +422,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
+    private onPassPhraseNextClick = async (e: React.FormEvent): Promise => {
         e.preventDefault();
         if (!this.passphraseField.current) return; // unmounting
 
@@ -434,7 +436,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
+    private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise => {
         e.preventDefault();
 
         if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx
index 1998c7c7ed..c8a561e7da 100644
--- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx
+++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx
@@ -121,7 +121,7 @@ export default class ExportE2eKeysDialog extends React.Component
         return false;
     };
 
-    private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase) => {
+    private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase): void => {
         this.setState({
             [phrase]: ev.target.value,
         } as Pick);
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx
index b546ffc91a..079271b021 100644
--- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx
+++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx
@@ -91,7 +91,7 @@ export default class ImportE2eKeysDialog extends React.Component
         return false;
     };
 
-    private startImport(file: File, passphrase: string) {
+    private startImport(file: File, passphrase: string): Promise {
         this.setState({
             errStr: null,
             phase: Phase.Importing,
diff --git a/src/audio/ManagedPlayback.ts b/src/audio/ManagedPlayback.ts
index 5db07671f1..c33d032b68 100644
--- a/src/audio/ManagedPlayback.ts
+++ b/src/audio/ManagedPlayback.ts
@@ -30,7 +30,7 @@ export class ManagedPlayback extends Playback {
         return super.play();
     }
 
-    public destroy() {
+    public destroy(): void {
         this.manager.destroyPlaybackInstance(this);
         super.destroy();
     }
diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts
index d5971ad73c..e1ab1a1c59 100644
--- a/src/audio/Playback.ts
+++ b/src/audio/Playback.ts
@@ -145,7 +145,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
         return true; // we don't ever care if the event had listeners, so just return "yes"
     }
 
-    public destroy() {
+    public destroy(): void {
         // Dev note: It's critical that we call stop() during cleanup to ensure that downstream callers
         // are aware of the final clock position before the user triggered an unload.
         // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here
@@ -159,7 +159,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
         }
     }
 
-    public async prepare() {
+    public async prepare(): Promise {
         // don't attempt to decode the media again
         // AudioContext.decodeAudioData detaches the array buffer `this.buf`
         // meaning it cannot be re-read
@@ -190,7 +190,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
                 this.context.decodeAudioData(
                     this.buf,
                     (b) => resolve(b),
-                    async (e) => {
+                    async (e): Promise => {
                         try {
                             // This error handler is largely for Safari as well, which doesn't support Opus/Ogg
                             // very well.
@@ -232,12 +232,12 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
         this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
     }
 
-    private onPlaybackEnd = async () => {
+    private onPlaybackEnd = async (): Promise => {
         await this.context.suspend();
         this.emit(PlaybackState.Stopped);
     };
 
-    public async play() {
+    public async play(): Promise {
         // We can't restart a buffer source, so we need to create a new one if we hit the end
         if (this.state === PlaybackState.Stopped) {
             this.disconnectSource();
@@ -256,13 +256,13 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
         this.emit(PlaybackState.Playing);
     }
 
-    private disconnectSource() {
+    private disconnectSource(): void {
         if (this.element) return; // leave connected, we can (and must) re-use it
         this.source?.disconnect();
         this.source?.removeEventListener("ended", this.onPlaybackEnd);
     }
 
-    private makeNewSourceBuffer() {
+    private makeNewSourceBuffer(): void {
         if (this.element && this.source) return; // leave connected, we can (and must) re-use it
 
         if (this.element) {
@@ -276,22 +276,22 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
         this.source.connect(this.context.destination);
     }
 
-    public async pause() {
+    public async pause(): Promise {
         await this.context.suspend();
         this.emit(PlaybackState.Paused);
     }
 
-    public async stop() {
+    public async stop(): Promise {
         await this.onPlaybackEnd();
         this.clock.flagStop();
     }
 
-    public async toggle() {
+    public async toggle(): Promise {
         if (this.isPlaying) await this.pause();
         else await this.play();
     }
 
-    public async skipTo(timeSeconds: number) {
+    public async skipTo(timeSeconds: number): Promise {
         // Dev note: this function talks a lot about clock desyncs. There is a clock running
         // independently to the audio context and buffer so that accurate human-perceptible
         // time can be exposed. The PlaybackClock class has more information, but the short
diff --git a/src/audio/PlaybackClock.ts b/src/audio/PlaybackClock.ts
index 4f2d8f14aa..1a6d1a2e5d 100644
--- a/src/audio/PlaybackClock.ts
+++ b/src/audio/PlaybackClock.ts
@@ -89,7 +89,7 @@ export class PlaybackClock implements IDestroyable {
         return this.observable;
     }
 
-    private checkTime = (force = false) => {
+    private checkTime = (force = false): void => {
         const now = this.timeSeconds; // calculated dynamically
         if (this.lastCheck !== now || force) {
             this.observable.update([now, this.durationSeconds]);
@@ -102,7 +102,7 @@ export class PlaybackClock implements IDestroyable {
      * The placeholders will be overridden once known.
      * @param {MatrixEvent} event The event to use for placeholders.
      */
-    public populatePlaceholdersFrom(event: MatrixEvent) {
+    public populatePlaceholdersFrom(event: MatrixEvent): void {
         const durationMs = Number(event.getContent()["info"]?.["duration"]);
         if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
     }
@@ -112,11 +112,11 @@ export class PlaybackClock implements IDestroyable {
      * This is to ensure the clock isn't skewed into thinking it is ~0.5s into
      * a clip when the duration is set.
      */
-    public flagLoadTime() {
+    public flagLoadTime(): void {
         this.clipStart = this.context.currentTime;
     }
 
-    public flagStart() {
+    public flagStart(): void {
         if (this.stopped) {
             this.clipStart = this.context.currentTime;
             this.stopped = false;
@@ -128,7 +128,7 @@ export class PlaybackClock implements IDestroyable {
         }
     }
 
-    public flagStop() {
+    public flagStop(): void {
         this.stopped = true;
 
         // Reset the clock time now so that the update going out will trigger components
@@ -136,13 +136,13 @@ export class PlaybackClock implements IDestroyable {
         this.clipStart = this.context.currentTime;
     }
 
-    public syncTo(contextTime: number, clipTime: number) {
+    public syncTo(contextTime: number, clipTime: number): void {
         this.clipStart = contextTime - clipTime;
         this.stopped = false; // count as a mid-stream pause (if we were stopped)
         this.checkTime(true);
     }
 
-    public destroy() {
+    public destroy(): void {
         this.observable.close();
         if (this.timerId) clearInterval(this.timerId);
     }
diff --git a/src/audio/PlaybackManager.ts b/src/audio/PlaybackManager.ts
index 01a597d58f..0cc52e7f0e 100644
--- a/src/audio/PlaybackManager.ts
+++ b/src/audio/PlaybackManager.ts
@@ -38,13 +38,13 @@ export class PlaybackManager {
      * instances are paused.
      * @param playback Optional. The playback to leave untouched.
      */
-    public pauseAllExcept(playback?: Playback) {
+    public pauseAllExcept(playback?: Playback): void {
         this.instances
             .filter((p) => p !== playback && p.currentState === PlaybackState.Playing)
             .forEach((p) => p.pause());
     }
 
-    public destroyPlaybackInstance(playback: ManagedPlayback) {
+    public destroyPlaybackInstance(playback: ManagedPlayback): void {
         this.instances = this.instances.filter((p) => p !== playback);
     }
 
diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts
index 1ea8a85fa6..7c521b9ca6 100644
--- a/src/audio/PlaybackQueue.ts
+++ b/src/audio/PlaybackQueue.ts
@@ -75,28 +75,28 @@ export class PlaybackQueue {
         return queue;
     }
 
-    private persistClocks() {
+    private persistClocks(): void {
         localStorage.setItem(
             `mx_voice_message_clocks_${this.room.roomId}`,
             JSON.stringify(Array.from(this.clockStates.entries())),
         );
     }
 
-    private loadClocks() {
+    private loadClocks(): void {
         const val = localStorage.getItem(`mx_voice_message_clocks_${this.room.roomId}`);
         if (!!val) {
             this.clockStates = new Map(JSON.parse(val));
         }
     }
 
-    public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback) {
+    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);
         playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state));
         playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock));
     }
 
-    private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState) {
+    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) {
@@ -210,7 +210,7 @@ export class PlaybackQueue {
         }
     }
 
-    private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]) {
+    private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]): void {
         if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values
 
         if (playback.currentState !== PlaybackState.Stopped) {
diff --git a/src/audio/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts
index 5079ec58a9..0c0cc56cd6 100644
--- a/src/audio/RecorderWorklet.ts
+++ b/src/audio/RecorderWorklet.ts
@@ -43,7 +43,7 @@ class MxVoiceWorklet extends AudioWorkletProcessor {
     private nextAmplitudeSecond = 0;
     private amplitudeIndex = 0;
 
-    public process(inputs, outputs, parameters) {
+    public process(inputs, outputs, parameters): 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/VoiceMessageRecording.ts b/src/audio/VoiceMessageRecording.ts
index f27fc36135..7d5c491261 100644
--- a/src/audio/VoiceMessageRecording.ts
+++ b/src/audio/VoiceMessageRecording.ts
@@ -141,7 +141,7 @@ export class VoiceMessageRecording implements IDestroyable {
         this.voiceRecording.destroy();
     }
 
-    private onDataAvailable = (data: ArrayBuffer) => {
+    private onDataAvailable = (data: ArrayBuffer): void => {
         const buf = new Uint8Array(data);
         this.buffer = concat(this.buffer, buf);
     };
@@ -153,6 +153,6 @@ export class VoiceMessageRecording implements IDestroyable {
     }
 }
 
-export const createVoiceMessageRecording = (matrixClient: MatrixClient) => {
+export const createVoiceMessageRecording = (matrixClient: MatrixClient): VoiceMessageRecording => {
     return new VoiceMessageRecording(matrixClient, new VoiceRecording());
 };
diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts
index 20434d998d..32fcb5a97a 100644
--- a/src/audio/VoiceRecording.ts
+++ b/src/audio/VoiceRecording.ts
@@ -110,7 +110,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
         return !MediaDeviceHandler.getAudioNoiseSuppression();
     }
 
-    private async makeRecorder() {
+    private async makeRecorder(): Promise {
         try {
             this.recorderStream = await navigator.mediaDevices.getUserMedia({
                 audio: {
@@ -212,14 +212,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
         return !!Recorder.isRecordingSupported();
     }
 
-    private onAudioProcess = (ev: AudioProcessingEvent) => {
+    private onAudioProcess = (ev: AudioProcessingEvent): void => {
         this.processAudioUpdate(ev.playbackTime);
 
         // We skip the functionality of the worklet regarding waveform calculations: we
         // should get that information pretty quick during the playback info.
     };
 
-    private processAudioUpdate = (timeSeconds: number) => {
+    private processAudioUpdate = (timeSeconds: number): void => {
         if (!this.recording) return;
 
         this.observable.update({
@@ -260,7 +260,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
     /**
      * {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds}
      */
-    public get recorderSeconds() {
+    public get recorderSeconds(): number {
         return this.recorder.encodedSamplePosition / 48000;
     }
 
@@ -279,7 +279,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
     }
 
     public async stop(): Promise {
-        return Singleflight.for(this, "stop").do(async () => {
+        return Singleflight.for(this, "stop").do(async (): Promise => {
             if (!this.recording) {
                 throw new Error("No recording to stop");
             }
@@ -307,7 +307,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
         });
     }
 
-    public destroy() {
+    public destroy(): void {
         // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here
         this.stop();
         this.removeAllListeners();
diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx
index e76c4f1903..546e052f58 100644
--- a/src/autocomplete/AutocompleteProvider.tsx
+++ b/src/autocomplete/AutocompleteProvider.tsx
@@ -22,7 +22,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
 import type { ICompletion, ISelectionRange } from "./Autocompleter";
 
 export interface ICommand {
-    command: string | null;
+    command: RegExpExecArray | null;
     range: {
         start: number;
         end: number;
@@ -59,7 +59,7 @@ export default abstract class AutocompleteProvider {
         }
     }
 
-    public destroy() {
+    public destroy(): void {
         // stub
     }
 
@@ -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) {
+    public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand {
         let commandRegex = this.commandRegex;
 
         if (force && this.shouldForceComplete()) {
@@ -83,7 +83,7 @@ export default abstract class AutocompleteProvider {
 
         commandRegex.lastIndex = 0;
 
-        let match;
+        let match: RegExpExecArray;
         while ((match = commandRegex.exec(query)) !== null) {
             const start = match.index;
             const end = start + match[0].length;
diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts
index 67a40db158..b609f265f1 100644
--- a/src/autocomplete/Autocompleter.ts
+++ b/src/autocomplete/Autocompleter.ts
@@ -69,7 +69,7 @@ export default class Autocompleter {
         });
     }
 
-    public destroy() {
+    public destroy(): void {
         this.providers.forEach((p) => {
             p.destroy();
         });
@@ -88,7 +88,7 @@ export default class Autocompleter {
         */
         // list of results from each provider, each being a list of completions or null if it times out
         const completionsList: ICompletion[][] = await Promise.all(
-            this.providers.map(async (provider) => {
+            this.providers.map(async (provider): Promise => {
                 return timeout(
                     provider.getCompletions(query, selection, force, limit),
                     null,
diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx
index 68850a9a15..caafe98f08 100644
--- a/src/autocomplete/CommandProvider.tsx
+++ b/src/autocomplete/CommandProvider.tsx
@@ -100,7 +100,7 @@ export default class CommandProvider extends AutocompleteProvider {
             });
     }
 
-    public getName() {
+    public getName(): string {
         return "*️⃣ " + _t("Commands");
     }
 
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 821edb4a3e..cc25068db8 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -55,7 +55,7 @@ const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
     _orderBy: index,
 }));
 
-function score(query, space) {
+function score(query: string, space: string): number {
     const index = space.indexOf(query);
     if (index === -1) {
         return Infinity;
@@ -154,7 +154,7 @@ export default class EmojiProvider extends AutocompleteProvider {
         return [];
     }
 
-    public getName() {
+    public getName(): string {
         return "😃 " + _t("Emoji");
     }
 
diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx
index 28f01c178a..5efe0e86f6 100644
--- a/src/autocomplete/NotifProvider.tsx
+++ b/src/autocomplete/NotifProvider.tsx
@@ -65,7 +65,7 @@ export default class NotifProvider extends AutocompleteProvider {
         return [];
     }
 
-    public getName() {
+    public getName(): string {
         return "❗️ " + _t("Room Notification");
     }
 
diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts
index 23545075bc..1f7b5a5a7f 100644
--- a/src/autocomplete/QueryMatcher.ts
+++ b/src/autocomplete/QueryMatcher.ts
@@ -61,7 +61,7 @@ export default class QueryMatcher {
         }
     }
 
-    public setObjects(objects: T[]) {
+    public setObjects(objects: T[]): void {
         this._items = new Map();
 
         for (const object of objects) {
diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx
index a225676898..bf102d55bc 100644
--- a/src/autocomplete/RoomProvider.tsx
+++ b/src/autocomplete/RoomProvider.tsx
@@ -37,7 +37,15 @@ function canonicalScore(displayedAlias: string, room: Room): number {
     return displayedAlias === room.getCanonicalAlias() ? 0 : 1;
 }
 
-function matcherObject(room: Room, displayedAlias: string, matchName = "") {
+function matcherObject(
+    room: Room,
+    displayedAlias: string,
+    matchName = "",
+): {
+    room: Room;
+    matchName: string;
+    displayedAlias: string;
+} {
     return {
         room,
         matchName,
@@ -46,7 +54,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
 }
 
 export default class RoomProvider extends AutocompleteProvider {
-    protected matcher: QueryMatcher;
+    protected matcher: QueryMatcher>;
 
     public constructor(room: Room, renderingType?: TimelineRenderingType) {
         super({ commandRegex: ROOM_REGEX, renderingType });
@@ -55,7 +63,7 @@ export default class RoomProvider extends AutocompleteProvider {
         });
     }
 
-    protected getRooms() {
+    protected getRooms(): Room[] {
         const cli = MatrixClientPeg.get();
 
         // filter out spaces here as they get their own autocomplete provider
@@ -68,7 +76,6 @@ export default class RoomProvider extends AutocompleteProvider {
         force = false,
         limit = -1,
     ): Promise {
-        let completions = [];
         const { command, range } = this.getCurrentCommand(query, selection, force);
         if (command) {
             // the only reason we need to do this is because Fuse only matches on properties
@@ -96,15 +103,15 @@ export default class RoomProvider extends AutocompleteProvider {
 
             this.matcher.setObjects(matcherObjects);
             const matchedString = command[0];
-            completions = this.matcher.match(matchedString, limit);
+            let completions = this.matcher.match(matchedString, limit);
             completions = sortBy(completions, [
                 (c) => canonicalScore(c.displayedAlias, c.room),
                 (c) => c.displayedAlias.length,
             ]);
             completions = uniqBy(completions, (match) => match.room);
-            completions = completions
-                .map((room) => {
-                    return {
+            return completions
+                .map(
+                    (room): ICompletion => ({
                         completion: room.displayedAlias,
                         completionId: room.room.roomId,
                         type: "room",
@@ -116,14 +123,14 @@ export default class RoomProvider extends AutocompleteProvider {
                             
                         ),
                         range,
-                    };
-                })
+                    }),
+                )
                 .filter((completion) => !!completion.completion && completion.completion.length > 0);
         }
-        return completions;
+        return [];
     }
 
-    public getName() {
+    public getName(): string {
         return _t("Rooms");
     }
 
diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx
index 14f9e2c375..bef3b57354 100644
--- a/src/autocomplete/SpaceProvider.tsx
+++ b/src/autocomplete/SpaceProvider.tsx
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import { Room } from "matrix-js-sdk/src/models/room";
 import React from "react";
 
 import { _t } from "../languageHandler";
@@ -21,13 +22,13 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
 import RoomProvider from "./RoomProvider";
 
 export default class SpaceProvider extends RoomProvider {
-    protected getRooms() {
+    protected getRooms(): Room[] {
         return MatrixClientPeg.get()
             .getVisibleRooms()
             .filter((r) => r.isSpaceRoom());
     }
 
-    public getName() {
+    public getName(): string {
         return _t("Spaces");
     }
 
diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index 551a7bc141..65de4b1bb4 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -64,7 +64,7 @@ export default class UserProvider extends AutocompleteProvider {
         MatrixClientPeg.get().on(RoomStateEvent.Update, this.onRoomStateUpdate);
     }
 
-    public destroy() {
+    public destroy(): void {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.onRoomTimeline);
             MatrixClientPeg.get().removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
@@ -77,7 +77,7 @@ export default class UserProvider extends AutocompleteProvider {
         toStartOfTimeline: boolean,
         removed: boolean,
         data: IRoomTimelineData,
-    ) => {
+    ): void => {
         if (!room) return; // notification timeline, we'll get this event again with a room specific timeline
         if (removed) return;
         if (room.roomId !== this.room.roomId) return;
@@ -93,7 +93,7 @@ export default class UserProvider extends AutocompleteProvider {
         this.onUserSpoke(ev.sender);
     };
 
-    private onRoomStateUpdate = (state: RoomState) => {
+    private onRoomStateUpdate = (state: RoomState): void => {
         // ignore updates in other rooms
         if (state.roomId !== this.room.roomId) return;
 
@@ -150,7 +150,7 @@ export default class UserProvider extends AutocompleteProvider {
         return _t("Users");
     }
 
-    private makeUsers() {
+    private makeUsers(): void {
         const events = this.room.getLiveTimeline().getEvents();
         const lastSpoken = {};
 
@@ -167,7 +167,7 @@ export default class UserProvider extends AutocompleteProvider {
         this.matcher.setObjects(this.users);
     }
 
-    public onUserSpoke(user: RoomMember) {
+    public onUserSpoke(user: RoomMember): 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 719be59f6c..90fda3fe21 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -22,7 +22,7 @@ type DynamicHtmlElementProps =
     JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">;
 type DynamicElementProps = Partial>;
 
-export type IProps = DynamicHtmlElementProps & {
+export type IProps = Omit, "onScroll"> & {
     element?: T;
     className?: string;
     onScroll?: (event: Event) => void;
@@ -39,7 +39,7 @@ export default class AutoHideScrollbar ex
 
     public readonly containerRef: React.RefObject = React.createRef();
 
-    public componentDidMount() {
+    public componentDidMount(): void {
         if (this.containerRef.current && this.props.onScroll) {
             // Using the passive option to not block the main thread
             // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
@@ -49,13 +49,13 @@ export default class AutoHideScrollbar ex
         this.props.wrappedRef?.(this.containerRef.current);
     }
 
-    public componentWillUnmount() {
+    public componentWillUnmount(): void {
         if (this.containerRef.current && this.props.onScroll) {
             this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
         }
     }
 
-    public render() {
+    public render(): JSX.Element {
         // 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/AutocompleteInput.tsx b/src/components/structures/AutocompleteInput.tsx
index 2d09a247c6..9f16211022 100644
--- a/src/components/structures/AutocompleteInput.tsx
+++ b/src/components/structures/AutocompleteInput.tsx
@@ -52,11 +52,11 @@ export const AutocompleteInput: React.FC = ({
     const editorContainerRef = useRef(null);
     const editorRef = useRef(null);
 
-    const focusEditor = () => {
+    const focusEditor = (): void => {
         editorRef?.current?.focus();
     };
 
-    const onQueryChange = async (e: ChangeEvent) => {
+    const onQueryChange = async (e: ChangeEvent): Promise => {
         const value = e.target.value.trim();
         setQuery(value);
 
@@ -74,11 +74,11 @@ export const AutocompleteInput: React.FC = ({
         setSuggestions(matches);
     };
 
-    const onClickInputArea = () => {
+    const onClickInputArea = (): void => {
         focusEditor();
     };
 
-    const onKeyDown = (e: KeyboardEvent) => {
+    const onKeyDown = (e: KeyboardEvent): void => {
         const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
 
         // when the field is empty and the user hits backspace remove the right-most target
@@ -87,7 +87,7 @@ export const AutocompleteInput: React.FC = ({
         }
     };
 
-    const toggleSelection = (completion: ICompletion) => {
+    const toggleSelection = (completion: ICompletion): void => {
         const newSelection = [...selection];
         const index = selection.findIndex((selection) => selection.completionId === completion.completionId);
 
@@ -101,7 +101,7 @@ export const AutocompleteInput: React.FC = ({
         focusEditor();
     };
 
-    const removeSelection = (completion: ICompletion) => {
+    const removeSelection = (completion: ICompletion): void => {
         const newSelection = [...selection];
         const index = selection.findIndex((selection) => selection.completionId === completion.completionId);
 
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 6dbdc4a7eb..978dd07be9 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -64,7 +64,7 @@ export enum ChevronFace {
     None = "none",
 }
 
-export interface IProps extends IPosition {
+export interface MenuProps extends IPosition {
     menuWidth?: number;
     menuHeight?: number;
 
@@ -77,7 +77,9 @@ export interface IProps extends IPosition {
     menuPaddingRight?: number;
 
     zIndex?: number;
+}
 
+export interface IProps extends MenuProps {
     // If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
     hasBackground?: boolean;
     // whether this context menu should be focus managed. If false it must handle itself
@@ -128,21 +130,21 @@ export default class ContextMenu extends React.PureComponent {
         this.initialFocus = document.activeElement as HTMLElement;
     }
 
-    public componentDidMount() {
+    public componentDidMount(): void {
         Modal.on(ModalManagerEvent.Opened, this.onModalOpen);
     }
 
-    public componentWillUnmount() {
+    public componentWillUnmount(): void {
         Modal.off(ModalManagerEvent.Opened, this.onModalOpen);
         // return focus to the thing which had it before us
         this.initialFocus.focus();
     }
 
-    private onModalOpen = () => {
+    private onModalOpen = (): void => {
         this.props.onFinished?.();
     };
 
-    private collectContextMenuRect = (element: HTMLDivElement) => {
+    private collectContextMenuRect = (element: HTMLDivElement): void => {
         // We don't need to clean up when unmounting, so ignore
         if (!element) return;
 
@@ -159,7 +161,7 @@ export default class ContextMenu extends React.PureComponent {
         });
     };
 
-    private onContextMenu = (e) => {
+    private onContextMenu = (e: React.MouseEvent): void => {
         if (this.props.onFinished) {
             this.props.onFinished();
 
@@ -184,20 +186,20 @@ export default class ContextMenu extends React.PureComponent {
         }
     };
 
-    private onContextMenuPreventBubbling = (e) => {
+    private onContextMenuPreventBubbling = (e: React.MouseEvent): void => {
         // stop propagation so that any context menu handlers don't leak out of this context menu
         // but do not inhibit the default browser menu
         e.stopPropagation();
     };
 
     // Prevent clicks on the background from going through to the component which opened the menu.
-    private onFinished = (ev: React.MouseEvent) => {
+    private onFinished = (ev: React.MouseEvent): void => {
         ev.stopPropagation();
         ev.preventDefault();
         this.props.onFinished?.();
     };
 
-    private onClick = (ev: React.MouseEvent) => {
+    private onClick = (ev: React.MouseEvent): void => {
         // Don't allow clicks to escape the context menu wrapper
         ev.stopPropagation();
 
@@ -208,7 +210,7 @@ export default class ContextMenu extends React.PureComponent {
 
     // We now only handle closing the ContextMenu in this keyDown handler.
     // All of the item/option navigation is delegated to RovingTabIndex.
-    private onKeyDown = (ev: React.KeyboardEvent) => {
+    private onKeyDown = (ev: React.KeyboardEvent): void => {
         ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked
 
         const action = getKeyBindingsManager().getAccessibilityAction(ev);
@@ -243,7 +245,7 @@ export default class ContextMenu extends React.PureComponent {
         }
     };
 
-    protected renderMenu(hasBackground = this.props.hasBackground) {
+    protected renderMenu(hasBackground = this.props.hasBackground): JSX.Element {
         const position: Partial> = {};
         const {
             top,
@@ -501,17 +503,13 @@ export const toLeftOrRightOf = (elementRect: DOMRect, chevronOffset = 12): ToRig
     return toRightOf(elementRect, chevronOffset);
 };
 
-export type AboveLeftOf = IPosition & {
-    chevronFace: ChevronFace;
-};
-
 // Placement method for  to position context menu right-aligned and flowing to the left of elementRect,
 // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
 export const aboveLeftOf = (
     elementRect: Pick,
     chevronFace = ChevronFace.None,
     vPadding = 0,
-): AboveLeftOf => {
+): MenuProps => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonRight = elementRect.right + window.scrollX;
@@ -535,7 +533,7 @@ export const aboveRightOf = (
     elementRect: Pick,
     chevronFace = ChevronFace.None,
     vPadding = 0,
-): AboveLeftOf => {
+): MenuProps => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonLeft = elementRect.left + window.scrollX;
@@ -555,11 +553,11 @@ export const aboveRightOf = (
 
 // Placement method for  to position context menu right-aligned and flowing to the left of elementRect
 // and always above elementRect
-export const alwaysAboveLeftOf = (
+export const alwaysMenuProps = (
     elementRect: Pick,
     chevronFace = ChevronFace.None,
     vPadding = 0,
-) => {
+): IPosition & { chevronFace: ChevronFace } => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonRight = elementRect.right + window.scrollX;
@@ -578,7 +576,7 @@ export const alwaysAboveRightOf = (
     elementRect: Pick,
     chevronFace = ChevronFace.None,
     vPadding = 0,
-) => {
+): IPosition & { chevronFace: ChevronFace } => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonLeft = elementRect.left + window.scrollX;
@@ -607,12 +605,12 @@ export const useContextMenu = (inputRef?: RefObject
     }
 
     const [isOpen, setIsOpen] = useState(false);
-    const open = (ev?: SyntheticEvent) => {
+    const open = (ev?: SyntheticEvent): void => {
         ev?.preventDefault();
         ev?.stopPropagation();
         setIsOpen(true);
     };
-    const close = (ev?: SyntheticEvent) => {
+    const close = (ev?: SyntheticEvent): void => {
         ev?.preventDefault();
         ev?.stopPropagation();
         setIsOpen(false);
@@ -622,8 +620,11 @@ export const useContextMenu = (inputRef?: RefObject
 };
 
 // XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
-export function createMenu(ElementClass, props) {
-    const onFinished = function (...args) {
+export function createMenu(
+    ElementClass: typeof React.Component,
+    props: Record,
+): { close: (...args: any[]) => void } {
+    const onFinished = function (...args): void {
         ReactDOM.unmountComponentAtNode(getOrCreateContainer());
         props?.onFinished?.apply(null, args);
     };
diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx
index d531e4fcc4..e3cacf0114 100644
--- a/src/components/structures/EmbeddedPage.tsx
+++ b/src/components/structures/EmbeddedPage.tsx
@@ -60,7 +60,7 @@ export default class EmbeddedPage extends React.PureComponent {
         return sanitizeHtml(_t(s));
     }
 
-    private async fetchEmbed() {
+    private async fetchEmbed(): Promise {
         let res: Response;
 
         try {
diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx
index ce24bb3783..e8a8fa5e28 100644
--- a/src/components/structures/FileDropTarget.tsx
+++ b/src/components/structures/FileDropTarget.tsx
@@ -37,7 +37,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => {
     useEffect(() => {
         if (!parent || parent.ondrop) return;
 
-        const onDragEnter = (ev: DragEvent) => {
+        const onDragEnter = (ev: DragEvent): void => {
             ev.stopPropagation();
             ev.preventDefault();
 
@@ -55,7 +55,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => {
             }));
         };
 
-        const onDragLeave = (ev: DragEvent) => {
+        const onDragLeave = (ev: DragEvent): void => {
             ev.stopPropagation();
             ev.preventDefault();
 
@@ -65,7 +65,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => {
             }));
         };
 
-        const onDragOver = (ev: DragEvent) => {
+        const onDragOver = (ev: DragEvent): void => {
             ev.stopPropagation();
             ev.preventDefault();
 
@@ -79,7 +79,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => {
             }
         };
 
-        const onDrop = (ev: DragEvent) => {
+        const onDrop = (ev: DragEvent): void => {
             ev.stopPropagation();
             ev.preventDefault();
             onFileDrop(ev.dataTransfer);
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index 6efa4f857a..4390dcf36e 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -223,7 +223,7 @@ class FilePanel extends React.Component {
         }
     }
 
-    public render() {
+    public render(): JSX.Element {
         if (MatrixClientPeg.get().isGuest()) {
             return (
                 
diff --git a/src/components/structures/GenericErrorPage.tsx b/src/components/structures/GenericErrorPage.tsx
index 4179abe7fd..4261d9b2f4 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() {
+    public render(): JSX.Element {
         return (
             
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 54aa635fe7..13fc132516 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -33,17 +33,17 @@ import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUpl import PosthogTrackers from "../../PosthogTrackers"; import EmbeddedPage from "./EmbeddedPage"; -const onClickSendDm = (ev: ButtonEvent) => { +const onClickSendDm = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeCreateChatButton", ev); dis.dispatch({ action: "view_create_chat" }); }; -const onClickExplore = (ev: ButtonEvent) => { +const onClickExplore = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeExploreRoomsButton", ev); dis.fire(Action.ViewRoomDirectory); }; -const onClickNewRoom = (ev: ButtonEvent) => { +const onClickNewRoom = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebHomeCreateRoomButton", ev); dis.dispatch({ action: "view_create_room" }); }; @@ -52,12 +52,17 @@ interface IProps { justRegistered?: boolean; } -const getOwnProfile = (userId: string) => ({ +const getOwnProfile = ( + userId: string, +): { + displayName: string; + avatarUrl: string; +} => ({ displayName: OwnProfileStore.instance.displayName || userId, avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), }); -const UserWelcomeTop = () => { +const UserWelcomeTop: React.FC = () => { const cli = useContext(MatrixClientContext); const userId = cli.getUserId(); const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId)); diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx index 7d4652e3ee..757a7360fb 100644 --- a/src/components/structures/HostSignupAction.tsx +++ b/src/components/structures/HostSignupAction.tsx @@ -28,7 +28,7 @@ interface IProps { interface IState {} export default class HostSignupAction extends React.PureComponent { - private openDialog = async () => { + private openDialog = async (): Promise => { this.props.onClick?.(); await HostSignupStore.instance.setHostSignupActive(true); }; diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index b5582323bf..99be8705a4 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -130,7 +130,7 @@ export default class InteractiveAuthComponent extends React.Component { @@ -155,7 +155,7 @@ export default class InteractiveAuthComponent extends React.Component { return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy; } - public componentDidMount() { + public componentDidMount(): void { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.on("ListContainer", this.refreshStickyHeaders); // Using the passive option to not block the main thread @@ -97,7 +97,7 @@ export default class LeftPanel extends React.Component { this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true }); } - public componentWillUnmount() { + public componentWillUnmount(): void { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); @@ -112,25 +112,25 @@ export default class LeftPanel extends React.Component { } } - private updateActiveSpace = (activeSpace: SpaceKey) => { + private updateActiveSpace = (activeSpace: SpaceKey): void => { this.setState({ activeSpace }); }; - private onDialPad = () => { + private onDialPad = (): void => { dis.fire(Action.OpenDialPad); }; - private onExplore = (ev: ButtonEvent) => { + private onExplore = (ev: ButtonEvent): void => { dis.fire(Action.ViewRoomDirectory); PosthogTrackers.trackInteraction("WebLeftPanelExploreRoomsButton", ev); }; - private refreshStickyHeaders = () => { + private refreshStickyHeaders = (): void => { if (!this.listContainerRef.current) return; // ignore: no headers to sticky this.handleStickyHeaders(this.listContainerRef.current); }; - private onBreadcrumbsUpdate = () => { + private onBreadcrumbsUpdate = (): void => { const newVal = LeftPanel.breadcrumbsMode; if (newVal !== this.state.showBreadcrumbs) { this.setState({ showBreadcrumbs: newVal }); @@ -141,7 +141,7 @@ export default class LeftPanel extends React.Component { } }; - private handleStickyHeaders(list: HTMLDivElement) { + private handleStickyHeaders(list: HTMLDivElement): void { if (this.isDoingStickyHeaders) return; this.isDoingStickyHeaders = true; window.requestAnimationFrame(() => { @@ -150,7 +150,7 @@ export default class LeftPanel extends React.Component { }); } - private doStickyHeaders(list: HTMLDivElement) { + private doStickyHeaders(list: HTMLDivElement): void { const topEdge = list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); @@ -282,20 +282,20 @@ export default class LeftPanel extends React.Component { } } - private onScroll = (ev: Event) => { + private onScroll = (ev: Event): void => { const list = ev.target as HTMLDivElement; this.handleStickyHeaders(list); }; - private onFocus = (ev: React.FocusEvent) => { + private onFocus = (ev: React.FocusEvent): void => { this.focusedElement = ev.target; }; - private onBlur = () => { + private onBlur = (): void => { this.focusedElement = null; }; - private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => { + private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState): void => { if (!this.focusedElement) return; const action = getKeyBindingsManager().getRoomListAction(ev); diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 9a4d82a9f8..5365352921 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -142,7 +142,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { return [...this.events][0]?.getRoomId(); } - private onSilencedCallsChanged = () => { + private onSilencedCallsChanged = (): void => { const newState = LegacyCallHandler.instance.isCallSilenced(this.callId); this.emit(LegacyCallEventGrouperEvent.SilencedChanged, newState); }; @@ -163,20 +163,20 @@ export default class LegacyCallEventGrouper extends EventEmitter { LegacyCallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video); }; - public toggleSilenced = () => { + public toggleSilenced = (): void => { const silenced = LegacyCallHandler.instance.isCallSilenced(this.callId); silenced ? LegacyCallHandler.instance.unSilenceCall(this.callId) : LegacyCallHandler.instance.silenceCall(this.callId); }; - private setCallListeners() { + private setCallListeners(): void { if (!this.call) return; this.call.addListener(CallEvent.State, this.setState); this.call.addListener(CallEvent.LengthChanged, this.onLengthChanged); } - private setState = () => { + private setState = (): void => { if (CONNECTING_STATES.includes(this.call?.state)) { this.state = CallState.Connecting; } else if (SUPPORTED_STATES.includes(this.call?.state)) { @@ -190,7 +190,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { this.emit(LegacyCallEventGrouperEvent.StateChanged, this.state); }; - private setCall = () => { + private setCall = (): void => { if (this.call) return; this.call = LegacyCallHandler.instance.getCallById(this.callId); @@ -198,7 +198,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { this.setState(); }; - public add(event: MatrixEvent) { + public add(event: MatrixEvent): void { if (this.events.has(event)) return; // nothing to do this.events.add(event); this.setCall(); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 6e18f8a6f7..242bbdc028 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -159,7 +159,7 @@ class LoggedInView extends React.Component { this.resizeHandler = React.createRef(); } - public componentDidMount() { + public componentDidMount(): void { document.addEventListener("keydown", this.onNativeKeyDown, false); LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState); @@ -191,7 +191,7 @@ class LoggedInView extends React.Component { this.refreshBackgroundImage(); } - public componentWillUnmount() { + public componentWillUnmount(): void { document.removeEventListener("keydown", this.onNativeKeyDown, false); LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState); this._matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData); @@ -221,14 +221,14 @@ class LoggedInView extends React.Component { this.setState({ backgroundImage }); }; - public canResetTimelineInRoom = (roomId: string) => { + public canResetTimelineInRoom = (roomId: string): boolean => { if (!this._roomView.current) { return true; } return this._roomView.current.canResetTimeline(); }; - private createResizer() { + private createResizer(): Resizer { let panelSize; let panelCollapsed; const collapseConfig: ICollapseConfig = { @@ -268,7 +268,7 @@ class LoggedInView extends React.Component { return resizer; } - private loadResizerPreferences() { + private loadResizerPreferences(): void { let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); if (isNaN(lhsSize)) { lhsSize = 350; @@ -276,13 +276,13 @@ class LoggedInView extends React.Component { this.resizer.forHandleWithId("lp-resizer").resize(lhsSize); } - private onAccountData = (event: MatrixEvent) => { + private onAccountData = (event: MatrixEvent): void => { if (event.getType() === "m.ignored_user_list") { dis.dispatch({ action: "ignore_state_changed" }); } }; - private onCompactLayoutChanged = () => { + private onCompactLayoutChanged = (): void => { this.setState({ useCompactLayout: SettingsStore.getValue("useCompactLayout"), }); @@ -311,13 +311,13 @@ class LoggedInView extends React.Component { } }; - private onUsageLimitDismissed = () => { + private onUsageLimitDismissed = (): void => { this.setState({ usageLimitDismissed: true, }); }; - private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit): void { const error = (syncError?.error as MatrixError)?.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { usageLimitEventContent = (syncError?.error as MatrixError).data as IUsageLimit; @@ -337,9 +337,9 @@ class LoggedInView extends React.Component { } } - private updateServerNoticeEvents = async () => { + private updateServerNoticeEvents = async (): Promise => { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; - if (!serverNoticeList) return []; + if (!serverNoticeList) return; const events = []; let pinnedEventTs = 0; @@ -379,7 +379,7 @@ class LoggedInView extends React.Component { }); }; - private onPaste = (ev: ClipboardEvent) => { + private onPaste = (ev: ClipboardEvent): void => { const element = ev.target as HTMLElement; const inputableElement = getInputableElement(element); if (inputableElement === document.activeElement) return; // nothing to do @@ -422,13 +422,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) => { + private onReactKeyDown = (ev): void => { // events caught while bubbling up on the root element // of this component, so something must be focused. this.onKeyDown(ev); }; - private onNativeKeyDown = (ev) => { + private onNativeKeyDown = (ev): 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. @@ -437,7 +437,7 @@ class LoggedInView extends React.Component { } }; - private onKeyDown = (ev) => { + private onKeyDown = (ev): void => { let handled = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); @@ -615,13 +615,13 @@ 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) => { + private onScrollKeyPressed = (ev): void => { if (this._roomView.current) { this._roomView.current.handleScrollKey(ev); } }; - public render() { + public render(): JSX.Element { let pageElement; switch (this.props.page_type) { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 536626f270..429adb6f50 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -134,7 +134,7 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { SdkContextClass, SDKContext } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; -import { VoiceBroadcastResumer } from "../../voice-broadcast"; +import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; import { Linkify } from "../views/elements/Linkify"; import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog"; @@ -216,7 +216,7 @@ export default class MatrixChat extends React.PureComponent { realQueryParams: {}, startingFragmentQueryParams: {}, config: {}, - onTokenLoginCompleted: () => {}, + onTokenLoginCompleted: (): void => {}, }; private firstSyncComplete = false; @@ -317,7 +317,7 @@ export default class MatrixChat extends React.PureComponent { this.props.realQueryParams, this.props.defaultDeviceDisplayName, this.getFragmentAfterLogin(), - ).then(async (loggedIn) => { + ).then(async (loggedIn): Promise => { if (this.props.realQueryParams?.loginToken) { // remove the loginToken from the URL regardless this.props.onTokenLoginCompleted(); @@ -353,7 +353,7 @@ export default class MatrixChat extends React.PureComponent { initSentry(SdkConfig.get("sentry")); } - private async postLoginSetup() { + private async postLoginSetup(): Promise { const cli = MatrixClientPeg.get(); const cryptoEnabled = cli.isCryptoEnabled(); if (!cryptoEnabled) { @@ -367,7 +367,7 @@ export default class MatrixChat extends React.PureComponent { // as a proxy to figure out if it's worth prompting the user to verify // from another device. promisesList.push( - (async () => { + (async (): Promise => { crossSigningIsSetUp = await cli.userHasCrossSigningKeys(); })(), ); @@ -417,7 +417,7 @@ export default class MatrixChat extends React.PureComponent { window.addEventListener("resize", this.onWindowResized); } - public componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState): void { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs); @@ -428,7 +428,7 @@ export default class MatrixChat extends React.PureComponent { } } - public componentWillUnmount() { + public componentWillUnmount(): void { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); @@ -477,7 +477,7 @@ export default class MatrixChat extends React.PureComponent { } } - private getServerProperties() { + private getServerProperties(): { serverConfig: ValidatedServerConfig } { let props = this.state.serverConfig; if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = SdkConfig.get("validated_server_config"); @@ -513,11 +513,11 @@ export default class MatrixChat extends React.PureComponent { // to try logging out. } - private startPageChangeTimer() { + private startPageChangeTimer(): void { PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } - private stopPageChangeTimer() { + private stopPageChangeTimer(): number | null { const perfMonitor = PerformanceMonitor.instance; perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); @@ -591,9 +591,10 @@ export default class MatrixChat extends React.PureComponent { break; case "logout": LegacyCallHandler.instance.hangupAllCalls(); - Promise.all([...CallStore.instance.activeCalls].map((call) => call.disconnect())).finally(() => - Lifecycle.logout(), - ); + Promise.all([ + ...[...CallStore.instance.activeCalls].map((call) => call.disconnect()), + cleanUpBroadcasts(this.stores), + ]).finally(() => Lifecycle.logout()); break; case "require_registration": startAnyRegistrationFlow(payload as any); @@ -876,13 +877,13 @@ export default class MatrixChat extends React.PureComponent { } }; - private setPage(pageType: PageType) { + private setPage(pageType: PageType): void { this.setState({ page_type: pageType, }); } - private async startRegistration(params: { [key: string]: string }) { + private async startRegistration(params: { [key: string]: string }): Promise { const newState: Partial = { view: Views.REGISTER, }; @@ -916,7 +917,7 @@ export default class MatrixChat extends React.PureComponent { } // switch view to the given room - private async viewRoom(roomInfo: ViewRoomPayload) { + private async viewRoom(roomInfo: ViewRoomPayload): Promise { this.focusComposer = true; if (roomInfo.room_alias) { @@ -987,12 +988,14 @@ export default class MatrixChat extends React.PureComponent { roomJustCreatedOpts: roomInfo.justCreatedOpts, }, () => { + ThemeController.isLogin = false; + this.themeWatcher.recheck(); this.notifyNewScreen("room/" + presentedId, replaceLast); }, ); } - private viewSomethingBehindModal() { + private viewSomethingBehindModal(): void { if (this.state.view !== Views.LOGGED_IN) { this.viewWelcome(); return; @@ -1002,7 +1005,7 @@ export default class MatrixChat extends React.PureComponent { } } - private viewWelcome() { + private viewWelcome(): void { if (shouldUseLoginForWelcome(SdkConfig.get())) { return this.viewLogin(); } @@ -1014,7 +1017,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewLogin(otherState?: any) { + private viewLogin(otherState?: any): void { this.setStateForNewView({ view: Views.LOGIN, ...otherState, @@ -1024,7 +1027,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewHome(justRegistered = false) { + private viewHome(justRegistered = false): void { // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ view: Views.LOGGED_IN, @@ -1037,7 +1040,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } - private viewUser(userId: string, subAction: string) { + private viewUser(userId: string, subAction: string): void { // Wait for the first sync so that `getRoom` gives us a room object if it's // in the sync response const waitForSync = this.firstSyncPromise ? this.firstSyncPromise.promise : Promise.resolve(); @@ -1052,7 +1055,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType) { + private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise { const modal = Modal.createDialog(CreateRoomDialog, { type, defaultPublic, @@ -1065,7 +1068,7 @@ export default class MatrixChat extends React.PureComponent { } } - private chatCreateOrReuse(userId: string) { + private chatCreateOrReuse(userId: string): void { const snakedConfig = new SnakedObject(this.props.config); // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.get().isGuest()) { @@ -1115,11 +1118,11 @@ export default class MatrixChat extends React.PureComponent { } } - private leaveRoomWarnings(roomId: string) { + private leaveRoomWarnings(roomId: string): JSX.Element[] { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const isSpace = roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. - const warnings = []; + const warnings: JSX.Element[] = []; const memberCount = roomToLeave.currentState.getJoinedMemberCount(); if (memberCount === 1) { @@ -1153,7 +1156,7 @@ export default class MatrixChat extends React.PureComponent { return warnings; } - private leaveRoom(roomId: string) { + private leaveRoom(roomId: string): void { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); @@ -1184,7 +1187,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private forgetRoom(roomId: string) { + private forgetRoom(roomId: string): void { const room = MatrixClientPeg.get().getRoom(roomId); MatrixClientPeg.get() .forget(roomId) @@ -1208,7 +1211,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async copyRoom(roomId: string) { + private async copyRoom(roomId: string): Promise { const roomLink = makeRoomPermalink(roomId); const success = await copyPlaintext(roomLink); if (!success) { @@ -1223,13 +1226,13 @@ export default class MatrixChat extends React.PureComponent { * Starts a chat with the welcome user, if the user doesn't already have one * @returns {string} The room ID of the new room, or null if no room was created */ - private async startWelcomeUserChat() { + private async startWelcomeUserChat(): Promise { // We can end up with multiple tabs post-registration where the user // might then end up with a session and we don't want them all making // a chat with the welcome user: try to de-dupe. // We need to wait for the first sync to complete for this to // work though. - let waitFor; + let waitFor: Promise; if (!this.firstSyncComplete) { waitFor = this.firstSyncPromise.promise; } else { @@ -1254,7 +1257,7 @@ export default class MatrixChat extends React.PureComponent { // run without the update to m.direct, making another welcome // user room (it doesn't wait for new data from the server, just // the saved sync to be loaded). - const saveWelcomeUser = (ev: MatrixEvent) => { + const saveWelcomeUser = (ev: MatrixEvent): void => { if (ev.getType() === EventType.Direct && ev.getContent()[snakedConfig.get("welcome_user_id")]) { MatrixClientPeg.get().store.save(true); MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser); @@ -1270,7 +1273,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when a new logged in session has started */ - private async onLoggedIn() { + private async onLoggedIn(): Promise { ThemeController.isLogin = false; this.themeWatcher.recheck(); StorageManager.tryPersistStorage(); @@ -1301,7 +1304,7 @@ export default class MatrixChat extends React.PureComponent { } } - private async onShowPostLoginScreen(useCase?: UseCase) { + private async onShowPostLoginScreen(useCase?: UseCase): Promise { if (useCase) { PosthogAnalytics.instance.setProperty("ftueUseCaseSelection", useCase); SettingsStore.setValue("FTUE.useCaseSelection", null, SettingLevel.ACCOUNT, useCase); @@ -1370,7 +1373,7 @@ export default class MatrixChat extends React.PureComponent { } } - private initPosthogAnalyticsToast() { + private initPosthogAnalyticsToast(): void { // Show the analytics toast if necessary if (SettingsStore.getValue("pseudonymousAnalyticsOptIn") === null) { showAnalyticsToast(); @@ -1397,7 +1400,7 @@ export default class MatrixChat extends React.PureComponent { ); } - private showScreenAfterLogin() { + private showScreenAfterLogin(): void { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory if (this.screenAfterLogin && this.screenAfterLogin.screen) { @@ -1415,7 +1418,7 @@ export default class MatrixChat extends React.PureComponent { } } - private viewLastRoom() { + private viewLastRoom(): void { dis.dispatch({ action: Action.ViewRoom, room_id: localStorage.getItem("mx_last_room_id"), @@ -1426,7 +1429,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when the session is logged out */ - private onLoggedOut() { + private onLoggedOut(): void { this.viewLogin({ ready: false, collapseLhs: false, @@ -1439,7 +1442,7 @@ export default class MatrixChat extends React.PureComponent { /** * Called when the session is softly logged out */ - private onSoftLogout() { + private onSoftLogout(): void { this.notifyNewScreen("soft_logout"); this.setStateForNewView({ view: Views.SOFT_LOGOUT, @@ -1455,7 +1458,7 @@ export default class MatrixChat extends React.PureComponent { * Called just before the matrix client is started * (useful for setting listeners) */ - private onWillStartClient() { + private onWillStartClient(): void { // reset the 'have completed first sync' flag, // since we're about to start the client and therefore about // to do the first sync @@ -1610,7 +1613,7 @@ export default class MatrixChat extends React.PureComponent { break; } }); - cli.on(CryptoEvent.KeyBackupFailed, async (errcode) => { + cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => { let haveNewVersion; let newVersionInfo; // if key backup is still enabled, there must be a new backup in place @@ -1678,7 +1681,7 @@ export default class MatrixChat extends React.PureComponent { * setting up anything that requires the client to be started. * @private */ - private onClientStarted() { + private onClientStarted(): void { const cli = MatrixClientPeg.get(); if (cli.isCryptoEnabled()) { @@ -1700,7 +1703,7 @@ export default class MatrixChat extends React.PureComponent { } } - public showScreen(screen: string, params?: { [key: string]: any }) { + public showScreen(screen: string, params?: { [key: string]: any }): void { const cli = MatrixClientPeg.get(); const isLoggedOutOrGuest = !cli || cli.isGuest(); if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) { @@ -1861,14 +1864,14 @@ export default class MatrixChat extends React.PureComponent { } } - private notifyNewScreen(screen: string, replaceLast = false) { + private notifyNewScreen(screen: string, replaceLast = false): void { if (this.props.onNewScreen) { this.props.onNewScreen(screen, replaceLast); } this.setPageSubtitle(); } - private onLogoutClick(event: React.MouseEvent) { + private onLogoutClick(event: React.MouseEvent): void { dis.dispatch({ action: "logout", }); @@ -1876,7 +1879,7 @@ export default class MatrixChat extends React.PureComponent { event.preventDefault(); } - private handleResize = () => { + private handleResize = (): void => { const LHS_THRESHOLD = 1000; const width = UIStore.instance.windowWidth; @@ -1892,19 +1895,19 @@ export default class MatrixChat extends React.PureComponent { this.state.resizeNotifier.notifyWindowResized(); }; - private dispatchTimelineResize() { + private dispatchTimelineResize(): void { dis.dispatch({ action: "timeline_resize" }); } - private onRegisterClick = () => { + private onRegisterClick = (): void => { this.showScreen("register"); }; - private onLoginClick = () => { + private onLoginClick = (): void => { this.showScreen("login"); }; - private onForgotPasswordClick = () => { + private onForgotPasswordClick = (): void => { this.showScreen("forgot_password"); }; @@ -1926,7 +1929,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private setPageSubtitle(subtitle = "") { + private setPageSubtitle(subtitle = ""): void { if (this.state.currentRoomId) { const client = MatrixClientPeg.get(); const room = client && client.getRoom(this.state.currentRoomId); @@ -1963,11 +1966,11 @@ export default class MatrixChat extends React.PureComponent { this.setPageSubtitle(); }; - private onServerConfigChange = (serverConfig: ValidatedServerConfig) => { + private onServerConfigChange = (serverConfig: ValidatedServerConfig): void => { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict) => { + private makeRegistrationUrl = (params: QueryDict): string => { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; } @@ -2016,7 +2019,7 @@ export default class MatrixChat extends React.PureComponent { return fragmentAfterLogin; } - public render() { + public render(): JSX.Element { const fragmentAfterLogin = this.getFragmentAfterLogin(); let view = null; @@ -2132,7 +2135,7 @@ export default class MatrixChat extends React.PureComponent { /> ); } else if (this.state.view === Views.USE_CASE_SELECTION) { - view = this.onShowPostLoginScreen(useCase)} />; + view = => this.onShowPostLoginScreen(useCase)} />; } else { logger.error(`Unknown view ${this.state.view}`); } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index cd3f322369..98e8f79ec7 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -296,19 +296,19 @@ export default class MessagePanel extends React.Component { ); } - public componentDidMount() { + public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); this.isMounted = true; } - public componentWillUnmount() { + public componentWillUnmount(): void { this.isMounted = false; this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } - public componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState): void { if (prevProps.layout !== this.props.layout) { this.calculateRoomMembersCount(); } @@ -752,7 +752,7 @@ export default class MessagePanel extends React.Component { const readReceipts = this.readReceiptsByEvent[eventId]; let isLastSuccessful = false; - const isSentState = (s) => !s || s === "sent"; + const isSentState = (s): boolean => !s || s === "sent"; const isSent = isSentState(mxEv.getAssociatedStatus()); const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { @@ -982,7 +982,7 @@ export default class MessagePanel extends React.Component { } } - public render() { + public render(): JSX.Element { let topSpinner; let bottomSpinner; if (this.props.backPaginating) { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index c2926a6448..813522ffcb 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -37,15 +37,15 @@ export default class NonUrgentToastContainer extends React.PureComponent { + private onUpdateToasts = (): void => { this.setState({ toasts: NonUrgentToastStore.instance.components }); }; - public render() { + public render(): JSX.Element { const toasts = this.state.toasts.map((t, i) => { return (
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 9e4365880e..ac351399d4 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -55,7 +55,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 1daea9eb89..19205229c8 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -79,7 +79,7 @@ export default class PictureInPictureDragger extends React.Component { this._moving = value; } - public componentDidMount() { + public componentDidMount(): void { document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); UIStore.instance.on(UI_EVENTS.Resize, this.onResize); @@ -87,7 +87,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(); } - public componentWillUnmount() { + public componentWillUnmount(): void { document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); UIStore.instance.off(UI_EVENTS.Resize, this.onResize); @@ -97,7 +97,7 @@ export default class PictureInPictureDragger extends React.Component { if (prevProps.children !== this.props.children) this.snap(true); } - private animationCallback = () => { + private animationCallback = (): void => { if ( !this.moving && Math.abs(this.translationX - this.desiredTranslationX) <= 1 && @@ -119,13 +119,13 @@ export default class PictureInPictureDragger extends React.Component { this.props.onMove?.(); }; - private setStyle = () => { + private setStyle = (): void => { if (!this.callViewWrapper.current) return; // Set the element's style directly, bypassing React for efficiency this.callViewWrapper.current.style.transform = `translateX(${this.translationX}px) translateY(${this.translationY}px)`; }; - private setTranslation(inTranslationX: number, inTranslationY: number) { + private setTranslation(inTranslationX: number, inTranslationY: number): void { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; @@ -152,7 +152,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(false); }; - private snap = (animate = false) => { + private snap = (animate = false): void => { const translationX = this.desiredTranslationX; const translationY = this.desiredTranslationY; // We subtract the PiP size from the window size in order to calculate @@ -187,14 +187,14 @@ export default class PictureInPictureDragger extends React.Component { this.scheduledUpdate.mark(); }; - private onStartMoving = (event: React.MouseEvent | MouseEvent) => { + private onStartMoving = (event: React.MouseEvent | MouseEvent): void => { event.preventDefault(); event.stopPropagation(); this.mouseHeld = true; }; - private onMoving = (event: MouseEvent) => { + private onMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; event.preventDefault(); @@ -210,7 +210,7 @@ export default class PictureInPictureDragger extends React.Component { this.setTranslation(event.pageX - this.initX, event.pageY - this.initY); }; - private onEndMoving = (event: MouseEvent) => { + private onEndMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; event.preventDefault(); @@ -223,7 +223,7 @@ export default class PictureInPictureDragger extends React.Component { this.snap(true); }; - private onClickCapture = (event: React.MouseEvent) => { + private onClickCapture = (event: React.MouseEvent): void => { // To prevent mouse up events during dragging from being double-counted // as clicks, we cancel clicks before they ever reach the target if (this.moving) { @@ -232,7 +232,7 @@ export default class PictureInPictureDragger extends React.Component { } }; - public render() { + public render(): JSX.Element { 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 a932c43e7d..416458e6ff 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -135,7 +135,7 @@ class PipContainerInner extends React.Component { }; } - public componentDidMount() { + public componentDidMount(): void { LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls); LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls); SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate); @@ -149,7 +149,7 @@ class PipContainerInner extends React.Component { ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); } - public componentWillUnmount() { + public componentWillUnmount(): void { LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls); LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls); const cli = MatrixClientPeg.get(); @@ -164,9 +164,9 @@ class PipContainerInner extends React.Component { ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); } - private onMove = () => this.props.movePersistedElement.current?.(); + private onMove = (): void => this.props.movePersistedElement.current?.(); - private onRoomViewStoreUpdate = () => { + private onRoomViewStoreUpdate = (): void => { const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); const oldRoomId = this.state.viewedRoomId; if (newRoomId === oldRoomId) return; @@ -213,7 +213,7 @@ class PipContainerInner extends React.Component { this.updateShowWidgetInPip(); }; - private onCallRemoteHold = () => { + private onCallRemoteHold = (): void => { if (!this.state.viewedRoomId) return; const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId); @@ -238,7 +238,7 @@ class PipContainerInner extends React.Component { public updateShowWidgetInPip( persistentWidgetId = this.state.persistentWidgetId, persistentRoomId = this.state.persistentRoomId, - ) { + ): void { let fromAnotherRoom = false; let notDocked = false; // Sanity check the room - the widget may have been destroyed between render cycles, and @@ -293,7 +293,7 @@ class PipContainerInner extends React.Component { ); } - public render() { + public render(): JSX.Element { const pipMode = true; let pipContent: Array = []; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 8759160057..3748ee0ec7 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -101,7 +101,7 @@ export default class RightPanel extends React.Component { }; } - private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => { + private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } @@ -118,11 +118,11 @@ export default class RightPanel extends React.Component { } }; - private onRightPanelStoreUpdate = () => { + private onRightPanelStoreUpdate = (): void => { this.setState({ ...(RightPanel.getDerivedStateFromProps(this.props) as IState) }); }; - private onClose = () => { + private onClose = (): void => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 6c7ddbe755..a387a2e0d5 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -39,15 +39,15 @@ export default class RoomSearch extends React.PureComponent { this.dispatcherRef = defaultDispatcher.register(this.onAction); } - public componentWillUnmount() { + public componentWillUnmount(): void { defaultDispatcher.unregister(this.dispatcherRef); } - private openSpotlight() { + private openSpotlight(): void { Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true); } - private onAction = (payload: ActionPayload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action === "focus_room_filter") { this.openSpotlight(); } diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index d7a995b5c0..132e2a191b 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -37,7 +37,7 @@ import RoomContext from "../../contexts/RoomContext"; import SettingsStore from "../../settings/SettingsStore"; const DEBUG = false; -let debuglog = function (msg: string) {}; +let debuglog = function (msg: string): void {}; /* istanbul ignore next */ if (DEBUG) { @@ -76,7 +76,7 @@ export const RoomSearchView = forwardRef( return searchPromise .then( - async (results) => { + async (results): Promise => { debuglog("search complete"); if (aborted.current) { logger.error("Discarding stale search results"); @@ -209,7 +209,7 @@ export const RoomSearchView = forwardRef( // once dynamic content in the search results load, make the scrollPanel check // the scroll offsets. - const onHeightChanged = () => { + const onHeightChanged = (): void => { const scrollPanel = ref.current; scrollPanel?.checkScroll(); }; diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 7d621afc5d..f370091a8a 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -146,7 +146,7 @@ export default class RoomStatusBar extends React.PureComponent { dis.fire(Action.FocusSendMessageComposer); }; - private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { + private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room): void => { if (room.roomId !== this.props.room.roomId) return; const messages = getUnsentMessages(this.props.room); this.setState({ diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index eb034fd2b7..8d85b54df7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -115,7 +115,7 @@ import VoipUserMapper from "../../VoipUserMapper"; import { isCallEvent } from "./LegacyCallEventGrouper"; const DEBUG = false; -let debuglog = function (msg: string) {}; +let debuglog = function (msg: string): void {}; const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe"); @@ -248,7 +248,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { encryptionTile = ; } - const onRetryClicked = () => { + const onRetryClicked = (): void => { room.state = LocalRoomState.NEW; defaultDispatcher.dispatch({ action: "local_room_event", @@ -470,21 +470,21 @@ export class RoomView extends React.Component { ]; } - private onIsResizing = (resizing: boolean) => { + private onIsResizing = (resizing: boolean): void => { this.setState({ resizing }); }; - private onWidgetStoreUpdate = () => { + private onWidgetStoreUpdate = (): void => { if (!this.state.room) return; this.checkWidgets(this.state.room); }; - private onWidgetEchoStoreUpdate = () => { + private onWidgetEchoStoreUpdate = (): void => { if (!this.state.room) return; this.checkWidgets(this.state.room); }; - private onWidgetLayoutChange = () => { + private onWidgetLayoutChange = (): void => { if (!this.state.room) return; dis.dispatch({ action: "appsDrawer", @@ -505,7 +505,7 @@ export class RoomView extends React.Component { }); }; - private getMainSplitContentType = (room: Room) => { + private getMainSplitContentType = (room: Room): MainSplitContentType => { if ( (SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall()) || isVideoRoom(room) @@ -707,7 +707,7 @@ export class RoomView extends React.Component { } }; - private onActiveCalls = () => { + private onActiveCalls = (): void => { if (this.state.roomId === undefined) return; const activeCall = CallStore.instance.getActiveCall(this.state.roomId); @@ -727,7 +727,7 @@ export class RoomView extends React.Component { this.setState({ activeCall }); }; - private getRoomId = () => { + private getRoomId = (): string => { // According to `onRoomViewStoreUpdate`, `state.roomId` can be null // if we have a room alias we haven't resolved yet. To work around this, // first we'll try the room object if it's there, and then fallback to @@ -736,7 +736,7 @@ export class RoomView extends React.Component { return this.state.room ? this.state.room.roomId : this.state.roomId; }; - private getPermalinkCreatorForRoom(room: Room) { + private getPermalinkCreatorForRoom(room: Room): RoomPermalinkCreator { if (this.permalinkCreators[room.roomId]) return this.permalinkCreators[room.roomId]; this.permalinkCreators[room.roomId] = new RoomPermalinkCreator(room); @@ -750,14 +750,14 @@ export class RoomView extends React.Component { return this.permalinkCreators[room.roomId]; } - private stopAllPermalinkCreators() { + private stopAllPermalinkCreators(): void { if (!this.permalinkCreators) return; for (const roomId of Object.keys(this.permalinkCreators)) { this.permalinkCreators[roomId].stop(); } } - private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) { + private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean): void { // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -822,7 +822,7 @@ export class RoomView extends React.Component { } } - private shouldShowApps(room: Room) { + private shouldShowApps(room: Room): boolean { if (!BROWSER_SUPPORTS_SANDBOX || !room) return false; // Check if user has previously chosen to hide the app drawer for this @@ -838,7 +838,7 @@ export class RoomView extends React.Component { return isManuallyShown && widgets.length > 0; } - public componentDidMount() { + public componentDidMount(): void { this.onRoomViewStoreUpdate(true); const call = this.getCallForRoom(); @@ -851,7 +851,7 @@ export class RoomView extends React.Component { window.addEventListener("beforeunload", this.onPageUnload); } - public shouldComponentUpdate(nextProps, nextState) { + public shouldComponentUpdate(nextProps, nextState): boolean { const hasPropsDiff = objectHasDiff(this.props, nextProps); const { upgradeRecommendation, ...state } = this.state; @@ -864,7 +864,7 @@ export class RoomView extends React.Component { return hasPropsDiff || hasStateDiff; } - public componentDidUpdate() { + public componentDidUpdate(): void { // Note: We check the ref here with a flag because componentDidMount, despite // documentation, does not define our messagePanel ref. It looks like our spinner // in render() prevents the ref from being set on first mount, so we try and @@ -877,7 +877,7 @@ export class RoomView extends React.Component { } } - public componentWillUnmount() { + public componentWillUnmount(): void { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -947,13 +947,13 @@ export class RoomView extends React.Component { } } - private onRightPanelStoreUpdate = () => { + private onRightPanelStoreUpdate = (): void => { this.setState({ showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.state.roomId), }); }; - private onPageUnload = (event) => { + private onPageUnload = (event): 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") { @@ -961,7 +961,7 @@ export class RoomView extends React.Component { } }; - private onReactKeyDown = (ev) => { + private onReactKeyDown = (ev): void => { let handled = false; const action = getKeyBindingsManager().getRoomAction(ev); @@ -1120,12 +1120,12 @@ export class RoomView extends React.Component { } }; - private onLocalRoomEvent(roomId: string) { + private onLocalRoomEvent(roomId: string): void { if (roomId !== this.state.room.roomId) return; createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } - private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => { + private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data): void => { if (this.unmounted) return; // ignore events for other rooms or the notification timeline set @@ -1167,7 +1167,7 @@ export class RoomView extends React.Component { } }; - private onEventDecrypted = (ev: MatrixEvent) => { + private onEventDecrypted = (ev: MatrixEvent): void => { if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all if (ev.getRoomId() !== this.state.room.roomId) return; // not for us this.updateVisibleDecryptionFailures(); @@ -1175,7 +1175,7 @@ export class RoomView extends React.Component { this.handleEffects(ev); }; - private handleEffects = (ev: MatrixEvent) => { + private handleEffects = (ev: MatrixEvent): void => { const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room); if (!notifState.isUnread) return; @@ -1189,19 +1189,19 @@ export class RoomView extends React.Component { }); }; - private onRoomName = (room: Room) => { + private onRoomName = (room: Room): void => { if (this.state.room && room.roomId == this.state.room.roomId) { this.forceUpdate(); } }; - private onKeyBackupStatus = () => { + private onKeyBackupStatus = (): void => { // Key backup status changes affect whether the in-room recovery // reminder is displayed. this.forceUpdate(); }; - public canResetTimeline = () => { + public canResetTimeline = (): boolean => { if (!this.messagePanel) { return true; } @@ -1216,7 +1216,7 @@ export class RoomView extends React.Component { // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). - private onRoomLoaded = (room: Room) => { + private onRoomLoaded = (room: Room): void => { if (this.unmounted) return; // Attach a widget store listener only when we get a room this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); @@ -1251,17 +1251,17 @@ export class RoomView extends React.Component { } }; - private getRoomTombstone(room = this.state.room) { + private getRoomTombstone(room = this.state.room): MatrixEvent | undefined { return room?.currentState.getStateEvents(EventType.RoomTombstone, ""); } - private async calculateRecommendedVersion(room: Room) { + private async calculateRecommendedVersion(room: Room): Promise { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; this.setState({ upgradeRecommendation }); } - private async loadMembersIfJoined(room: Room) { + private async loadMembersIfJoined(room: Room): Promise { // lazy load members if enabled if (this.context.client.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === "join") { @@ -1280,14 +1280,14 @@ export class RoomView extends React.Component { } } - private calculatePeekRules(room: Room) { + private calculatePeekRules(room: Room): void { const historyVisibility = room.currentState.getStateEvents(EventType.RoomHistoryVisibility, ""); this.setState({ canPeek: historyVisibility?.getContent().history_visibility === HistoryVisibility.WorldReadable, }); } - private updatePreviewUrlVisibility({ roomId }: Room) { + private updatePreviewUrlVisibility({ roomId }: Room): void { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit const key = this.context.client.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; this.setState({ @@ -1295,7 +1295,7 @@ export class RoomView extends React.Component { }); } - private onRoom = (room: Room) => { + private onRoom = (room: Room): void => { if (!room || room.roomId !== this.state.roomId) { return; } @@ -1318,7 +1318,7 @@ export class RoomView extends React.Component { ); }; - private onDeviceVerificationChanged = (userId: string) => { + private onDeviceVerificationChanged = (userId: string): void => { const room = this.state.room; if (!room?.currentState.getMember(userId)) { return; @@ -1326,7 +1326,7 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); }; - private onUserVerificationChanged = (userId: string) => { + private onUserVerificationChanged = (userId: string): void => { const room = this.state.room; if (!room || !room.currentState.getMember(userId)) { return; @@ -1334,14 +1334,14 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); }; - private onCrossSigningKeysChanged = () => { + private onCrossSigningKeysChanged = (): void => { const room = this.state.room; if (room) { this.updateE2EStatus(room); } }; - private async updateE2EStatus(room: Room) { + private async updateE2EStatus(room: Room): Promise { if (!this.context.client.isRoomEncrypted(room.roomId)) return; // If crypto is not currently enabled, we aren't tracking devices at all, @@ -1357,13 +1357,13 @@ export class RoomView extends React.Component { this.setState({ e2eStatus }); } - private onUrlPreviewsEnabledChange = () => { + private onUrlPreviewsEnabledChange = (): void => { if (this.state.room) { this.updatePreviewUrlVisibility(this.state.room); } }; - private onRoomStateEvents = (ev: MatrixEvent, state: RoomState) => { + private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { // ignore if we don't have a room yet if (!this.state.room || this.state.room.roomId !== state.roomId) return; @@ -1377,7 +1377,7 @@ export class RoomView extends React.Component { } }; - private onRoomStateUpdate = (state: RoomState) => { + private onRoomStateUpdate = (state: RoomState): void => { // ignore members in other rooms if (state.roomId !== this.state.room?.roomId) { return; @@ -1386,7 +1386,7 @@ export class RoomView extends React.Component { this.updateRoomMembers(); }; - private onMyMembership = (room: Room, membership: string, oldMembership: string) => { + private onMyMembership = (room: Room, membership: string, oldMembership: string): void => { if (room.roomId === this.state.roomId) { this.forceUpdate(); this.loadMembersIfJoined(room); @@ -1394,7 +1394,7 @@ export class RoomView extends React.Component { } }; - private updatePermissions(room: Room) { + private updatePermissions(room: Room): void { if (room) { const me = this.context.client.getUserId(); const canReact = @@ -1420,7 +1420,7 @@ export class RoomView extends React.Component { { leading: true, trailing: true }, ); - private checkDesktopNotifications() { + private checkDesktopNotifications(): void { const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); // if they are not alone prompt the user about notifications so they don't miss replies if (memberCount > 1 && Notifier.shouldShowPrompt()) { @@ -1428,7 +1428,7 @@ export class RoomView extends React.Component { } } - private updateDMState() { + private updateDMState(): void { const room = this.state.room; if (room.getMyMembership() != "join") { return; @@ -1439,7 +1439,7 @@ export class RoomView extends React.Component { } } - private onInviteClick = () => { + private onInviteClick = (): void => { // open the room inviter dis.dispatch({ action: "view_invite", @@ -1447,7 +1447,7 @@ export class RoomView extends React.Component { }); }; - private onJoinButtonClicked = () => { + private onJoinButtonClicked = (): void => { // If the user is a ROU, allow them to transition to a PWLU if (this.context.client?.isGuest()) { // Join this room once the user has registered and logged in @@ -1489,7 +1489,7 @@ export class RoomView extends React.Component { { leading: false, trailing: true }, ); - private onMessageListScroll = () => { + private onMessageListScroll = (): void => { if (this.messagePanel.isAtEndOfLiveTimeline()) { this.setState({ numUnreadMessages: 0, @@ -1504,7 +1504,7 @@ export class RoomView extends React.Component { this.updateVisibleDecryptionFailures(); }; - private resetJumpToEvent = (eventId?: string) => { + private resetJumpToEvent = (eventId?: string): void => { if ( this.state.initialEventId && this.state.initialEventScrollIntoView && @@ -1523,7 +1523,7 @@ export class RoomView extends React.Component { } }; - private injectSticker(url: string, info: object, text: string, threadId: string | null) { + private injectSticker(url: string, info: object, text: string, threadId: string | null): void { if (this.context.client.isGuest()) { dis.dispatch({ action: "require_registration" }); return; @@ -1539,7 +1539,7 @@ export class RoomView extends React.Component { }); } - private onSearch = (term: string, scope: SearchScope) => { + private onSearch = (term: string, scope: SearchScope): void => { const roomId = scope === SearchScope.Room ? this.state.room.roomId : undefined; debuglog("sending search request"); const abortController = new AbortController(); @@ -1569,21 +1569,21 @@ export class RoomView extends React.Component { }); }; - private onAppsClick = () => { + private onAppsClick = (): void => { dis.dispatch({ action: "appsDrawer", show: !this.state.showApps, }); }; - private onForgetClick = () => { + private onForgetClick = (): void => { dis.dispatch({ action: "forget_room", room_id: this.state.room.roomId, }); }; - private onRejectButtonClicked = () => { + private onRejectButtonClicked = (): void => { this.setState({ rejecting: true, }); @@ -1611,7 +1611,7 @@ export class RoomView extends React.Component { ); }; - private onRejectAndIgnoreClick = async () => { + private onRejectAndIgnoreClick = async (): Promise => { this.setState({ rejecting: true, }); @@ -1644,7 +1644,7 @@ export class RoomView extends React.Component { } }; - private onRejectThreepidInviteButtonClicked = () => { + private onRejectThreepidInviteButtonClicked = (): void => { // We can reject 3pid invites in the same way that we accept them, // using /leave rather than /join. In the short term though, we // just ignore them. @@ -1652,7 +1652,7 @@ export class RoomView extends React.Component { dis.fire(Action.ViewRoomDirectory); }; - private onSearchClick = () => { + private onSearchClick = (): void => { this.setState({ timelineRenderingType: this.state.timelineRenderingType === TimelineRenderingType.Search @@ -1674,7 +1674,7 @@ export class RoomView extends React.Component { }; // jump down to the bottom of this room, where new events are arriving - private jumpToLiveTimeline = () => { + private jumpToLiveTimeline = (): void => { if (this.state.initialEventId && this.state.isInitialEventHighlighted) { // If we were viewing a highlighted event, firing view_room without // an event will take care of both clearing the URL fragment and @@ -1692,18 +1692,18 @@ export class RoomView extends React.Component { }; // jump up to wherever our read marker is - private jumpToReadMarker = () => { + private jumpToReadMarker = (): void => { this.messagePanel.jumpToReadMarker(); }; // update the read marker to match the read-receipt - private forgetReadMarker = (ev) => { + private forgetReadMarker = (ev): void => { ev.stopPropagation(); this.messagePanel.forgetReadMarker(); }; // decide whether or not the top 'unread messages' bar should be shown - private updateTopUnreadMessagesBar = () => { + private updateTopUnreadMessagesBar = (): void => { if (!this.messagePanel) { return; } @@ -1754,12 +1754,12 @@ export class RoomView extends React.Component { }; } - private onStatusBarVisible = () => { + private onStatusBarVisible = (): void => { if (this.unmounted || this.state.statusBarVisible) return; this.setState({ statusBarVisible: true }); }; - private onStatusBarHidden = () => { + private onStatusBarHidden = (): void => { // This is currently not desired as it is annoying if it keeps expanding and collapsing if (this.unmounted || !this.state.statusBarVisible) return; this.setState({ statusBarVisible: false }); @@ -1770,7 +1770,7 @@ export class RoomView extends React.Component { * * We pass it down to the scroll panel. */ - public handleScrollKey = (ev) => { + public handleScrollKey = (ev): void => { let panel: ScrollPanel | TimelinePanel; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; @@ -1793,24 +1793,24 @@ 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) => { + private gatherTimelinePanelRef = (r): void => { this.messagePanel = r; }; - private getOldRoom() { + 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"]); } - public getHiddenHighlightCount() { + public getHiddenHighlightCount(): number { const oldRoom = this.getOldRoom(); if (!oldRoom) return 0; return oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight); } - public onHiddenHighlightsClick = () => { + public onHiddenHighlightsClick = (): void => { const oldRoom = this.getOldRoom(); if (!oldRoom) return; dis.dispatch({ @@ -1826,7 +1826,7 @@ export class RoomView extends React.Component { }); } - private onFileDrop = (dataTransfer: DataTransfer) => + private onFileDrop = (dataTransfer: DataTransfer): Promise => ContentMessages.sharedInstance().sendContentListToRoom( Array.from(dataTransfer.files), this.state.room?.roomId ?? this.state.roomId, @@ -1869,7 +1869,7 @@ export class RoomView extends React.Component { ); } - public render() { + public render(): JSX.Element { 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 66676666df..f51cba66a3 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -35,7 +35,7 @@ const UNFILL_REQUEST_DEBOUNCE_MS = 200; // much while the content loads. const PAGE_SIZE = 400; -const debuglog = (...args: any[]) => { +const debuglog = (...args: any[]): void => { if (SettingsStore.getValue("debug_scroll_panel")) { logger.log.call(console, "ScrollPanel debuglog:", ...args); } @@ -227,14 +227,14 @@ export default class ScrollPanel extends React.Component { this.props.resizeNotifier?.removeListener("middlePanelResizedNoisy", this.onResize); } - private onScroll = (ev: Event | React.UIEvent): void => { + private onScroll = (ev: Event): void => { // skip scroll events caused by resizing if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; debuglog("onScroll called past resize gate; scroll node top:", this.getScrollNode().scrollTop); this.scrollTimeout.restart(); this.saveScrollState(); this.updatePreventShrinking(); - this.props.onScroll?.(ev as Event); + this.props.onScroll?.(ev); // noinspection JSIgnoredPromiseFromCall this.checkFillState(); }; @@ -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) => { + public handleScrollKey = (ev: KeyboardEvent): void => { const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case KeyBindingAction.ScrollUp: @@ -853,7 +853,7 @@ export default class ScrollPanel extends React.Component { return this.divScroll; } - private collectScroll = (divScroll: HTMLDivElement) => { + private collectScroll = (divScroll: HTMLDivElement): void => { this.divScroll = divScroll; }; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 13674347aa..af6f298382 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -114,12 +114,12 @@ const Tile: React.FC = ({ const [onFocus, isActive, ref] = useRovingTabIndex(); const [busy, setBusy] = useState(false); - const onPreviewClick = (ev: ButtonEvent) => { + const onPreviewClick = (ev: ButtonEvent): void => { ev.preventDefault(); ev.stopPropagation(); onViewRoomClick(); }; - const onJoinClick = async (ev: ButtonEvent) => { + const onJoinClick = async (ev: ButtonEvent): Promise => { setBusy(true); ev.preventDefault(); ev.stopPropagation(); @@ -271,7 +271,7 @@ const Tile: React.FC = ({ ); if (showChildren) { - const onChildrenKeyDown = (e) => { + const onChildrenKeyDown = (e): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.ArrowLeft: @@ -439,7 +439,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => return room; }; -export const HierarchyLevel = ({ +export const HierarchyLevel: React.FC = ({ root, roomSet, hierarchy, @@ -448,7 +448,7 @@ export const HierarchyLevel = ({ onViewRoomClick, onJoinRoomClick, onToggleClick, -}: IHierarchyLevelProps) => { +}) => { const cli = useContext(MatrixClientContext); const space = cli.getRoom(root.room_id); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); @@ -553,7 +553,7 @@ export const useRoomHierarchy = ( }); const loadMore = useCallback( - async (pageSize?: number) => { + async (pageSize?: number): Promise => { if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport || error) return; await hierarchy.load(pageSize).catch(setError); setRooms(hierarchy.rooms); @@ -578,8 +578,8 @@ export const useRoomHierarchy = ( }; }; -const useIntersectionObserver = (callback: () => void) => { - const handleObserver = (entries: IntersectionObserverEntry[]) => { +const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElement) => void) => { + const handleObserver = (entries: IntersectionObserverEntry[]): void => { const target = entries[0]; if (target.isIntersecting) { callback(); @@ -610,7 +610,7 @@ interface IManageButtonsProps { setError: Dispatch>; } -const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageButtonsProps) => { +const ManageButtons: React.FC = ({ hierarchy, selected, setSelected, setError }) => { const cli = useContext(MatrixClientContext); const [removing, setRemoving] = useState(false); @@ -645,7 +645,7 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu <>