diff --git a/.editorconfig b/.editorconfig index 56631484cd5..98ebc4dc8f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,4 +23,4 @@ indent_size = 4 trim_trailing_whitespace = true [*.{yml,yaml}] -indent_size = 2 +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js index 7c2ebb96df5..d133a3c0051 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -165,10 +165,31 @@ module.exports = { }, { files: ["test/**/*.{ts,tsx}", "cypress/**/*.ts"], + extends: ["plugin:matrix-org/jest"], rules: { // We don't need super strict typing in test utilities "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "off", + + // Jest/Cypress specific + + // Disabled tests are a reality for now but as soon as all of the xits are + // eliminated, we should enforce this. + "jest/no-disabled-tests": "off", + // TODO: There are many tests with invalid expects that should be fixed, + // https://github.com/vector-im/element-web/issues/24709 + "jest/valid-expect": "off", + // TODO: There are many cases to refactor away, + // https://github.com/vector-im/element-web/issues/24710 + "jest/no-conditional-expect": "off", + // Also treat "oldBackendOnly" as a test function. + // Used in some crypto tests. + "jest/no-standalone-expect": [ + "error", + { + additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"], + }, + ], }, }, { @@ -176,6 +197,11 @@ module.exports = { parserOptions: { project: ["./cypress/tsconfig.json"], }, + rules: { + // Cypress "promises" work differently - disable some related rules + "jest/valid-expect-in-promise": "off", + "jest/no-done-callback": "off", + }, }, ], settings: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8428ec020..3b156c98a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ -Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28) +Changes in [3.68.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.68.0) (2023-03-15) ===================================================================================================== ## ✨ Features + * Only allow to start a DM with one email if encryption by default is enabled ([\#10253](https://github.com/matrix-org/matrix-react-sdk/pull/10253)). Fixes vector-im/element-web#23133. + * DM rooms are now encrypted if encryption by default is enabled and only inviting a single email address. Any action in the result DM room will be blocked until the other has joined. ([\#10229](https://github.com/matrix-org/matrix-react-sdk/pull/10229)). + * Reduce bottom margin of ReplyChain on compact modern layout ([\#8972](https://github.com/matrix-org/matrix-react-sdk/pull/8972)). Fixes vector-im/element-web#22748. Contributed by @luixxiul. + * Support for v2 of MSC3903 ([\#10165](https://github.com/matrix-org/matrix-react-sdk/pull/10165)). Contributed by @hughns. + * When starting a DM, existing rooms with pending third-party invites will be reused. ([\#10256](https://github.com/matrix-org/matrix-react-sdk/pull/10256)). Fixes vector-im/element-web#23139. + * Polls push rules: synchronise poll rules with message rules ([\#10263](https://github.com/matrix-org/matrix-react-sdk/pull/10263)). Contributed by @kerryarchibald. + * New verification request toast button labels ([\#10259](https://github.com/matrix-org/matrix-react-sdk/pull/10259)). + * Remove padding around integration manager iframe ([\#10148](https://github.com/matrix-org/matrix-react-sdk/pull/10148)). * Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner. * Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald. * Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt. @@ -13,6 +21,28 @@ Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk. * The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)). * Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)). + +## 🐛 Bug Fixes + * Fix incorrect copy in space creation flow ([\#10296](https://github.com/matrix-org/matrix-react-sdk/pull/10296)). Fixes vector-im/element-web#24741. + * Fix space settings dialog having rogue title tooltip ([\#10293](https://github.com/matrix-org/matrix-react-sdk/pull/10293)). Fixes vector-im/element-web#24740. + * Show spinner when starting a DM from the user profile (right panel) ([\#10290](https://github.com/matrix-org/matrix-react-sdk/pull/10290)). + * Reduce height of toggle on expanded view source event ([\#10283](https://github.com/matrix-org/matrix-react-sdk/pull/10283)). Fixes vector-im/element-web#22873. Contributed by @luixxiul. + * Pillify http and non-prefixed matrix.to links ([\#10277](https://github.com/matrix-org/matrix-react-sdk/pull/10277)). Fixes vector-im/element-web#20844. + * Fix some features not being configurable via `features` ([\#10276](https://github.com/matrix-org/matrix-react-sdk/pull/10276)). + * Fix starting a DM from the right panel in some cases ([\#10278](https://github.com/matrix-org/matrix-react-sdk/pull/10278)). Fixes vector-im/element-web#24722. + * Align info EventTile and normal EventTile on IRC layout ([\#10197](https://github.com/matrix-org/matrix-react-sdk/pull/10197)). Fixes vector-im/element-web#22782. Contributed by @luixxiul. + * Fix blowout of waveform of the voice message player on narrow UI ([\#8861](https://github.com/matrix-org/matrix-react-sdk/pull/8861)). Fixes vector-im/element-web#22604. Contributed by @luixxiul. + * Fix the hidden view source toggle on IRC layout ([\#10266](https://github.com/matrix-org/matrix-react-sdk/pull/10266)). Fixes vector-im/element-web#22872. Contributed by @luixxiul. + * Fix buttons on the room header being compressed due to long room name ([\#10155](https://github.com/matrix-org/matrix-react-sdk/pull/10155)). Contributed by @luixxiul. + * Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)). + * Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)). + * Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009. + * Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul. + +Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28) +===================================================================================================== + +## ✨ Features * Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros. * Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald. * Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald. @@ -25,11 +55,7 @@ Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. ## 🐛 Bug Fixes - * Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)). - * Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)). * Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33. - * Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009. - * Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul. * Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt. * Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald. * Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne. diff --git a/cypress/e2e/create-room/create-room.spec.ts b/cypress/e2e/create-room/create-room.spec.ts index 72805d5e12b..a51c22ef862 100644 --- a/cypress/e2e/create-room/create-room.spec.ts +++ b/cypress/e2e/create-room/create-room.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -61,4 +61,31 @@ describe("Create Room", () => { cy.contains(".mx_RoomHeader_nametext", name); cy.contains(".mx_RoomHeader_topic", topic); }); + + it("should create a room with a long room name, which is displayed with ellipsis", () => { + let roomId: string; + const LONG_ROOM_NAME = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + + "officia deserunt mollit anim id est laborum."; + + cy.createRoom({ name: LONG_ROOM_NAME }).then((_roomId) => { + roomId = _roomId; + cy.visit("/#/room/" + roomId); + }); + + // Wait until the room name is set + cy.get(".mx_RoomHeader_nametext").contains("Lorem ipsum"); + + // Make sure size of buttons on RoomHeader (except .mx_RoomHeader_name) are specified + // and the buttons are not compressed + // TODO: use a same class name + cy.get(".mx_RoomHeader_button").should("have.css", "height", "32px").should("have.css", "width", "32px"); + cy.get(".mx_HeaderButtons > .mx_RightPanel_headerButton") + .should("have.css", "height", "32px") + .should("have.css", "width", "32px"); + cy.get(".mx_RoomHeader").percySnapshotElement("Room header with a long room name"); + }); }); diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index 13e3c56abab..15b437d6211 100644 --- a/cypress/e2e/crypto/decryption-failure.spec.ts +++ b/cypress/e2e/crypto/decryption-failure.spec.ts @@ -118,7 +118,12 @@ describe("Decryption Failure Bar", () => { "Verify this device to access all messages", ); - cy.percySnapshot("DecryptionFailureBar prompts user to verify"); + cy.get(".mx_DecryptionFailureBar").percySnapshotElement( + "DecryptionFailureBar prompts user to verify", + { + widths: [320, 640], + }, + ); cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); cy.contains(".mx_DecryptionFailureBar_button", "Verify").click(); @@ -146,8 +151,11 @@ describe("Decryption Failure Bar", () => { "Open another device to load encrypted messages", ); - cy.percySnapshot( + cy.get(".mx_DecryptionFailureBar").percySnapshotElement( "DecryptionFailureBar prompts user to open another device, with Resend Key Requests button", + { + widths: [320, 640], + }, ); cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest"); @@ -155,8 +163,11 @@ describe("Decryption Failure Bar", () => { cy.wait("@keyRequest"); cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); - cy.percySnapshot( - "DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button", + cy.get(".mx_DecryptionFailureBar").percySnapshotElement( + "DecryptionFailureBar prompts user to open another device, without Resend Key Requests button", + { + widths: [320, 640], + }, ); }, ); @@ -177,7 +188,9 @@ describe("Decryption Failure Bar", () => { "Reset your keys to prevent future decryption errors", ); - cy.percySnapshot("DecryptionFailureBar prompts user to reset keys"); + cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar prompts user to reset keys", { + widths: [320, 640], + }); cy.contains(".mx_DecryptionFailureBar_button", "Reset").click(); @@ -196,7 +209,12 @@ describe("Decryption Failure Bar", () => { "Some messages could not be decrypted", ); - cy.percySnapshot("DecryptionFailureBar displays general message with no call to action"); + cy.get(".mx_DecryptionFailureBar").percySnapshotElement( + "DecryptionFailureBar displays general message with no call to action", + { + widths: [320, 640], + }, + ); }, ); @@ -210,7 +228,10 @@ describe("Decryption Failure Bar", () => { cy.get(".mx_DecryptionFailureBar").should("exist"); cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist"); - cy.percySnapshot("DecryptionFailureBar displays loading spinner"); + cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar displays loading spinner", { + allowSpinners: true, + widths: [320, 640], + }); cy.wait(5000); cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist"); diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index 07a14533c70..b2537c2cbeb 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -20,7 +20,7 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { MatrixClient } from "../../global"; import Chainable = Cypress.Chainable; -const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }"; +const hidePercyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; describe("Polls", () => { let homeserver: HomeserverInstance; @@ -133,7 +133,7 @@ describe("Polls", () => { .as("pollId"); cy.get("@pollId").then((pollId) => { - getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS }); + getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hidePercyCSS }); // Bot votes 'Maybe' in the poll botVoteForOption(bot, roomId, pollId, pollParams.options[2]); diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index c6d2c298fed..2cae1a12184 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -356,7 +356,7 @@ describe("Sliding Sync", () => { }); // Regression test for https://github.com/vector-im/element-web/issues/21462 - it("should not cancel replies when permalinks are clicked ", () => { + it("should not cancel replies when permalinks are clicked", () => { cy.get("@roomId").then((roomId) => { // we require a first message as you cannot click the permalink text with the avatar in the way return cy diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index d9ead17bb3b..d8453b9d993 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -24,7 +24,7 @@ import Timeoutable = Cypress.Timeoutable; import Withinable = Cypress.Withinable; import Shadow = Cypress.Shadow; -export enum Filter { +enum Filter { People = "people", PublicRooms = "public_rooms", } @@ -297,27 +297,28 @@ describe("Spotlight", () => { // TODO: We currently can’t test finding rooms on other homeservers/other protocols // We obviously don’t have federation or bridges in cypress tests - /* - const room3Name = "Matrix HQ"; - const room3Id = "#matrix:matrix.org"; - - it("should find unknown public rooms on other homeservers", () => { - cy.openSpotlightDialog().within(() => { - cy.spotlightFilter(Filter.PublicRooms); - cy.spotlightSearch().clear().type(room3Name); - cy.get("[aria-haspopup=true][role=button]").click(); - }).then(() => { - cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org") - .next("[role=menuitemradio]") - .click(); - cy.wait(3_600_000); - }).then(() => cy.spotlightDialog().within(() => { - cy.spotlightResults().should("have.length", 1); - cy.spotlightResults().eq(0).should("contain", room3Name); - cy.spotlightResults().eq(0).should("contain", room3Id); - })); + it.skip("should find unknown public rooms on other homeservers", () => { + cy.openSpotlightDialog() + .within(() => { + cy.spotlightFilter(Filter.PublicRooms); + cy.spotlightSearch().clear().type(room3Name); + cy.get("[aria-haspopup=true][role=button]").click(); + }) + .then(() => { + cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org") + .next("[role=menuitemradio]") + .click(); + cy.wait(3_600_000); + }) + .then(() => + cy.spotlightDialog().within(() => { + cy.spotlightResults().should("have.length", 1); + cy.spotlightResults().eq(0).should("contain", room3Name); + cy.spotlightResults().eq(0).should("contain", room3Id); + }), + ); }); - */ + it("should find known people", () => { cy.openSpotlightDialog() .within(() => { diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index d946ad34dac..c1af15d2b07 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -18,6 +18,8 @@ limitations under the License. import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { MatrixClient } from "../../global"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { Layout } from "../../../src/settings/enums/Layout"; describe("Threads", () => { let homeserver: HomeserverInstance; @@ -54,9 +56,25 @@ describe("Threads", () => { cy.visit("/#/room/" + roomId); }); + // Around 200 characters + const MessageLong = + "Hello there. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt " + + "ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi"; + + // --MessageTimestamp-color = #acacac = rgb(172, 172, 172) + // See: _MessageTimestamp.pcss + const MessageTimestampColor = "rgb(172, 172, 172)"; + // User sends message cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + // Check the colour of timestamp on the main timeline + cy.get(".mx_RoomView_body .mx_EventTile_last .mx_EventTile_line .mx_MessageTimestamp").should( + "have.css", + "color", + MessageTimestampColor, + ); + // Wait for message to send, get its ID and save as @threadId cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") .invoke("attr", "data-scroll-tokens") @@ -65,7 +83,8 @@ describe("Threads", () => { // Bot starts thread cy.get("@threadId").then((threadId) => { bot.sendMessage(roomId, threadId, { - body: "Hello there", + // Send a message long enough to be wrapped to check if avatars inside the ReadReceiptGroup are visible + body: MessageLong, msgtype: "m.text", }); }); @@ -75,9 +94,41 @@ describe("Threads", () => { cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there"); cy.get(".mx_RoomView_body .mx_ThreadSummary").click(); + cy.get(".mx_ThreadView .mx_EventTile[data-layout='group'].mx_EventTile_last").within(() => { + // Wait until the messages are rendered + cy.get(".mx_EventTile_line .mx_MTextBody").should("have.text", MessageLong); + + // Make sure the avatar inside ReadReceiptGroup is visible on the group layout + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + }); + + // Enable the bubble layout + cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + + cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last").within(() => { + // TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout + // See: https://github.com/vector-im/element-web/issues/23569 + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("exist"); + + // Make sure the avatar inside ReadReceiptGroup is visible on bubble layout + // TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout + // See: https://github.com/vector-im/element-web/issues/23569 + // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + }); + + // Re-enable the group layout + cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); + // User responds in thread cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}"); + // Check the colour of timestamp on EventTile in a thread (mx_ThreadView) + cy.get(".mx_ThreadView .mx_EventTile_last[data-layout='group'] .mx_EventTile_line .mx_MessageTimestamp").should( + "have.css", + "color", + MessageTimestampColor, + ); + // User asserts summary was updated correctly cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test"); @@ -130,6 +181,10 @@ describe("Threads", () => { cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => { cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot"); cy.get(".mx_ThreadSummary_content").should("contain", "How are things?"); + + // Check the colour of timestamp on thread list + cy.get(".mx_EventTile_details .mx_MessageTimestamp").should("have.css", "color", MessageTimestampColor); + // User opens thread via threads list cy.get(".mx_EventTile_line").click(); }); diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index bef1cd0393c..3876e457b56 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -159,8 +159,8 @@ describe("Timeline", () => { ".mx_GenericEventListSummary_summary", "created and configured the room.", ).should("exist"); - cy.get(".mx_Spinner").should("not.exist"); - cy.percySnapshot("Configured room on IRC layout"); + + cy.get(".mx_MainSplit").percySnapshotElement("Configured room on IRC layout"); }); it("should add inline start margin to an event line on IRC layout", () => { @@ -179,20 +179,128 @@ describe("Timeline", () => { // Check the event line has margin instead of inset property // cf. _EventTile.pcss // --EventTile_irc_line_info-margin-inline-start - // = calc(var(--name-width) + 10px + var(--icon-width)) - // = 80 + 10 + 14 = 104px + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 5 = 99px + cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") - .should("have.css", "margin-inline-start", "104px") + .should("have.css", "margin-inline-start", "99px") .should("have.css", "inset-inline-start", "0px"); - cy.get(".mx_Spinner").should("not.exist"); - // Exclude timestamp from snapshot - const percyCSS = - ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }"; - cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS }); + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; + cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", { + percyCSS, + }); cy.checkA11y(); }); + it("should align generic event list summary with messages and emote on IRC layout", () => { + // This test aims to check: + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // 2. Alignment of expanded GELS and messages + // 3. Alignment of expanded GELS and placeholder of deleted message + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + + // Exclude timestamp from snapshot of mx_MainSplit + const percyCSS = ".mx_MainSplit .mx_MessageTimestamp { visibility: hidden !important; }"; + + 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.", + ).should("exist"); + + // Send messages + cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello again, Mr. Bot{enter}"); + // Make sure the second message was sent + cy.get(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); + + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // Check inline start spacing of collapsed GELS + // See: _EventTile.pcss + // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line + // = var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 2 * var(--right-padding) + // = 80 + 14 + 46 + 2 * 5 + // = 150px + cy.get(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line").should( + "have.css", + "padding-inline-start", + "150px", + ); + // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px + // --right-padding should be applied + cy.get(".mx_EventTile > *").should("have.css", "margin-right", "5px"); + // --name-width width zero inline end margin should be applied + cy.get(".mx_EventTile .mx_DisambiguatedProfile") + .should("have.css", "width", "80px") + .should("have.css", "margin-inline-end", "0px"); + // --icon-width should be applied + cy.get(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").should("have.css", "width", "14px"); + // $MessageTimestamp_width should be applied + cy.get(".mx_EventTile > a").should("have.css", "min-width", "46px"); + // Record alignment of collapsed GELS and messages on messagePanel + cy.get(".mx_MainSplit").percySnapshotElement("Collapsed GELS and messages on IRC layout", { percyCSS }); + + // 2. Alignment of expanded GELS and messages + // Click "expand" link button + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); + // Check inline start spacing of info line on expanded GELS + cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") + // See: _EventTile.pcss + // --EventTile_irc_line_info-margin-inline-start + // = 80 + 14 + 1 * 5 + .should("have.css", "margin-inline-start", "99px"); + // Record alignment of expanded GELS and messages on messagePanel + cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and messages on IRC layout", { percyCSS }); + + // 3. Alignment of expanded GELS and placeholder of deleted message + // Delete the second (last) message + cy.get(".mx_RoomView_MessageList > .mx_EventTile_last").realHover(); + cy.get(".mx_RoomView_MessageList > .mx_EventTile_last .mx_MessageActionBar_optionsButton", { + timeout: 1000, + }) + .should("exist") + .realHover() + .click({ force: false }); + cy.get(".mx_IconizedContextMenu_item[aria-label=Remove]").should("be.visible").click({ force: false }); + // Confirm deletion + cy.get(".mx_Dialog_buttons button[data-testid=dialog-primary-button]") + .should("have.text", "Remove") + .click({ force: false }); + // Make sure the dialog was closed and the second (last) message was redacted + cy.get(".mx_Dialog").should("not.exist"); + cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody").should("be.visible"); + cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); + // Record alignment of expanded GELS and placeholder of deleted message on messagePanel + cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and with placeholder of deleted message", { + percyCSS, + }); + + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + // Send a emote + cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("/me says hello to Mr. Bot{enter}"); + // Check inline start margin of its avatar + // Here --right-padding is for the avatar on the message line + // See: _IRCLayout.pcss + // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 1 * 5 + cy.get(".mx_EventTile_emote .mx_EventTile_avatar").should("have.css", "margin-left", "99px"); + // Make sure emote was sent + cy.get(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent").should("be.visible"); + // Record alignment of expanded GELS, placeholder of deleted message, and emote + cy.get(".mx_MainSplit").percySnapshotElement( + "Expanded GELS and with emote and placeholder of deleted message", + { + percyCSS, + }, + ); + }); + it("should set inline start padding to a hidden event line", () => { sendEvent(roomId); cy.visit("/#/room/" + roomId); @@ -212,9 +320,8 @@ describe("Timeline", () => { // Click timestamp to highlight hidden event line cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); - // Exclude timestamp from snapshot - const percyCSS = - ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }"; + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; // should not add inline start padding to a hidden event line on IRC layout cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); @@ -223,17 +330,30 @@ describe("Timeline", () => { "padding-inline-start", "0px", ); - cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS }); + + cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with zero padding on IRC layout", { + percyCSS, + }); // should add inline start padding to a hidden event line on modern layout cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line") // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px .should("have.css", "padding-inline-start", "84px"); - cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS }); + + cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", { + percyCSS, + }); }); - it("should click top left of view source event toggle", () => { + it("should click view source event toggle", () => { + // This test checks: + // 1. clickability of top left of view source event toggle + // 2. clickability of view source toggle on IRC layout + + // Exclude timestamp from snapshot + const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }"; + sendEvent(roomId); cy.visit("/#/room/" + roomId); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); @@ -249,8 +369,47 @@ describe("Timeline", () => { }); cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); + // 1. clickability of top left of view source event toggle + // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area - cy.get(".mx_EventTile:not(:first-child) .mx_ViewSourceEvent") + cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent") + .should("exist") + .realHover() + .within(() => { + cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false }); + }); + + // Make sure the expand toggle works + cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded") + .should("be.visible") + .realHover() + .within(() => { + cy.get(".mx_ViewSourceEvent_toggle") + // Check size and position of toggle on expanded view source event + // See: _ViewSourceEvent.pcss + .should("have.css", "height", "12px") // --ViewSourceEvent_toggle-size + .should("have.css", "align-self", "flex-end") + + // Click again to collapse the source + .click("topLeft", { force: false }); + }); + + // Make sure the collapse toggle works + cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded").should("not.exist"); + + // 2. clickability of view source toggle on IRC layout + + // Enable IRC layout + cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + + // Hover the view source toggle on IRC layout + cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent") + .should("exist") + .realHover() + .percySnapshotElement("Hovered hidden event line on IRC layout", { percyCSS }); + + // Click view source event toggle + cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent") .should("exist") .realHover() .within(() => { @@ -258,7 +417,7 @@ describe("Timeline", () => { }); // Make sure the expand toggle worked - cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible"); + cy.get(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded").should("be.visible"); }); it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => { @@ -329,7 +488,11 @@ describe("Timeline", () => { cy.wait("@mxc"); cy.checkA11y(); + + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", { + percyCSS, widths: [800, 400], }); }); diff --git a/cypress/e2e/widgets/stickers.spec.ts b/cypress/e2e/widgets/stickers.spec.ts index 5c016b406a9..27986af10e7 100644 --- a/cypress/e2e/widgets/stickers.spec.ts +++ b/cypress/e2e/widgets/stickers.spec.ts @@ -133,6 +133,7 @@ describe("Stickers", () => { type: "m.stickerpicker", name: STICKER_PICKER_WIDGET_NAME, url: stickerPickerUrl, + creatorUserId: "@userId", }, id: STICKER_PICKER_WIDGET_ID, }, diff --git a/cypress/support/percy.ts b/cypress/support/percy.ts index f5e30a58fcb..9183d5ebf6b 100644 --- a/cypress/support/percy.ts +++ b/cypress/support/percy.ts @@ -22,6 +22,7 @@ declare global { namespace Cypress { interface SnapshotOptions extends PercySnapshotOptions { domTransformation?: (documentClone: Document) => void; + allowSpinners?: boolean; } interface Chainable { @@ -38,6 +39,12 @@ declare global { } Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => { + if (!options?.allowSpinners) { + // Await spinners to vanish + cy.get(".mx_Spinner", { log: false }).should("not.exist"); + // But like really no more spinners please + cy.get(".mx_Spinner", { log: false }).should("not.exist"); + } cy.percySnapshot(name, { domTransformation: (documentClone) => scope(documentClone, subject.selector), ...options, diff --git a/package.json b/package.json index 3642474ee7d..40152a0a353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.67.0", + "version": "3.68.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -54,11 +54,15 @@ "test:cypress:open": "cypress open", "coverage": "yarn test --coverage" }, + "resolutions": { + "@types/react-dom": "17.0.19", + "@types/react": "17.0.53" + }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.4.0", + "@matrix-org/analytics-events": "^0.5.0", "@matrix-org/matrix-wysiwyg": "^1.1.1", - "@matrix-org/react-sdk-module-api": "^0.0.3", + "@matrix-org/react-sdk-module-api": "^0.0.4", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", @@ -75,7 +79,7 @@ "emojibase-regex": "6.0.1", "escape-html": "^1.0.3", "file-saver": "^2.0.5", - "filesize": "10.0.5", + "filesize": "10.0.6", "flux": "4.0.3", "focus-visible": "^5.2.0", "gfm.css": "^1.1.2", @@ -93,26 +97,26 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "23.4.0", + "matrix-js-sdk": "23.5.0", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.36.0", + "posthog-js": "1.50.3", "qrcode": "1.5.1", "re-resizable": "^6.9.0", "react": "17.0.2", "react-beautiful-dnd": "^13.1.0", - "react-blurhash": "^0.2.0", + "react-blurhash": "^0.3.0", "react-dom": "17.0.2", "react-focus-lock": "^2.5.1", "react-lite-youtube-embed": "^2.3.52", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "2.8.0", + "sanitize-html": "2.10.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", "url": "^0.11.0", @@ -144,12 +148,10 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.4.3", - "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", "@types/css-font-loading-module": "^0.0.7", "@types/diff-match-patch": "^1.0.32", - "@types/enzyme": "^3.10.9", "@types/escape-html": "^1.0.1", "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", @@ -165,9 +167,9 @@ "@types/pako": "^2.0.0", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.49", + "@types/react": "17.0.53", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.17", + "@types/react-dom": "17.0.19", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.8.0", "@types/tar-js": "^0.3.2", @@ -175,7 +177,6 @@ "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/parser": "^5.6.0", - "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "allchange": "^1.1.0", "axe-core": "4.4.3", "babel-jest": "^29.0.0", @@ -185,15 +186,14 @@ "cypress-axe": "^1.0.0", "cypress-multi-reporters": "^1.6.1", "cypress-real-events": "^1.7.1", - "enzyme": "^3.11.0", - "enzyme-to-json": "^3.6.2", - "eslint": "8.28.0", + "eslint": "8.35.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jest": "^27.2.1", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-matrix-org": "0.10.0", + "eslint-plugin-matrix-org": "1.1.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unicorn": "^45.0.0", @@ -210,20 +210,17 @@ "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", "postcss-scss": "^4.0.4", - "prettier": "2.8.0", + "prettier": "2.8.4", "raw-loader": "^4.0.2", - "rimraf": "^3.0.2", - "stylelint": "^14.9.1", + "rimraf": "^4.0.0", + "stylelint": "^15.0.0", "stylelint-config-prettier": "^9.0.4", - "stylelint-config-standard": "^29.0.0", + "stylelint-config-standard": "^30.0.0", "stylelint-scss": "^4.2.0", - "typescript": "4.9.3", + "typescript": "4.9.5", "walk": "^2.3.14" }, "jest": { - "snapshotSerializers": [ - "enzyme-to-json/serializer" - ], "testEnvironment": "jsdom", "testMatch": [ "/test/**/*-test.[jt]s?(x)" diff --git a/res/css/_components.pcss b/res/css/_components.pcss index f1f15287d5e..367684b9331 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -17,6 +17,7 @@ @import "./components/views/beacon/_ShareLatestLocation.pcss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss"; +@import "./components/views/dialogs/polls/_PollDetailHeader.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss"; @import "./components/views/dialogs/polls/_PollListItemEnded.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; @@ -135,7 +136,6 @@ @import "./views/dialogs/_FeedbackDialog.pcss"; @import "./views/dialogs/_ForwardDialog.pcss"; @import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss"; -@import "./views/dialogs/_HostSignupDialog.pcss"; @import "./views/dialogs/_IncomingSasDialog.pcss"; @import "./views/dialogs/_InviteDialog.pcss"; @import "./views/dialogs/_JoinRuleDropdown.pcss"; diff --git a/src/components/views/dialogs/IDialogProps.ts b/res/css/components/views/dialogs/polls/_PollDetailHeader.pcss similarity index 66% rename from src/components/views/dialogs/IDialogProps.ts rename to res/css/components/views/dialogs/polls/_PollDetailHeader.pcss index b294fdafe19..6f29b6e08fd 100644 --- a/src/components/views/dialogs/IDialogProps.ts +++ b/res/css/components/views/dialogs/polls/_PollDetailHeader.pcss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -export interface IDialogProps { - onFinished(...args: any): void; +.mx_PollDetailHeader { + // override accessiblebutton style + font-size: $font-15px !important; +} + +.mx_PollDetailHeader_icon { + height: 15px; + width: 15px; + margin-right: $spacing-8; + vertical-align: middle; } diff --git a/res/css/components/views/dialogs/polls/_PollListItem.pcss b/res/css/components/views/dialogs/polls/_PollListItem.pcss index 7b19e675943..d6036fa3787 100644 --- a/res/css/components/views/dialogs/polls/_PollListItem.pcss +++ b/res/css/components/views/dialogs/polls/_PollListItem.pcss @@ -16,12 +16,17 @@ limitations under the License. .mx_PollListItem { width: 100%; +} + +.mx_PollListItem_content { + width: 100%; display: grid; justify-content: left; align-items: center; grid-gap: $spacing-8; grid-template-columns: auto auto auto; grid-template-rows: auto; + cursor: pointer; color: $primary-content; } diff --git a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss index 6518052ab61..16ea5dcce07 100644 --- a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss +++ b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss @@ -16,9 +16,14 @@ limitations under the License. .mx_PollListItemEnded { width: 100%; +} + +.mx_PollListItemEnded_content { + width: 100%; display: flex; flex-direction: column; color: $primary-content; + cursor: pointer; } .mx_PollListItemEnded_title { diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index 434d8629e9b..177e367b58f 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -48,6 +48,9 @@ limitations under the License. align-items: center; } +/* See: mx_RoomHeader_button, of which this is a copy. + * TODO: factor out a common component to avoid this duplication. + */ .mx_RightPanel_headerButton { cursor: pointer; flex: 0 0 auto; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index ef5d47e1cd4..8536ff90bf1 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -111,12 +111,6 @@ $activeBorderColor: $primary-content; .mx_SpaceItem_new { position: relative; - - .mx_BetaDot { - position: absolute; - left: 33px; - top: -5px; - } } .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 6be33771af1..f9b399a3074 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -150,12 +150,8 @@ limitations under the License. justify-content: center; } - &.mx_UserMenu_contextMenu_guestPrompts, - &.mx_UserMenu_contextMenu_hostingLink { - padding-top: 0; - } - &.mx_UserMenu_contextMenu_guestPrompts { + padding-top: 0; display: inline-block; > span { @@ -198,10 +194,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/roomlist/dnd-cross.svg"); } - .mx_UserMenu_iconHosting::before { - mask-image: url("$(res)/img/element-icons/brands/element.svg"); - } - .mx_UserMenu_iconBell::before { mask-image: url("$(res)/img/element-icons/notifications.svg"); } diff --git a/res/css/views/audio_messages/_PlaybackContainer.pcss b/res/css/views/audio_messages/_PlaybackContainer.pcss index d2c101658c8..67b311e9674 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.pcss +++ b/res/css/views/audio_messages/_PlaybackContainer.pcss @@ -29,6 +29,11 @@ limitations under the License. contain: content; + .mx_Waveform, + .mx_RecordingPlayback_timelineLayoutMiddle { + min-width: 0; /* Prevent a blowout */ + } + /* Waveforms are present in live recording only */ .mx_Waveform { .mx_Waveform_bar { diff --git a/res/css/views/beta/_BetaCard.pcss b/res/css/views/beta/_BetaCard.pcss index 0f8d8a66e73..e4e4db01e56 100644 --- a/res/css/views/beta/_BetaCard.pcss +++ b/res/css/views/beta/_BetaCard.pcss @@ -15,8 +15,8 @@ limitations under the License. */ .mx_BetaCard { - margin-bottom: 20px; - padding: 24px; + margin-bottom: $spacing-20; + padding: $spacing-24; background-color: $system; border-radius: 8px; box-sizing: border-box; @@ -25,7 +25,7 @@ limitations under the License. .mx_BetaCard_columns { display: flex; flex-flow: wrap; - gap: 20px; + gap: $spacing-20; justify-content: center; .mx_BetaCard_columns_description { @@ -36,11 +36,11 @@ limitations under the License. font-size: $font-18px; line-height: $font-22px; color: $primary-content; - margin: 4px 0 14px; + margin: $spacing-4 0 14px; // TODO: use a spacing variable display: flex; align-items: center; - column-gap: 12px; + column-gap: $spacing-12; } .mx_BetaCard_caption { @@ -78,7 +78,7 @@ limitations under the License. line-height: $font-15px; > h4 { - margin: 12px 0 0; + margin: $spacing-12 0 0; } > p { @@ -102,13 +102,13 @@ limitations under the License. .mx_BetaCard_relatedSettings { .mx_SettingsFlag { - margin: 16px 0 0; + margin: $spacing-16 0 0; font-size: $font-15px; line-height: $font-24px; color: $primary-content; .mx_SettingsFlag_microcopy { - margin-top: 4px; + margin-top: $spacing-4; font-size: $font-12px; line-height: $font-15px; } @@ -122,10 +122,10 @@ limitations under the License. .mx_BetaCard_betaPill { background-color: $accent-alt; - padding: 4px 10px; + padding: $spacing-4 10px; // TODO: use a spacing variable border-radius: 8px; text-transform: uppercase; - font-size: 12px; + font-size: $font-12px; font-weight: $font-semi-bold; line-height: 15px; color: $button-primary-fg-color; @@ -137,64 +137,3 @@ limitations under the License. cursor: pointer; } } - -$pulse-color: $accent-alt; -$dot-size: 12px; - -.mx_BetaDot { - border-radius: 50%; - margin: 10px; - height: $dot-size; - width: $dot-size; - transform: scale(1); - background: rgba($pulse-color, 1); - animation: mx_Beta_bluePulse 2s infinite; - animation-iteration-count: 20; - position: relative; - pointer-events: none; - - &::after { - content: ""; - position: absolute; - width: inherit; - height: inherit; - top: 0; - left: 0; - transform: scale(1); - transform-origin: center center; - animation-name: mx_Beta_bluePulse_shadow; - animation-duration: inherit; - animation-iteration-count: inherit; - border-radius: 50%; - background: rgba($pulse-color, 1); - } -} - -@keyframes mx_Beta_bluePulse { - 0% { - transform: scale(0.95); - } - - 70% { - transform: scale(1); - } - - 100% { - transform: scale(0.95); - } -} - -@keyframes mx_Beta_bluePulse_shadow { - 0% { - opacity: 0.7; - } - - 70% { - transform: scale(2.2); - opacity: 0; - } - - 100% { - opacity: 0; - } -} diff --git a/res/css/views/dialogs/_HostSignupDialog.pcss b/res/css/views/dialogs/_HostSignupDialog.pcss deleted file mode 100644 index 15c383f1e84..00000000000 --- a/res/css/views/dialogs/_HostSignupDialog.pcss +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_HostSignupDialog { - width: 90vw; - max-width: 580px; - height: 80vh; - max-height: 600px; - /* Ensure dialog borders are always white as the HostSignupDialog */ - /* does not yet support dark mode or theming in general. */ - /* In the future we might want to pass the theme to the called */ - /* iframe, should some hosting provider have that need. */ - background-color: #ffffff; - - .mx_HostSignupDialog_info { - text-align: center; - - .mx_HostSignupDialog_content_top { - margin-bottom: 24px; - } - - .mx_HostSignupDialog_paragraphs { - text-align: left; - padding-left: 25%; - padding-right: 25%; - } - - .mx_HostSignupDialog_buttons { - margin-bottom: 24px; - display: flex; - justify-content: center; - - button { - padding: 12px; - margin: 0 16px; - } - } - - .mx_HostSignupDialog_footer { - display: flex; - justify-content: center; - align-items: baseline; - - img { - padding-right: 5px; - } - } - } - - iframe { - width: 100%; - height: 100%; - border: none; - background-color: #fff; - min-height: 540px; - } -} - -.mx_HostSignupDialog_text_dark { - color: $primary-content; -} - -.mx_HostSignupDialog_text_light { - color: $secondary-content; -} - -.mx_HostSignup_maximize_button { - mask: url("$(res)/img/element-icons/maximise-expand.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; - width: 14px; - height: 14px; - background-color: $dialog-close-fg-color; - cursor: pointer; - position: absolute; - top: 10px; - right: 10px; -} - -.mx_HostSignup_minimize_button { - mask: url("$(res)/img/element-icons/minimise-collapse.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; - width: 14px; - height: 14px; - background-color: $dialog-close-fg-color; - cursor: pointer; - position: absolute; - top: 10px; - right: 25px; -} - -.mx_HostSignupDialog_minimized { - position: fixed; - bottom: 80px; - right: 26px; - width: 314px; - height: 217px; - overflow: hidden; - - &.mx_Dialog { - padding: 12px; - } - - .mx_Dialog_title { - text-align: left !important; - padding-left: 20px; - font-size: $font-15px; - } - - iframe { - width: 100%; - height: 100%; - border: none; - background-color: #fff; - } -} diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss index 581d4f1de03..d2db7fa163d 100644 --- a/res/css/views/dialogs/_InviteDialog.pcss +++ b/res/css/views/dialogs/_InviteDialog.pcss @@ -452,3 +452,8 @@ limitations under the License. .mx_InviteDialog_identityServer { margin-top: 1em; /* TODO: Use a spacing variable */ } + +.mx_InviteDialog_oneThreepid { + font-size: $font-12px; + margin: $spacing-8 0; +} diff --git a/res/css/views/dialogs/polls/_PollHistoryList.pcss b/res/css/views/dialogs/polls/_PollHistoryList.pcss index ee6f0254f71..be6ca7423af 100644 --- a/res/css/views/dialogs/polls/_PollHistoryList.pcss +++ b/res/css/views/dialogs/polls/_PollHistoryList.pcss @@ -41,10 +41,20 @@ limitations under the License. .mx_PollHistoryList_noResults { height: 100%; width: 100%; + box-sizing: border-box; + padding: 0 $spacing-64; display: flex; + flex-direction: column; align-items: center; justify-content: center; + text-align: center; + + line-height: $font-24px; color: $secondary-content; + + .mx_PollHistoryList_loadMorePolls { + margin-top: $spacing-16; + } } .mx_PollHistoryList_loading { @@ -57,3 +67,7 @@ limitations under the License. margin: auto auto; } } + +.mx_PollHistoryList_loadMorePolls { + width: max-content; +} diff --git a/res/css/views/elements/_CopyableText.pcss b/res/css/views/elements/_CopyableText.pcss index 8e1d3f3cfd7..edd1cdf7163 100644 --- a/res/css/views/elements/_CopyableText.pcss +++ b/res/css/views/elements/_CopyableText.pcss @@ -23,11 +23,12 @@ limitations under the License. max-width: 100%; &.mx_CopyableText_border { + overflow: auto; border-radius: 5px; border: solid 1px $light-fg-color; margin-bottom: 10px; margin-top: 10px; - padding: 10px; + padding: 10px 0 10px 10px; } .mx_CopyableText_copyButton { @@ -36,7 +37,8 @@ limitations under the License. width: 1em; height: 1em; cursor: pointer; - margin-left: 20px; + padding-left: 12px; + padding-right: 10px; display: block; /* If the copy button is used within a scrollable div, make it stick to the right while scrolling */ position: sticky; diff --git a/res/css/views/elements/_ReplyChain.pcss b/res/css/views/elements/_ReplyChain.pcss index a84f04547fc..b404f643940 100644 --- a/res/css/views/elements/_ReplyChain.pcss +++ b/res/css/views/elements/_ReplyChain.pcss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_ReplyChain { - margin: 0 0 $spacing-8 0; + margin: 0; // Reset default blockquote margin padding-left: 10px; // TODO: Use a spacing variable border-left: 2px solid var(--username-color); // TODO: Use a spacing variable diff --git a/res/css/views/messages/_LegacyCallEvent.pcss b/res/css/views/messages/_LegacyCallEvent.pcss index 873f5d4b5b1..8d0f5abbc30 100644 --- a/res/css/views/messages/_LegacyCallEvent.pcss +++ b/res/css/views/messages/_LegacyCallEvent.pcss @@ -160,6 +160,7 @@ limitations under the License. flex-wrap: wrap; align-items: center; color: $secondary-content; + font-size: $font-12px; gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */ margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */ word-break: break-word; @@ -168,6 +169,7 @@ limitations under the License. .mx_LegacyCallEvent_content_button { @mixin LegacyCallButton; padding: 0 $spacing-12; + font-size: inherit; span::before { mask-size: 16px; diff --git a/res/css/views/messages/_MessageTimestamp.pcss b/res/css/views/messages/_MessageTimestamp.pcss index e4e1a475ec8..1b94ad3a6f1 100644 --- a/res/css/views/messages/_MessageTimestamp.pcss +++ b/res/css/views/messages/_MessageTimestamp.pcss @@ -16,8 +16,9 @@ limitations under the License. .mx_MessageTimestamp { --MessageTimestamp-max-width: 80px; + --MessageTimestamp-color: $event-timestamp-color; - color: $event-timestamp-color; + color: var(--MessageTimestamp-color); font-size: $font-10px; font-variant-numeric: tabular-nums; display: block; /* enable the width setting below */ diff --git a/res/css/views/messages/_ViewSourceEvent.pcss b/res/css/views/messages/_ViewSourceEvent.pcss index 63698a13719..725c8341887 100644 --- a/res/css/views/messages/_ViewSourceEvent.pcss +++ b/res/css/views/messages/_ViewSourceEvent.pcss @@ -29,32 +29,34 @@ limitations under the License. pre { line-height: 1.2; - margin: 3.5px 0; + margin: 3.5px 0; /* TODO: use a variable */ } .mx_ViewSourceEvent_toggle { + --ViewSourceEvent_toggle-size: 12px; + visibility: hidden; /* override styles from AccessibleButton */ border-radius: 0; /* icon */ mask-repeat: no-repeat; mask-position: 0 center; - mask-size: auto 12px; - width: 12px; - min-width: 12px; + mask-size: auto var(--ViewSourceEvent_toggle-size); + width: var(--ViewSourceEvent_toggle-size); + min-width: var(--ViewSourceEvent_toggle-size); background-color: $accent; mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); + + .mx_EventTile:hover & { + visibility: visible; + } } &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { + align-self: flex-end; + height: var(--ViewSourceEvent_toggle-size); mask-position: 0 bottom; - margin-bottom: 5px; + margin-bottom: 5px; /* TODO: use a variable */ mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); } } - -.mx_EventTile:hover { - .mx_ViewSourceEvent_toggle { - visibility: visible; - } -} diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 683b576d2a6..a1ad8298804 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -130,7 +130,7 @@ limitations under the License. } .mx_MessageTimestamp { - color: $event-timestamp-color; + color: var(--MessageTimestamp-color); /* TODO: check whether needed or not */ } .mx_BaseCard_footer { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index f577f4836dc..050f164a050 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -267,10 +267,18 @@ $left-gutter: 64px; .mx_EventTileBubble { margin-inline: auto; } + + .mx_ReplyChain { + margin-bottom: $spacing-8; + } } &[data-layout="irc"] { - --EventTile_irc_line_info-margin-inline-start: calc(var(--name-width) + 10px + var(--icon-width)); + /* add --right-padding value of MessageTimestamp only */ + /* stylelint-disable-next-line declaration-colon-space-after */ + --EventTile_irc_line_info-margin-inline-start: calc( + var(--name-width) + var(--icon-width) + 1 * var(--right-padding) + ); .mx_EventTile_msgOption { .mx_ReadReceiptGroup { @@ -291,6 +299,10 @@ $left-gutter: 64px; } } + .mx_ReplyChain { + margin: 0; + } + .mx_ReplyTile .mx_EventTileBubble { left: unset; /* Cancel the value specified above for the tile inside ReplyTile */ } @@ -483,20 +495,12 @@ $left-gutter: 64px; } &[data-layout="irc"] { - .mx_EventTile_line .mx_RedactedBody { - padding-left: 24px; /* 25px - 1px */ - - &::before { - left: var(--right-padding); - } - } - /* Apply only collapsed events block */ > .mx_EventTile_line { - /* 15 px of padding */ + /* add --right-padding value of MessageTimestamp and avatar only */ /* stylelint-disable-next-line declaration-colon-space-after */ padding-left: calc( - var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 3 * var(--right-padding) + var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 2 * var(--right-padding) ); } } @@ -1260,6 +1264,10 @@ $left-gutter: 64px; padding-block: var(--MatrixChat_useCompactLayout_line-spacing-block); } + .mx_ReplyChain { + margin-bottom: $spacing-4; + } + &.mx_EventTile_info { padding-top: 0; /* same as the padding for non-compact .mx_EventTile.mx_EventTile_info */ font-size: $font-13px; diff --git a/res/css/views/rooms/_IRCLayout.pcss b/res/css/views/rooms/_IRCLayout.pcss index 02ae02fa5eb..ed27991839f 100644 --- a/res/css/views/rooms/_IRCLayout.pcss +++ b/res/css/views/rooms/_IRCLayout.pcss @@ -130,12 +130,17 @@ $irc-line-height: $font-18px; .mx_TextualEvent, .mx_ViewSourceEvent, .mx_MTextBody { - display: inline-block; /* add a 1px padding top and bottom because our larger emoji font otherwise gets cropped by anti-zalgo */ padding: var(--EventTile_irc_line-padding-block) 0; } + .mx_EventTile_e2eIcon, + .mx_TextualEvent, + .mx_MTextBody { + display: inline-block; + } + .mx_ReplyTile { .mx_MTextBody { display: -webkit-box; /* Enable -webkit-line-clamp */ @@ -154,7 +159,8 @@ $irc-line-height: $font-18px; .mx_EventTile_emote { .mx_EventTile_avatar { - margin-left: calc(var(--name-width) + var(--icon-width) + var(--right-padding)); + /* add --right-padding value of MessageTimestamp only */ + margin-left: calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)); } } @@ -177,7 +183,6 @@ $irc-line-height: $font-18px; } .mx_ReplyChain { - margin: 0; .mx_DisambiguatedProfile { order: unset; width: unset; diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 6410a1e5574..7a21629dd12 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -201,12 +201,13 @@ limitations under the License. } .mx_RoomHeader_button { - position: relative; + cursor: pointer; + flex: 0 0 auto; margin-left: 1px; margin-right: 1px; - cursor: pointer; height: 32px; width: 32px; + position: relative; border-radius: 100%; &::before { diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index 51a213192ca..a2c66202a4e 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -88,17 +88,17 @@ limitations under the License. border-radius: 2px; } - code { + code:not(pre *) { font-family: $monospace-font-family !important; background-color: $inlinecode-background-color; border: 1px solid $inlinecode-border-color; border-radius: 4px; padding: $spacing-2; - } - code:empty { - border: unset; - padding: unset; + &:empty { + border: unset; + padding: unset; + } } } diff --git a/res/css/views/settings/_IntegrationManager.pcss b/res/css/views/settings/_IntegrationManager.pcss index f91d3fdd6c9..505ccf86c24 100644 --- a/res/css/views/settings/_IntegrationManager.pcss +++ b/res/css/views/settings/_IntegrationManager.pcss @@ -17,6 +17,7 @@ limitations under the License. .mx_IntegrationManager { .mx_Dialog { box-sizing: border-box; + padding: 0; width: 60%; height: 70%; overflow: hidden; diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss index 5a61bcd2dab..c03de9f36ce 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.pcss @@ -28,9 +28,4 @@ limitations under the License. margin-bottom: $spacing-16; } } - - /* prevent the access token from overflowing the text box */ - div .mx_CopyableText { - overflow: scroll; - } } diff --git a/scripts/make-react-component.js b/scripts/make-react-component.js index 544a4f4f311..40eb331785a 100755 --- a/scripts/make-react-component.js +++ b/scripts/make-react-component.js @@ -31,19 +31,19 @@ const %%ComponentName%%: React.FC = () => { export default %%ComponentName%%; `, TEST: ` -import React from 'react'; -import { mount } from 'enzyme'; +import React from "react"; +import { render } from "@testing-library/react"; import %%ComponentName%% from '%%RelativeComponentPath%%'; -describe('<%%ComponentName%% />', () => { +describe("<%%ComponentName%% />", () => { const defaultProps = {}; const getComponent = (props = {}) => - mount(<%%ComponentName%% {...defaultProps} {...props} />); + render(<%%ComponentName%% {...defaultProps} {...props} />); - it('renders', () => { - const component = getComponent(); - expect(component).toBeTruthy(); + it("matches snapshot", () => { + const { asFragment } = getComponent(); + expect(asFragment()).toMatchSnapshot()(); }); }); `, diff --git a/src/@types/common.ts b/src/@types/common.ts index ddcd0bb3dc6..be77708a82e 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -48,16 +48,6 @@ export type RecursivePartial = { : T[P]; }; -// Inspired by https://stackoverflow.com/a/60206860 -export type KeysWithObjectShape = { - [P in keyof Input]: Input[P] extends object - ? // Arrays are counted as objects - exclude them - Input[P] extends Array - ? never - : P - : never; -}[keyof Input]; - export type KeysStartingWith = { // eslint-disable-next-line @typescript-eslint/no-unused-vars [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 5d8d947854d..51779042f9c 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -49,7 +49,7 @@ export type Binding = { */ export default class AddThreepid { private sessionId: string; - private submitUrl: string; + private submitUrl?: string; private clientSecret: string; private bind: boolean; @@ -93,7 +93,7 @@ export default class AddThreepid { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { // For separate bind, request a token directly from the IS. const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); + const identityAccessToken = (await authClient.getAccessToken()) ?? undefined; return MatrixClientPeg.get() .requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken) .then( @@ -155,7 +155,7 @@ export default class AddThreepid { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { // For separate bind, request a token directly from the IS. const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); + const identityAccessToken = (await authClient.getAccessToken()) ?? undefined; return MatrixClientPeg.get() .requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken) .then( @@ -184,7 +184,7 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> { + public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> { try { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (this.bind) { @@ -202,7 +202,7 @@ export default class AddThreepid { // The spec has always required this to use UI auth but synapse briefly // implemented it without, so this may just succeed and that's OK. - return; + return [true]; } catch (e) { if (e.httpStatus !== 401 || !e.data || !e.data.flows) { // doesn't look like an interactive-auth failure @@ -213,8 +213,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), body: _t( - "Confirm adding this email address by using " + - "Single Sign On to prove your identity.", + "Confirm adding this email address by using Single Sign On to prove your identity.", ), continueText: _t("Single Sign On"), continueKind: "primary", @@ -226,19 +225,16 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog<[boolean, IAuthData | Error | null]>( - InteractiveAuthDialog, - { - title: _t("Add Email Address"), - matrixClient: MatrixClientPeg.get(), - authData: e.data, - makeRequest: this.makeAddThreepidOnlyRequest, - aestheticsForStagePhases: { - [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, - [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, - }, + const { finished } = Modal.createDialog(InteractiveAuthDialog, { + title: _t("Add Email Address"), + matrixClient: MatrixClientPeg.get(), + authData: e.data, + makeRequest: this.makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, }, - ); + }); return finished; } } @@ -260,6 +256,7 @@ export default class AddThreepid { } throw err; } + return []; } /** @@ -282,7 +279,7 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async haveMsisdnToken(msisdnToken: string): Promise { + public async haveMsisdnToken(msisdnToken: string): Promise { const authClient = new IdentityAuthClient(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); @@ -333,7 +330,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), body: _t( - "Confirm adding this phone number by using " + "Single Sign On to prove your identity.", + "Confirm adding this phone number by using Single Sign On to prove your identity.", ), continueText: _t("Single Sign On"), continueKind: "primary", diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index e4e12bedfe5..4ee10ca22db 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -18,16 +18,16 @@ import React, { ComponentType } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "./languageHandler"; -import { IDialogProps } from "./components/views/dialogs/IDialogProps"; import BaseDialog from "./components/views/dialogs/BaseDialog"; import DialogButtons from "./components/views/elements/DialogButtons"; import Spinner from "./components/views/elements/Spinner"; type AsyncImport = { default: T }; -interface IProps extends IDialogProps { +interface IProps { // A promise which resolves with the real component - prom: Promise>; + prom: Promise | AsyncImport>>; + onFinished(): void; } interface IState { @@ -71,7 +71,7 @@ export default class AsyncWrapper extends React.Component { } private onWrapperCancelClick = (): void => { - this.props.onFinished(false); + this.props.onFinished(); }; public render(): React.ReactNode { diff --git a/src/Avatar.ts b/src/Avatar.ts index 5036b8f2561..d58420deefe 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -139,14 +139,18 @@ export function getInitialLetter(name: string): string | undefined { export function avatarUrlForRoom( room: Room | null, - width: number, - height: number, + width?: number, + height?: number, resizeMethod?: ResizeMethod, ): string | null { if (!room) return null; // null-guard if (room.getMxcAvatarUrl()) { - return mediaFromMxc(room.getMxcAvatarUrl() || undefined).getThumbnailOfSourceHttp(width, height, resizeMethod); + const media = mediaFromMxc(room.getMxcAvatarUrl() ?? undefined); + if (width !== undefined && height !== undefined) { + return media.getThumbnailOfSourceHttp(width, height, resizeMethod); + } + return media.srcHttp; } // space rooms cannot be DMs so skip the rest @@ -160,7 +164,11 @@ export function avatarUrlForRoom( // If there are only two members in the DM use the avatar of the other member const otherMember = room.getAvatarFallbackMember(); if (otherMember?.getMxcAvatarUrl()) { - return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + const media = mediaFromMxc(otherMember.getMxcAvatarUrl()); + if (width !== undefined && height !== undefined) { + return media.getThumbnailOfSourceHttp(width, height, resizeMethod); + } + return media.srcHttp; } return null; } diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index e05858bb166..41bd0361a2a 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -389,7 +389,7 @@ export default class ContentMessages { } if (tooBigFiles.length > 0) { - const { finished } = Modal.createDialog<[boolean]>(UploadFailureDialog, { + const { finished } = Modal.createDialog(UploadFailureDialog, { badFiles: tooBigFiles, totalFiles: files.length, contentMessages: this, @@ -407,7 +407,7 @@ export default class ContentMessages { const loopPromiseBefore = promBefore; if (!uploadAll) { - const { finished } = Modal.createDialog<[boolean, boolean]>(UploadConfirmDialog, { + const { finished } = Modal.createDialog(UploadConfirmDialog, { file, currentIndex: i, totalFiles: okFiles.length, @@ -546,7 +546,7 @@ export default class ContentMessages { if (upload.cancelled) throw new UploadCanceledError(); const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; - const response = await matrixClient.sendMessage(roomId, threadId, content); + const response = await matrixClient.sendMessage(roomId, threadId ?? null, content); if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { sendRoundTripMetric(matrixClient, roomId, response.event_id); diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 1db73cc0745..b2e44f23ce2 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -148,19 +148,6 @@ export interface IConfigOptions { analytics_owner?: string; // defaults to `brand` privacy_policy_url?: string; // location for cookie policy - // Server hosting upsell options - hosting_signup_link?: string; // slightly different from `host_signup` - host_signup?: { - brand?: string; // acts as the enabled flag too (truthy == show) - - // Required-ness denotes when `brand` is truthy - cookie_policy_url: string; - privacy_policy_url: string; - terms_of_service_url: string; - url: string; - domains?: string[]; - }; - enable_presence_by_hs_url?: Record; // terms_and_conditions_links?: { url: string; text: string }[]; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 34fffffc505..708db7a32d3 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -58,7 +58,7 @@ import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts import ToastStore from "./stores/ToastStore"; import Resend from "./Resend"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; -import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes"; +import { InviteKind } from "./components/views/dialogs/InviteDialogTypes"; import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload"; import { findDMForUser } from "./utils/dm/findDMForUser"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; @@ -737,7 +737,7 @@ export default class LegacyCallHandler extends EventEmitter { ); if (!stats) { logger.debug( - "Call statistics are undefined. The call has " + "probably failed before a peerConn was established", + "Call statistics are undefined. The call has probably failed before a peerConn was established", ); return; } @@ -1024,13 +1024,12 @@ export default class LegacyCallHandler extends EventEmitter { } public answerCall(roomId: string): void { - const call = this.calls.get(roomId); - - this.stopRingingIfPossible(call.callId); - // no call to answer if (!this.calls.has(roomId)) return; + const call = this.calls.get(roomId)!; + this.stopRingingIfPossible(call.callId); + if (this.getAllActiveCalls().length > 1) { Modal.createDialog(ErrorDialog, { title: _t("Too Many Calls"), @@ -1215,7 +1214,7 @@ export default class LegacyCallHandler extends EventEmitter { call.setRemoteOnHold(true); dis.dispatch({ action: Action.OpenInviteDialog, - kind: KIND_CALL_TRANSFER, + kind: InviteKind.CallTransfer, call, analyticsName: "Transfer Call", className: "mx_InviteDialog_transferWrapper", diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index b28a7f80380..590b7eb1801 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -265,7 +265,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { .then(() => { const lazyLoadEnabled = e.value; if (lazyLoadEnabled) { - return new Promise((resolve) => { + return new Promise((resolve) => { Modal.createDialog(LazyLoadingResyncDialog, { onFinished: resolve, }); @@ -275,7 +275,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { // between LL/non-LL version on same host. // as disabling LL when previously enabled // is a strong indicator of this (/develop & /app) - return new Promise((resolve) => { + return new Promise((resolve) => { Modal.createDialog(LazyLoadingDisabledDialog, { onFinished: resolve, host: window.location.host, @@ -287,7 +287,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { return MatrixClientPeg.get().store.deleteAllData(); }) .then(() => { - PlatformPeg.get().reload(); + PlatformPeg.get()?.reload(); }); } } @@ -519,7 +519,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise The available media devices */ - public static async getDevices(): Promise { + public static async getDevices(): Promise { try { const devices = await navigator.mediaDevices.enumerateDevices(); const output: Record = { diff --git a/src/Modal.tsx b/src/Modal.tsx index 3b21c47d3d0..54ae185ad14 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -27,34 +27,35 @@ import AsyncWrapper from "./AsyncWrapper"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -export interface IModal { +// Type which accepts a React Component which looks like a Modal (accepts an onFinished prop) +export type ComponentType = React.ComponentType<{ + onFinished?(...args: any): void; +}>; + +// Generic type which returns the props of the Modal component with the onFinished being optional. +export type ComponentProps = Omit, "onFinished"> & + Partial, "onFinished">>; + +export interface IModal { elem: React.ReactNode; className?: string; beforeClosePromise?: Promise; closeReason?: string; onBeforeClose?(reason?: string): Promise; - onFinished?(...args: T): void; - close(...args: T): void; + onFinished: ComponentProps["onFinished"]; + close(...args: Parameters["onFinished"]>): void; hidden?: boolean; } -export interface IHandle { - finished: Promise; - close(...args: T): void; -} - -interface IProps { - onFinished?(...args: T): void; - // TODO improve typing here once all Modals are TS and we can exhaustively check the props - [key: string]: any; +export interface IHandle { + finished: Promise["onFinished"]>>; + close(...args: Parameters["onFinished"]>): void; } -interface IOptions { - onBeforeClose?: IModal["onBeforeClose"]; +interface IOptions { + onBeforeClose?: IModal["onBeforeClose"]; } -type ParametersWithoutFirst any> = T extends (a: any, ...args: infer P) => any ? P : never; - export enum ModalManagerEvent { Opened = "opened", } @@ -111,18 +112,30 @@ export class ModalManager extends TypedEventEmitter 0; } - public createDialog( - Element: React.ComponentType, - ...rest: ParametersWithoutFirst - ): IHandle { - return this.createDialogAsync(Promise.resolve(Element), ...rest); + public createDialog( + Element: C, + props?: ComponentProps, + className?: string, + isPriorityModal = false, + isStaticModal = false, + options: IOptions = {}, + ): IHandle { + return this.createDialogAsync( + Promise.resolve(Element), + props, + className, + isPriorityModal, + isStaticModal, + options, + ); } - public appendDialog( + public appendDialog( Element: React.ComponentType, - ...rest: ParametersWithoutFirst - ): IHandle { - return this.appendDialogAsync(Promise.resolve(Element), ...rest); + props?: ComponentProps, + className?: string, + ): IHandle { + return this.appendDialogAsync(Promise.resolve(Element), props, className); } public closeCurrentModal(reason: string): void { @@ -134,15 +147,15 @@ export class ModalManager extends TypedEventEmitter( + private buildModal( prom: Promise, - props?: IProps, + props?: ComponentProps, className?: string, - options?: IOptions, + options?: IOptions, ): { - modal: IModal; - closeDialog: IHandle["close"]; - onFinishedProm: IHandle["finished"]; + modal: IModal; + closeDialog: IHandle["close"]; + onFinishedProm: IHandle["finished"]; } { const modal = { onFinished: props?.onFinished, @@ -151,10 +164,10 @@ export class ModalManager extends TypedEventEmitter; + } as IModal; // never call this from onFinished() otherwise it will loop - const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); + const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. @@ -168,13 +181,13 @@ export class ModalManager extends TypedEventEmitter( - modal: IModal, - props?: IProps, - ): [IHandle["close"], IHandle["finished"]] { - const deferred = defer(); + private getCloseFn( + modal: IModal, + props?: ComponentProps, + ): [IHandle["close"], IHandle["finished"]] { + const deferred = defer["onFinished"]>>(); return [ - async (...args: T): Promise => { + async (...args: Parameters["onFinished"]>): Promise => { if (modal.beforeClosePromise) { await modal.beforeClosePromise; } else if (modal.onBeforeClose) { @@ -249,16 +262,16 @@ export class ModalManager extends TypedEventEmitter( - prom: Promise, - props?: IProps, + public createDialogAsync( + prom: Promise, + props?: ComponentProps, className?: string, isPriorityModal = false, isStaticModal = false, - options: IOptions = {}, - ): IHandle { + options: IOptions = {}, + ): IHandle { const beforeModal = this.getCurrentModal(); - const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, options); + const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, options); if (isPriorityModal) { // XXX: This is destructive this.priorityModal = modal; @@ -278,13 +291,13 @@ export class ModalManager extends TypedEventEmitter( + private appendDialogAsync( prom: Promise, - props?: IProps, + props?: ComponentProps, className?: string, - ): IHandle { + ): IHandle { const beforeModal = this.getCurrentModal(); - const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, {}); + const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, {}); this.modals.push(modal); diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index a82b78c1dd2..c03a20216c1 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -54,8 +54,8 @@ export default class PosthogTrackers { } private view: Views = Views.LOADING; - private pageType?: PageType = null; - private override?: ScreenName = null; + private pageType?: PageType; + private override?: ScreenName; public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void { this.view = view; @@ -66,7 +66,7 @@ export default class PosthogTrackers { private trackPage(durationMs?: number): void { const screenName = - this.view === Views.LOGGED_IN ? loggedInPageTypeMap[this.pageType] : notLoggedInMap[this.view]; + this.view === Views.LOGGED_IN ? loggedInPageTypeMap[this.pageType!] : notLoggedInMap[this.view]; PosthogAnalytics.instance.trackEvent({ eventName: "$pageview", $current_url: screenName, @@ -85,7 +85,7 @@ export default class PosthogTrackers { public clearOverride(screenName: ScreenName): void { if (screenName !== this.override) return; - this.override = null; + this.override = undefined; this.trackPage(); } diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index c92ebcc55e2..0d2b64f244f 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ComponentProps } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { User } from "matrix-js-sdk/src/models/user"; @@ -29,7 +29,7 @@ import InviteDialog from "./components/views/dialogs/InviteDialog"; import BaseAvatar from "./components/views/avatars/BaseAvatar"; import { mediaFromMxc } from "./customisations/Media"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; -import { KIND_DM, KIND_INVITE } from "./components/views/dialogs/InviteDialogTypes"; +import { InviteKind } from "./components/views/dialogs/InviteDialogTypes"; import { Member } from "./utils/direct-messages"; export interface IInviteResult { @@ -64,7 +64,7 @@ export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createDialog( InviteDialog, - { kind: KIND_DM, initialText }, + { kind: InviteKind.Dm, initialText }, /*className=*/ "mx_InviteDialog_flexWrapper", /*isPriority=*/ false, /*isStatic=*/ true, @@ -76,10 +76,10 @@ export function showRoomInviteDialog(roomId: string, initialText = ""): void { Modal.createDialog( InviteDialog, { - kind: KIND_INVITE, + kind: InviteKind.Invite, initialText, roomId, - }, + } as Omit, "onFinished">, /*className=*/ "mx_InviteDialog_flexWrapper", /*isPriority=*/ false, /*isStatic=*/ true, diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 0ba1d4f46e4..ff4e63b22a9 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -19,14 +19,12 @@ import { Optional } from "matrix-events-sdk"; import { SnakedObject } from "./utils/SnakedObject"; import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions"; -import { KeysWithObjectShape } from "./@types/common"; // see element-web config.md for docs, or the IConfigOptions interface for dev docs export const DEFAULTS: IConfigOptions = { brand: "SchildiChat", 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: { @@ -79,10 +77,10 @@ export default class SdkConfig { return SdkConfig.fallback.get(key, altCaseName); } - public static getObject>( + public static getObject( key: K, altCaseName?: string, - ): Optional> { + ): Optional>> { const val = SdkConfig.get(key, altCaseName); if (val !== null && val !== undefined) { return new SnakedObject(val); diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 2a56de87513..563aac09ad0 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -22,13 +22,13 @@ import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib"; import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { logger } from "matrix-js-sdk/src/logger"; -import { ComponentType } from "react"; +import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog"; import Modal from "./Modal"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { _t } from "./languageHandler"; import { isSecureBackupRequired } from "./utils/WellKnownUtils"; -import AccessSecretStorageDialog from "./components/views/dialogs/security/AccessSecretStorageDialog"; +import AccessSecretStorageDialog, { KeyParams } from "./components/views/dialogs/security/AccessSecretStorageDialog"; import RestoreKeyBackupDialog from "./components/views/dialogs/security/RestoreKeyBackupDialog"; import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; @@ -83,8 +83,6 @@ async function confirmToDismiss(): Promise { return !sure; } -type KeyParams = { passphrase: string; recoveryKey: string }; - function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise { return async ({ passphrase, recoveryKey }): Promise => { if (passphrase) { @@ -333,7 +331,7 @@ export async function accessSecretStorage(func = async (): Promise => {}, // passphrase creation. const { finished } = Modal.createDialogAsync( import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise< - ComponentType<{}> + typeof CreateSecretStorageDialog >, { forceReset, diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 66f673445dd..b1df460f803 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -198,7 +198,7 @@ function reject(error?: any): RunResult { return { error }; } -function success(promise?: Promise): RunResult { +function success(promise: Promise = Promise.resolve()): RunResult { return { promise }; } @@ -221,7 +221,7 @@ export const Commands = [ command: "spoiler", args: "", description: _td("Sends the given message as a spoiler"), - runFn: function (roomId, message) { + runFn: function (roomId, message = "") { return successSync(ContentHelpers.makeHtmlMessage(message, `${message}`)); }, category: CommandCategories.messages, @@ -282,7 +282,7 @@ export const Commands = [ command: "plain", args: "", description: _td("Sends a message as plain text, without interpreting it as markdown"), - runFn: function (roomId, messages) { + runFn: function (roomId, messages = "") { return successSync(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages, @@ -291,7 +291,7 @@ export const Commands = [ command: "html", args: "", description: _td("Sends a message as html, without interpreting it as markdown"), - runFn: function (roomId, messages) { + runFn: function (roomId, messages = "") { return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages, @@ -551,7 +551,7 @@ export const Commands = [ ) { const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); if (defaultIdentityServerUrl) { - const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("Use an identity server"), description: (

diff --git a/src/Terms.ts b/src/Terms.ts index f66f543887c..ad4386d7aa0 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -191,7 +191,7 @@ export async function dialogTermsInteractionCallback( ): Promise { logger.log("Terms that need agreement", policiesAndServicePairs); - const { finished } = Modal.createDialog<[boolean, string[]]>( + const { finished } = Modal.createDialog( TermsDialog, { policiesAndServicePairs, diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 3011a5b5bd7..bcd720ee21b 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -154,7 +154,7 @@ export enum KeyBindingAction { ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility", } -type KeyboardShortcutSetting = IBaseSetting; +type KeyboardShortcutSetting = Omit, "supportedLevels">; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager export type IKeyboardShortcuts = Partial>; diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index 300e67df825..a1e9485e027 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -27,7 +27,7 @@ import { Action } from "../../../../dispatcher/actions"; import { SettingLevel } from "../../../../settings/SettingLevel"; interface IProps { - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; } interface IState { diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 5393ae3fc6e..517a56d23b3 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -27,17 +27,18 @@ import { SettingLevel } from "../../../../settings/SettingLevel"; import Field from "../../../../components/views/elements/Field"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; -import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import { IIndexStats } from "../../../../indexing/BaseEventIndexManager"; -interface IProps extends IDialogProps {} +interface IProps { + onFinished(): void; +} interface IState { eventIndexSize: number; eventCount: number; crawlingRoomsCount: number; roomCount: number; - currentRoom: string; + currentRoom: string | null; crawlerSleepTime: number; } @@ -60,7 +61,8 @@ export default class ManageEventIndexDialog extends React.Component => { const eventIndex = EventIndexPeg.get(); - let stats: IIndexStats; + if (!eventIndex) return; + let stats: IIndexStats | undefined; try { stats = await eventIndex.getStats(); @@ -70,7 +72,7 @@ export default class ManageEventIndexDialog extends React.Component => { const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default; - Modal.createDialog(DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true); + Modal.createDialog(DisableEventIndexDialog, undefined, undefined, /* priority = */ false, /* static = */ true); }; private onCrawlerSleepTimeChange = (e: ChangeEvent): void => { @@ -157,11 +161,9 @@ export default class ManageEventIndexDialog extends React.Component - {_t( - "%(brand)s is securely caching encrypted messages locally for them " + - "to appear in search results:", - { brand }, - )} + {_t("%(brand)s is securely caching encrypted messages locally for them to appear in search results:", { + brand, + })}

{crawlerState}
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index a9327f2a467..ec8cf31f30f 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -26,7 +26,6 @@ import { accessSecretStorage } from "../../../../SecurityManager"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import { copyNode } from "../../../../utils/strings"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; -import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import Field from "../../../../components/views/elements/Field"; import Spinner from "../../../../components/views/elements/Spinner"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; @@ -45,10 +44,12 @@ enum Phase { const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -interface IProps extends IDialogProps {} +interface IProps { + onFinished(done?: boolean): void; +} interface IState { - secureSecretStorage: boolean; + secureSecretStorage: boolean | null; phase: Phase; passPhrase: string; passPhraseValid: boolean; @@ -120,7 +121,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { this.setState({ - passPhraseValid: result.valid, + passPhraseValid: !!result.valid, }); }; @@ -305,7 +306,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 1f2e7fd6b98..3e487398261 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -43,7 +43,6 @@ import { SecureBackupSetupMethod, } from "../../../../utils/WellKnownUtils"; import SecurityCustomisations from "../../../../customisations/Security"; -import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import Field from "../../../../components/views/elements/Field"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import Spinner from "../../../../components/views/elements/Spinner"; @@ -67,10 +66,11 @@ enum Phase { const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -interface IProps extends IDialogProps { - hasCancel: boolean; - accountPassword: string; - forceReset: boolean; +interface IProps { + hasCancel?: boolean; + accountPassword?: string; + forceReset?: boolean; + onFinished(ok?: boolean): void; } interface IState { @@ -81,13 +81,13 @@ interface IState { copied: boolean; downloaded: boolean; setPassphrase: boolean; - backupInfo: IKeyBackupInfo; - backupSigStatus: TrustInfo; + backupInfo: IKeyBackupInfo | null; + backupSigStatus: TrustInfo | null; // does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? - canUploadKeysWithPasswordOnly: boolean; + canUploadKeysWithPasswordOnly: boolean | null; accountPassword: string; - accountPasswordCorrect: boolean; + accountPasswordCorrect: boolean | null; canSkip: boolean; passPhraseKeySelected: string; error?: string; @@ -119,7 +119,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { + private async fetchBackupInfo(): Promise<{ backupInfo?: IKeyBackupInfo; backupSigStatus?: TrustInfo }> { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupSigStatus = // we may not have started crypto yet, in which case we definitely don't trust the backup - MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)); + backupInfo && MatrixClientPeg.get().isCryptoEnabled() + ? await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) + : null; const { forceReset } = this.props; const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase; @@ -189,17 +191,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { try { - await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys); + await MatrixClientPeg.get().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); // We should never get here: the server should always require // UI auth to upload device signing keys. If we do, we upload // no keys which would be a no-op. @@ -248,7 +251,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { e.preventDefault(); - if (this.state.backupSigStatus.usable) { + if (this.state.backupSigStatus?.usable) { this.bootstrapSecretStorage(); } else { this.restoreBackup(); @@ -265,7 +268,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - const blob = new Blob([this.recoveryKey.encodedPrivateKey], { + const blob = new Blob([this.recoveryKey.encodedPrivateKey!], { type: "text/plain;charset=us-ascii", }); FileSaver.saveAs(blob, "security-key.txt"); @@ -323,7 +326,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { this.setState({ phase: Phase.Storing, - error: null, + error: undefined, }); const cli = MatrixClientPeg.get(); @@ -351,7 +354,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey, - keyBackupInfo: this.state.backupInfo, + keyBackupInfo: this.state.backupInfo!, setupNewKeyBackup: !this.state.backupInfo, getKeyBackupPassphrase: async (): Promise => { // We may already have the backup key if we earlier went @@ -399,14 +402,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ - passPhraseValid: result.valid, + passPhraseValid: !!result.valid, }); }; @@ -581,13 +584,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent
); - } else if (!this.state.backupSigStatus.usable) { + } else if (!this.state.backupSigStatus?.usable) { authPrompt = (
{_t("Restore your key backup to upgrade your encryption")}
@@ -612,7 +615,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent
- {audioFeedArraysForCalls} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d4eb7e28a9b..d622776482f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentType, createRef } from "react"; +import React, { createRef } from "react"; import { ClientEvent, createClient, @@ -38,6 +38,8 @@ import "focus-visible"; // what-input helps improve keyboard accessibility import "what-input"; +import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog"; +import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; import PosthogTrackers from "../../PosthogTrackers"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; @@ -141,6 +143,7 @@ import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/Spotli import { findDMForUser } from "../../utils/dm/findDMForUser"; import { Linkify } from "../../HtmlUtils"; import { NotificationColor } from "../../stores/notifications/NotificationColor"; +import { UserTab } from "../views/dialogs/UserTab"; // legacy export export { default as Views } from "../../Views"; @@ -227,8 +230,6 @@ export default class MatrixChat extends React.PureComponent { private screenAfterLogin?: IScreen; private tokenLogin?: boolean; - private accountPassword?: string; - private accountPasswordTimer?: number; private focusComposer: boolean; private subTitleStatus: string; private prevWindowWidth: number; @@ -297,9 +298,6 @@ export default class MatrixChat extends React.PureComponent { Lifecycle.loadSession(); } - this.accountPassword = null; - this.accountPasswordTimer = null; - this.dispatcherRef = dis.register(this.onAction); this.themeWatcher = new ThemeWatcher(); @@ -440,7 +438,7 @@ export default class MatrixChat extends React.PureComponent { this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); - if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); + this.stores.accountPasswordStore.clearPassword(); if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy(); } @@ -711,7 +709,7 @@ export default class MatrixChat extends React.PureComponent { const tabPayload = payload as OpenToTabPayload; Modal.createDialog( UserSettingsDialog, - { initialTabId: tabPayload.initialTabId }, + { initialTabId: tabPayload.initialTabId as UserTab }, /*className=*/ null, /*isPriority=*/ false, /*isStatic=*/ true, @@ -1658,14 +1656,14 @@ export default class MatrixChat extends React.PureComponent { Modal.createDialogAsync( import( "../../async-components/views/dialogs/security/NewRecoveryMethodDialog" - ) as unknown as Promise>, + ) as unknown as Promise, { newVersionInfo }, ); } else { Modal.createDialogAsync( import( "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog" - ) as unknown as Promise>, + ) as unknown as Promise, ); } }); @@ -2011,13 +2009,7 @@ export default class MatrixChat extends React.PureComponent { * this, as they instead jump straight into the app after `attemptTokenLogin`. */ private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise => { - this.accountPassword = password; - // self-destruct the password after 5mins - if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); - this.accountPasswordTimer = window.setTimeout(() => { - this.accountPassword = null; - this.accountPasswordTimer = null; - }, 60 * 5 * 1000); + this.stores.accountPasswordStore.setPassword(password); // Create and start the client await Lifecycle.setLoggedIn(credentials); @@ -2047,7 +2039,7 @@ export default class MatrixChat extends React.PureComponent { public render(): React.ReactNode { const fragmentAfterLogin = this.getFragmentAfterLogin(); - let view = null; + let view: JSX.Element; if (this.state.view === Views.LOADING) { view = ( @@ -2061,7 +2053,7 @@ export default class MatrixChat extends React.PureComponent { view = ( ); @@ -2164,6 +2156,7 @@ export default class MatrixChat extends React.PureComponent { view = => this.onShowPostLoginScreen(useCase)} />; } else { logger.error(`Unknown view ${this.state.view}`); + return null; } return ( diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index cda85e2a72d..166b5008f48 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -272,7 +272,14 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.RoomSummary: - card = ; + card = ( + + ); break; case RightPanelPhases.Widget: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a281dd70057..bc925cc692e 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -116,8 +116,13 @@ import { RoomSearchView } from "./RoomSearchView"; import eventSearch from "../../Searching"; import VoipUserMapper from "../../VoipUserMapper"; import { isCallEvent } from "./LegacyCallEventGrouper"; +import { WidgetType } from "../../widgets/WidgetType"; +import WidgetUtils from "../../utils/WidgetUtils"; +import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite"; +import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView"; const DEBUG = false; +const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; let debuglog = function (msg: string): void {}; const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe"); @@ -234,6 +239,7 @@ export interface IRoomState { } interface LocalRoomViewProps { + localRoom: LocalRoom; resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; roomView: RefObject; @@ -249,7 +255,7 @@ interface LocalRoomViewProps { function LocalRoomView(props: LocalRoomViewProps): ReactElement { const context = useContext(RoomContext); const room = context.room as LocalRoom; - const encryptionEvent = context.room.currentState.getStateEvents(EventType.RoomEncryption)[0]; + const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; let encryptionTile: ReactNode; if (encryptionEvent) { @@ -264,8 +270,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { }); }; - let statusBar: ReactElement; - let composer: ReactElement; + let statusBar: ReactElement | null = null; + let composer: ReactElement | null = null; if (room.isError) { const buttons = ( @@ -284,7 +290,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { } else { composer = ( @@ -296,7 +302,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { { private roomView = createRef(); private searchResultsPanel = createRef(); - private messagePanel: TimelinePanel; + private messagePanel?: TimelinePanel; private roomViewBody = createRef(); public static contextType = SDKContext; @@ -385,15 +391,19 @@ export class RoomView extends React.Component { public constructor(props: IRoomProps, context: React.ContextType) { super(props, context); + if (!context.client) { + throw new Error("Unable to create RoomView without MatrixClient"); + } + const llMembers = context.client.hasLazyLoadMembersEnabled(); this.state = { - roomId: null, + roomId: undefined, roomLoading: true, peekLoading: false, shouldPeek: true, membersLoaded: !llMembers, numUnreadMessages: 0, - callState: null, + callState: undefined, activeCall: null, canPeek: false, canSelfRedact: false, @@ -515,6 +525,7 @@ export class RoomView extends React.Component { private onWidgetStoreUpdate = (): void => { if (!this.state.room) return; this.checkWidgets(this.state.room); + this.doMaybeRemoveOwnJitsiWidget(); }; private onWidgetEchoStoreUpdate = (): void => { @@ -535,6 +546,56 @@ export class RoomView extends React.Component { this.checkWidgets(this.state.room); }; + /** + * Removes the Jitsi widget from the current user if + * - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN} + * - The last (server timestamp) of these widgets is from the currrent user + * This solves the issue if some people decide to start a conference and click the call button at the same time. + */ + private doMaybeRemoveOwnJitsiWidget(): void { + if (!this.state.roomId || !this.state.room || !this.context.client) return; + + const apps = this.context.widgetStore.getApps(this.state.roomId); + const jitsiApps = apps.filter((app) => app.eventId && WidgetType.JITSI.matches(app.type)); + + // less than two Jitsi widgets → nothing to do + if (jitsiApps.length < 2) return; + + const currentUserId = this.context.client.getSafeUserId(); + const createdByCurrentUser = jitsiApps.find((apps) => apps.creatorUserId === currentUserId); + + // no Jitsi widget from current user → nothing to do + if (!createdByCurrentUser) return; + + const createdByCurrentUserEvent = this.state.room.findEventById(createdByCurrentUser.eventId!); + + // widget event not found → nothing can be done + if (!createdByCurrentUserEvent) return; + + const createdByCurrentUserTs = createdByCurrentUserEvent.getTs(); + + // widget timestamp is empty → nothing can be done + if (!createdByCurrentUserTs) return; + + const lastCreatedByOtherTs = jitsiApps.reduce((maxByNow: number, app) => { + if (app.eventId === createdByCurrentUser.eventId) return maxByNow; + + const appCreateTs = this.state.room!.findEventById(app.eventId!)?.getTs() || 0; + return Math.max(maxByNow, appCreateTs); + }, 0); + + // last widget timestamp from other is empty → nothing can be done + if (!lastCreatedByOtherTs) return; + + if ( + createdByCurrentUserTs > lastCreatedByOtherTs && + createdByCurrentUserTs - lastCreatedByOtherTs < PREVENT_MULTIPLE_JITSI_WITHIN + ) { + // more than one Jitsi widget with the last one from the current user → remove it + WidgetUtils.setRoomWidget(this.state.roomId, createdByCurrentUser.id); + } + } + private checkWidgets = (room: Room): void => { this.setState({ hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room), @@ -1976,10 +2037,11 @@ export class RoomView extends React.Component { ); } - private renderLocalRoomView(): ReactElement { + private renderLocalRoomView(localRoom: LocalRoom): ReactElement { return ( { ); } + private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactElement { + return ( + + + + ); + } + public render(): React.ReactNode { if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { return this.renderLocalRoomCreateLoader(); } - return this.renderLocalRoomView(); + return this.renderLocalRoomView(this.state.room); + } + + if (this.state.room) { + const { shouldEncrypt, inviteEvent } = shouldEncryptRoomWithSingle3rdPartyInvite(this.state.room); + + if (shouldEncrypt) { + return this.renderWaitingForThirdPartyRoomView(inviteEvent); + } } if (!this.state.room) { @@ -2013,6 +2095,7 @@ export class RoomView extends React.Component { loading={loading} joining={this.state.joining} oobData={this.props.oobData} + roomId={this.state.roomId} /> @@ -2042,7 +2125,7 @@ export class RoomView extends React.Component { invitedEmail={invitedEmail} oobData={this.props.oobData} signUrl={this.props.threepidInvite?.signUrl} - room={this.state.room} + roomId={this.state.roomId} /> @@ -2079,6 +2162,7 @@ export class RoomView extends React.Component { error={this.state.roomLoadError} joining={this.state.joining} rejecting={this.state.rejecting} + roomId={this.state.roomId} /> ); @@ -2108,6 +2192,7 @@ export class RoomView extends React.Component { canPreview={false} joining={this.state.joining} room={this.state.room} + roomId={this.state.roomId} /> @@ -2200,6 +2285,7 @@ export class RoomView extends React.Component { oobData={this.props.oobData} canPreview={this.state.canPeek} room={this.state.room} + roomId={this.state.roomId} /> ); if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 238eaa18611..d466af5a510 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -332,6 +332,7 @@ const Tile: React.FC = ({
  • } - filterPlaceholder={_t("Search for rooms or spaces")} + filterPlaceholder={_t("Search for rooms")} onFinished={onFinished} roomsRenderer={defaultRoomsRenderer} dmsRenderer={defaultDmsRenderer} diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 0fca785cf3c..73770a51df2 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1501,9 +1501,7 @@ class TimelinePanel extends React.Component { "do not have permission to view the message in question.", ); } else { - description = _t( - "Tried to load a specific point in this room's timeline, but was " + "unable to find it.", - ); + description = _t("Tried to load a specific point in this room's timeline, but was unable to find it."); } Modal.createDialog(ErrorDialog, { diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 77bb39bf009..03bbdc72585 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -42,7 +42,6 @@ import IconizedContextMenu, { IconizedContextMenuOptionList, } from "../views/context_menus/IconizedContextMenu"; import { UIFeature } from "../../settings/UIFeature"; -import HostSignupAction from "./HostSignupAction"; import SpaceStore from "../../stores/spaces/SpaceStore"; import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import { Theme } from "../../settings/enums/Theme"; @@ -251,7 +250,6 @@ export default class UserMenu extends React.Component { if (!this.state.contextMenuPosition) return null; let topSection; - const hostSignupConfig = SdkConfig.getObject("host_signup"); if (MatrixClientPeg.get().isGuest()) { topSection = (
    @@ -279,15 +277,6 @@ export default class UserMenu extends React.Component { )}
    ); - } else if (hostSignupConfig?.get("url")) { - // If hostSignup.domains is set to a non-empty array, only show - // dialog if the user is on the domain or a subdomain. - const hostSignupDomains = hostSignupConfig.get("domains") || []; - const mxDomain = MatrixClientPeg.get().getDomain(); - const validDomains = hostSignupDomains.filter((d) => d === mxDomain || mxDomain.endsWith(`.${d}`)); - if (!hostSignupConfig.get("domains") || validDomains.length > 0) { - topSection = ; - } } let homeButton = null; @@ -397,7 +386,7 @@ export default class UserMenu extends React.Component { public render(): React.ReactNode { const avatarSize = 32; // should match border-radius of the avatar - const userId = MatrixClientPeg.get().getUserId(); + const userId = MatrixClientPeg.get().getSafeUserId(); const displayName = OwnProfileStore.instance.displayName || userId; const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index faf445cef5a..f804eb2ce81 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -23,15 +23,15 @@ import { _t } from "../../languageHandler"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { canEditContent } from "../../utils/EventUtils"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { IDialogProps } from "../views/dialogs/IDialogProps"; import BaseDialog from "../views/dialogs/BaseDialog"; import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool"; import { StateEventEditor } from "../views/dialogs/devtools/RoomState"; import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event"; import CopyableText from "../views/elements/CopyableText"; -interface IProps extends IDialogProps { +interface IProps { mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu + onFinished(): void; } interface IState { @@ -139,15 +139,15 @@ export default class ViewSource extends React.Component { private canSendStateEvent(mxEvent: MatrixEvent): boolean { const cli = MatrixClientPeg.get(); const room = cli.getRoom(mxEvent.getRoomId()); - return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); + return !!room?.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); } public render(): React.ReactNode { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isEditing = this.state.isEditing; - const roomId = mxEvent.getRoomId(); - const eventId = mxEvent.getId(); + const roomId = mxEvent.getRoomId()!; + const eventId = mxEvent.getId()!; const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); return ( diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx new file mode 100644 index 00000000000..b747fd007f8 --- /dev/null +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -0,0 +1,82 @@ +/* +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. +*/ + +import React from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { RefObject } from "react"; + +import { useRoomContext } from "../../contexts/RoomContext"; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import { E2EStatus } from "../../utils/ShieldUtils"; +import ErrorBoundary from "../views/elements/ErrorBoundary"; +import RoomHeader from "../views/rooms/RoomHeader"; +import ScrollPanel from "./ScrollPanel"; +import EventTileBubble from "../views/messages/EventTileBubble"; +import NewRoomIntro from "../views/rooms/NewRoomIntro"; +import { UnwrappedEventTile } from "../views/rooms/EventTile"; +import { _t } from "../../languageHandler"; + +interface Props { + roomView: RefObject; + resizeNotifier: ResizeNotifier; + inviteEvent: MatrixEvent; +} + +/** + * Component that displays a waiting room for an encrypted DM with a third party invite. + * If encryption by default is enabled, DMs with a third party invite should be encrypted as well. + * To avoid UTDs, users are shown a waiting room until the others have joined. + */ +export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resizeNotifier, inviteEvent }) => { + const context = useRoomContext(); + + return ( +
    + + +
    +
    + + + + + +
    +
    +
    +
    + ); +}; diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index c3a6201252b..d7a51956f4d 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -368,7 +368,7 @@ export default class ForgotPassword extends React.Component { } public async renderConfirmLogoutDevicesDialog(): Promise { - const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("Warning!"), description: (
    diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 243d56cee1a..97f3866f965 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -39,6 +39,7 @@ import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import { filterBoolean } from "../../../utils/arrays"; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -120,15 +121,11 @@ export default class LoginComponent extends React.PureComponent this.state = { busy: false, - busyLoggingIn: null, errorText: null, loginIncorrect: false, canTryLogin: true, - flows: null, - username: props.defaultUsername ? props.defaultUsername : "", - phoneCountry: null, phoneNumber: "", serverIsAlive: true, @@ -167,7 +164,7 @@ export default class LoginComponent extends React.PureComponent } } - public isBusy = (): boolean => this.state.busy || this.props.busy; + public isBusy = (): boolean => !!this.state.busy || !!this.props.busy; public onPasswordLogin: OnPasswordLogin = async ( username: string | undefined, @@ -349,7 +346,7 @@ export default class LoginComponent extends React.PureComponent ev.preventDefault(); ev.stopPropagation(); const ssoKind = ssoFlow.type === "m.login.sso" ? "sso" : "cas"; - PlatformPeg.get().startSingleSignOn( + PlatformPeg.get()?.startSingleSignOn( this.loginLogic.createTemporaryClient(), ssoKind, this.props.fragmentAfterLogin, @@ -457,7 +454,7 @@ export default class LoginComponent extends React.PureComponent } let errorText: ReactNode = - _t("There was a problem communicating with the homeserver, " + "please try again later.") + + _t("There was a problem communicating with the homeserver, please try again later.") + (errCode ? " (" + errCode + ")" : ""); if (err instanceof ConnectionError) { @@ -511,13 +508,13 @@ export default class LoginComponent extends React.PureComponent return errorText; } - public renderLoginComponentForFlows(): JSX.Element { + public renderLoginComponentForFlows(): ReactNode { if (!this.state.flows) return null; // this is the ideal order we want to show the flows in const order = ["m.login.password", "m.login.sso"]; - const flows = order.map((type) => this.state.flows.find((flow) => flow.type === type)).filter(Boolean); + const flows = filterBoolean(order.map((type) => this.state.flows.find((flow) => flow.type === type))); return ( {flows.map((flow) => { diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 196b3904177..dd9be190b2a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -78,9 +78,9 @@ interface IProps { } interface IState { + // true if we're waiting for the user to complete busy: boolean; errorText?: ReactNode; - // true if we're waiting for the user to complete // We remember the values entered by the user because // the registration form will be unmounted during the // course of registration, but if there's an error we @@ -88,7 +88,7 @@ interface IState { // values the user entered still in it. We can keep // them in this component's state since this component // persist for the duration of the registration process. - formVals: Record; + formVals: Record; // user-interactive auth // If we've been given a session ID, we're resuming // straight back into UI auth @@ -96,9 +96,11 @@ interface IState { // If set, we've registered but are not going to log // the user in to their new account automatically. completedNoSignin: boolean; - flows: { - stages: string[]; - }[]; + flows: + | { + stages: string[]; + }[] + | null; // We perform liveliness checks later, but for now suppress the errors. // We also track the server dead errors independently of the regular errors so // that we can render it differently, and override any other error the user may @@ -158,7 +160,7 @@ export default class Registration extends React.Component { window.removeEventListener("beforeunload", this.unloadCallback); } - private unloadCallback = (event: BeforeUnloadEvent): string => { + private unloadCallback = (event: BeforeUnloadEvent): string | undefined => { if (this.state.doingUIAuth) { event.preventDefault(); event.returnValue = ""; @@ -215,7 +217,7 @@ export default class Registration extends React.Component { this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setIdentityServerUrl(isUrl); - let ssoFlow: ISSOFlow; + let ssoFlow: ISSOFlow | undefined; try { const loginFlows = await this.loginLogic.getFlows(); if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us @@ -289,6 +291,7 @@ export default class Registration extends React.Component { sendAttempt: number, sessionId: string, ): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); return this.state.matrixClient.requestRegisterEmailToken( emailAddress, clientSecret, @@ -303,6 +306,8 @@ export default class Registration extends React.Component { }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); + debuglog("Registration: ui authentication finished: ", { success, response }); if (!success) { let errorText: ReactNode = (response as Error).message || (response as Error).toString(); @@ -327,10 +332,8 @@ export default class Registration extends React.Component {
    ); } else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) { - let msisdnAvailable = false; - for (const flow of (response as IAuthData).available_flows) { - msisdnAvailable = msisdnAvailable || flow.stages.includes(AuthType.Msisdn); - } + const flows = (response as IAuthData).available_flows ?? []; + const msisdnAvailable = flows.some((flow) => flow.stages.includes(AuthType.Msisdn)); if (!msisdnAvailable) { errorText = _t("This server does not support authentication with a phone number."); } @@ -348,12 +351,16 @@ export default class Registration extends React.Component { return; } - MatrixClientPeg.setJustRegisteredUserId((response as IAuthData).user_id); + const userId = (response as IAuthData).user_id; + const accessToken = (response as IAuthData).access_token; + if (!userId || !accessToken) throw new Error("Registration failed"); + + MatrixClientPeg.setJustRegisteredUserId(userId); const newState: Partial = { doingUIAuth: false, registeredUsername: (response as IAuthData).user_id, - differentLoggedInUserId: null, + differentLoggedInUserId: undefined, completedNoSignin: false, // we're still busy until we get unmounted: don't show the registration form again busy: true, @@ -393,13 +400,13 @@ export default class Registration extends React.Component { // the email, not the client that started the registration flow await this.props.onLoggedIn( { - userId: (response as IAuthData).user_id, + userId, deviceId: (response as IAuthData).device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken: (response as IAuthData).access_token, + accessToken, }, - this.state.formVals.password, + this.state.formVals.password!, ); this.setupPushers(); @@ -457,6 +464,8 @@ export default class Registration extends React.Component { }; private makeRegisterRequest = (auth: IAuthData | null): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); + const registerParams: IRegisterRequestParams = { username: this.state.formVals.username, password: this.state.formVals.password, @@ -494,7 +503,7 @@ export default class Registration extends React.Component { return sessionLoaded; }; - private renderRegisterComponent(): JSX.Element { + private renderRegisterComponent(): ReactNode { if (this.state.matrixClient && this.state.doingUIAuth) { return ( { ); - } else if (this.state.flows.length) { - let ssoSection; + } else if (this.state.matrixClient && this.state.flows.length) { + let ssoSection: JSX.Element | undefined; if (this.state.ssoFlow) { let continueWithSection; const providers = this.state.ssoFlow.identity_providers || []; @@ -571,6 +580,8 @@ export default class Registration extends React.Component { ); } + + return null; } public render(): React.ReactNode { diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index d1de4ba4b3f..72792a19990 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../../views/elements/AccessibleButton"; @@ -26,7 +26,8 @@ import { ErrorMessage } from "../../ErrorMessage"; interface Props { email: string; - errorText: string | null; + errorText: ReactNode | null; + onFinished(): void; // This modal is weird in that the way you close it signals intent onCloseClick: () => void; onReEnterEmailClick: () => void; onResendClick: () => Promise; diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index a865f0aeef2..4f2fddc4ab8 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -60,7 +60,7 @@ export default class PlayPauseButton extends React.PureComponent { return ( {this.state.errorText}; } diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 2731503e383..0426c08f867 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -50,12 +50,12 @@ class EmailField extends PureComponent { { key: "required", test: ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t(this.props.labelRequired), + invalid: () => _t(this.props.labelRequired!), }, { key: "email", test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t(this.props.labelInvalid), + invalid: () => _t(this.props.labelInvalid!), }, ], }); @@ -80,7 +80,7 @@ class EmailField extends PureComponent { id={this.props.id} ref={this.props.fieldRef} type="text" - label={_t(this.props.label)} + label={_t(this.props.label!)} value={this.props.value} autoFocus={this.props.autoFocus} onChange={this.props.onChange} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 79f777ce5d3..8910b91945c 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -83,7 +83,7 @@ export const DEFAULT_PHASE = 0; interface IAuthEntryProps { matrixClient: MatrixClient; loginType: string; - authSessionId: string; + authSessionId?: string; errorText?: string; errorCode?: string; // Is the auth logic currently waiting for something to happen? @@ -120,7 +120,7 @@ export class PasswordAuthEntry extends React.Component = {}; const pickedPolicies: { @@ -300,12 +300,12 @@ export class TermsAuthEntry extends React.Component e !== "version"); - langPolicy = policy[firstLang]; + langPolicy = firstLang ? policy[firstLang] : undefined; } if (!langPolicy) throw new Error("Failed to find a policy to show the user"); @@ -358,7 +358,7 @@ export class TermsAuthEntry extends React.Component; } - const checkboxes = []; + const checkboxes: JSX.Element[] = []; let allChecked = true; for (const policy of this.state.policies) { const checked = this.state.toggledPolicies[policy.id]; @@ -384,7 +384,7 @@ export class TermsAuthEntry extends React.Component { public static LOGIN_TYPE = AuthType.Msisdn; - private submitUrl: string; + private submitUrl?: string; private sid: string; private msisdn: string; @@ -798,11 +798,13 @@ export class SSOAuthEntry extends React.Component {_t("Cancel")} @@ -909,7 +911,7 @@ export class SSOAuthEntry extends React.Component { - private popupWindow: Window; + private popupWindow: Window | null; private fallbackButton = createRef(); public constructor(props: IAuthEntryProps) { @@ -927,18 +929,16 @@ export class FallbackAuthEntry extends React.Component { public componentWillUnmount(): void { window.removeEventListener("message", this.onReceiveMessage); - if (this.popupWindow) { - this.popupWindow.close(); - } + this.popupWindow?.close(); } public focus = (): void => { - if (this.fallbackButton.current) { - this.fallbackButton.current.focus(); - } + this.fallbackButton.current?.focus(); }; private onShowFallbackClick = (e: MouseEvent): void => { + if (!this.props.authSessionId) return; + e.preventDefault(); e.stopPropagation(); diff --git a/src/components/views/auth/LanguageSelector.tsx b/src/components/views/auth/LanguageSelector.tsx index 3eee9940662..b7816ea2967 100644 --- a/src/components/views/auth/LanguageSelector.tsx +++ b/src/components/views/auth/LanguageSelector.tsx @@ -26,7 +26,7 @@ import LanguageDropdown from "../elements/LanguageDropdown"; function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); - PlatformPeg.get().reload(); + PlatformPeg.get()?.reload(); } } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 80a57cab36b..2285d2b5890 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports"; -import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels"; +import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient } from "matrix-js-sdk/src/client"; @@ -158,7 +158,7 @@ export default class LoginWithQR extends React.Component { client: this.props.client, }); - const channel = new MSC3903ECDHv1RendezvousChannel( + const channel = new MSC3903ECDHv2RendezvousChannel( transport, undefined, this.onFailure, diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index a1fb67c5280..2411547912e 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -20,15 +20,16 @@ import Field, { IInputProps } from "../elements/Field"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import { _t, _td } from "../../../languageHandler"; -interface IProps extends Omit { +interface IProps extends Omit { id?: string; fieldRef?: RefCallback | RefObject; autoComplete?: string; value: string; password: string; // The password we're confirming - labelRequired?: string; - labelInvalid?: string; + label: string; + labelRequired: string; + labelInvalid: string; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index b221322e523..5175de6aa7a 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -31,10 +31,10 @@ interface IProps extends Omit { value: string; fieldRef?: RefCallback | RefObject; - label?: string; - labelEnterPassword?: string; - labelStrongPassword?: string; - labelAllowedButUnsafe?: string; + label: string; + labelEnterPassword: string; + labelStrongPassword: string; + labelAllowedButUnsafe: string; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; @@ -48,12 +48,12 @@ class PassphraseField extends PureComponent { labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), }; - public readonly validate = withValidation({ + public readonly validate = withValidation({ description: function (complexity) { const score = complexity ? complexity.score : 0; return ; }, - deriveData: async ({ value }): Promise => { + deriveData: async ({ value }): Promise => { if (!value) return null; const { scorePassword } = await import("../../../utils/PasswordScorer"); return scorePassword(value); @@ -67,7 +67,7 @@ class PassphraseField extends PureComponent { { key: "complexity", test: async function ({ value }, complexity): Promise { - if (!value) { + if (!value || !complexity) { return false; } const safe = complexity.score >= this.props.minScore; @@ -78,7 +78,7 @@ class PassphraseField extends PureComponent { // Unsafe passwords that are valid are only possible through a // configuration flag. We'll print some helper text to signal // to the user that their password is allowed, but unsafe. - if (complexity.score >= this.props.minScore) { + if (complexity && complexity.score >= this.props.minScore) { return _t(this.props.labelStrongPassword); } return _t(this.props.labelAllowedButUnsafe); diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index bb630b5fc22..727505551ac 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -36,7 +36,7 @@ interface IProps { phoneNumber: string; serverConfig: ValidatedServerConfig; - loginIncorrect?: boolean; + loginIncorrect: boolean; disableSubmit?: boolean; busy?: boolean; @@ -67,9 +67,9 @@ const enum LoginField { * The email/username/phone fields are fully-controlled, the password field is not. */ export default class PasswordLogin extends React.PureComponent { - private [LoginField.Email]: Field; - private [LoginField.Phone]: Field; - private [LoginField.MatrixId]: Field; + private [LoginField.Email]: Field | null; + private [LoginField.Phone]: Field | null; + private [LoginField.MatrixId]: Field | null; public static defaultProps = { onUsernameChanged: function () {}, @@ -93,7 +93,7 @@ export default class PasswordLogin extends React.PureComponent { private onForgotPasswordClick = (ev: ButtonEvent): void => { ev.preventDefault(); ev.stopPropagation(); - this.props.onForgotPasswordClick(); + this.props.onForgotPasswordClick?.(); }; private onSubmitForm = async (ev: SyntheticEvent): Promise => { @@ -116,25 +116,25 @@ export default class PasswordLogin extends React.PureComponent { }; private onUsernameChanged = (ev: React.ChangeEvent): void => { - this.props.onUsernameChanged(ev.target.value); + this.props.onUsernameChanged?.(ev.target.value); }; private onUsernameBlur = (ev: React.FocusEvent): void => { - this.props.onUsernameBlur(ev.target.value); + this.props.onUsernameBlur?.(ev.target.value); }; private onLoginTypeChange = (ev: React.ChangeEvent): void => { const loginType = ev.target.value as IState["loginType"]; this.setState({ loginType }); - this.props.onUsernameChanged(""); // Reset because email and username use the same state + this.props.onUsernameChanged?.(""); // Reset because email and username use the same state }; private onPhoneCountryChanged = (country: PhoneNumberCountryDefinition): void => { - this.props.onPhoneCountryChanged(country.iso2); + this.props.onPhoneCountryChanged?.(country.iso2); }; private onPhoneNumberChanged = (ev: React.ChangeEvent): void => { - this.props.onPhoneNumberChanged(ev.target.value); + this.props.onPhoneNumberChanged?.(ev.target.value); }; private onPasswordChanged = (ev: React.ChangeEvent): void => { @@ -199,7 +199,7 @@ export default class PasswordLogin extends React.PureComponent { return null; } - private markFieldValid(fieldID: LoginField, valid: boolean): void { + private markFieldValid(fieldID: LoginField, valid?: boolean): void { const { fieldValid } = this.state; fieldValid[fieldID] = valid; this.setState({ @@ -368,7 +368,7 @@ export default class PasswordLogin extends React.PureComponent { } public render(): React.ReactNode { - let forgotPasswordJsx; + let forgotPasswordJsx: JSX.Element | undefined; if (this.props.onForgotPasswordClick) { forgotPasswordJsx = ( diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 25a319e8179..17d540b4ed6 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { BaseSyntheticEvent } from "react"; +import React, { BaseSyntheticEvent, ReactNode } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixError } from "matrix-js-sdk/src/matrix"; @@ -82,7 +82,7 @@ interface IState { // Field error codes by field ID fieldValid: Partial>; // The ISO2 country code selected in the phone number entry - phoneCountry: string; + phoneCountry?: string; username: string; email: string; phoneNumber: string; @@ -95,11 +95,11 @@ interface IState { * A pure UI component which displays a registration form. */ export default class RegistrationForm extends React.PureComponent { - private [RegistrationField.Email]: Field; - private [RegistrationField.Password]: Field; - private [RegistrationField.PasswordConfirm]: Field; - private [RegistrationField.Username]: Field; - private [RegistrationField.PhoneNumber]: Field; + private [RegistrationField.Email]: Field | null; + private [RegistrationField.Password]: Field | null; + private [RegistrationField.PasswordConfirm]: Field | null; + private [RegistrationField.Username]: Field | null; + private [RegistrationField.PhoneNumber]: Field | null; public static defaultProps = { onValidationChange: logger.error, @@ -117,7 +117,6 @@ export default class RegistrationForm extends React.PureComponent => { - if (confirmed) { + if (confirmed && email !== undefined) { this.setState( { email, @@ -265,7 +264,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.Email, result.valid); + this.markFieldValid(RegistrationField.Email, !!result.valid); }; private validateEmailRules = withValidation({ @@ -294,7 +293,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.Password, result.valid); + this.markFieldValid(RegistrationField.Password, !!result.valid); }; private onPasswordConfirmChange = (ev: React.ChangeEvent): void => { @@ -304,7 +303,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.PasswordConfirm, result.valid); + this.markFieldValid(RegistrationField.PasswordConfirm, !!result.valid); }; private onPhoneCountryChange = (newVal: PhoneNumberCountryDefinition): void => { @@ -321,7 +320,7 @@ export default class RegistrationForm extends React.PureComponent => { const result = await this.validatePhoneNumberRules(fieldState); - this.markFieldValid(RegistrationField.PhoneNumber, result.valid); + this.markFieldValid(RegistrationField.PhoneNumber, !!result.valid); return result; }; @@ -352,14 +351,14 @@ export default class RegistrationForm extends React.PureComponent => { const result = await this.validateUsernameRules(fieldState); - this.markFieldValid(RegistrationField.Username, result.valid); + this.markFieldValid(RegistrationField.Username, !!result.valid); return result; }; private validateUsernameRules = withValidation({ description: (_, results) => { // omit the description if the only failing result is the `available` one as it makes no sense for it. - if (results.every(({ key, valid }) => key === "available" || valid)) return; + if (results.every(({ key, valid }) => key === "available" || valid)) return null; return _t("Use lowercase letters, numbers, dashes and underscores only"); }, hideDescriptionIfValid: true, @@ -448,7 +447,7 @@ export default class RegistrationForm extends React.PureComponent ); - let emailHelperText = null; + let emailHelperText: JSX.Element | undefined; if (this.showEmail()) { if (this.showPhoneNumber()) { emailHelperText = ( diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index adcbdad25a1..29294f70a60 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -34,7 +34,7 @@ interface IProps {} export default class Welcome extends React.PureComponent { public render(): React.ReactNode { const pagesConfig = SdkConfig.getObject("embedded_pages"); - let pageUrl = null; + let pageUrl: string | undefined; if (pagesConfig) { pageUrl = pagesConfig.get("welcome_url"); } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 469c5fbb2ab..8728673b589 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -32,13 +32,13 @@ import { toPx } from "../../../utils/units"; import { _t } from "../../../languageHandler"; interface IProps { - name: string; // The name (first initial used as default) + name?: string; // The name (first initial used as default) idName?: string; // ID for generating hash colours title?: string; // onHover title text - url?: string; // highest priority of them all, shortcut to set in urls[0] + url?: string | null; // highest priority of them all, shortcut to set in urls[0] urls?: string[]; // [highest_priority, ... , lowest_priority] - width?: number; - height?: number; + width: number; + height: number; // XXX: resizeMethod not actually used. resizeMethod?: ResizeMethod; defaultToInitialLetter?: boolean; // true to add default url @@ -48,7 +48,7 @@ interface IProps { tabIndex?: number; } -const calculateUrls = (url?: string, urls?: string[], lowBandwidth = false): string[] => { +const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, ...props.urls ] @@ -66,7 +66,7 @@ const calculateUrls = (url?: string, urls?: string[], lowBandwidth = false): str return Array.from(new Set(_urls)); }; -const useImageUrl = ({ url, urls }: { url?: string; urls?: string[] }): [string, () => void] => { +const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists const roomContext = useContext(RoomContext); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 6d1ec066e72..953ffc2288a 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -62,7 +62,7 @@ enum Icon { PresenceBusy = "BUSY", } -function tooltipText(variant: Icon): string { +function tooltipText(variant: Icon): string | undefined { switch (variant) { case Icon.Globe: return _t("This room is public"); @@ -78,7 +78,7 @@ function tooltipText(variant: Icon): string { } export default class DecoratedRoomAvatar extends React.PureComponent { - private _dmUser: User; + private _dmUser: User | null; private isUnmounted = false; private isWatchingTimeline = false; @@ -103,11 +103,11 @@ export default class DecoratedRoomAvatar extends React.PureComponent { + private onRoomTimeline = (ev: MatrixEvent, room?: Room): void => { if (this.isUnmounted) return; if (this.props.room.roomId !== room?.roomId) return; @@ -182,7 +182,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData & { + oobData: IOOBData & { roomId?: string; }; viewAvatarOnClick?: boolean; @@ -86,7 +87,7 @@ export default class RoomAvatar extends React.Component { }; private static getImageUrls(props: IProps): string[] { - let oobAvatar = null; + let oobAvatar: string | null = null; if (props.oobData.avatarUrl) { oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( props.width, @@ -94,28 +95,27 @@ export default class RoomAvatar extends React.Component { props.resizeMethod, ); } - return [ + + return filterBoolean([ oobAvatar, // highest priority RoomAvatar.getRoomAvatarUrl(props), - ].filter(function (url) { - return url !== null && url !== ""; - }); + ]); } - private static getRoomAvatarUrl(props: IProps): string { + private static getRoomAvatarUrl(props: IProps): string | null { if (!props.room) return null; return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod); } private onRoomAvatarClick = (): void => { - const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null); + const avatarUrl = Avatar.avatarUrlForRoom(this.props.room ?? null, undefined, undefined, undefined); const params = { src: avatarUrl, - name: this.props.room.name, + name: this.props.room?.name, }; - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true); }; private get roomIdName(): string | undefined { @@ -137,7 +137,7 @@ export default class RoomAvatar extends React.Component { public render(): React.ReactNode { const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; - const roomName = room?.name ?? oobData.name; + const roomName = room?.name ?? oobData.name ?? "?"; return ( , "name" | "url" | "urls"> { +interface IProps extends Omit, "name" | "url" | "urls" | "height" | "width"> { app: IApp; + height?: number; + width?: number; } const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 20, ...props }) => { @@ -44,7 +46,7 @@ const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 2 name={app.id} className={classNames("mx_WidgetAvatar", className)} // MSC2765 - url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined} + url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : null} urls={iconUrls} width={width} height={height} diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx index 24fdb05f325..caeff53bf5c 100644 --- a/src/components/views/beacon/BeaconListItem.tsx +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -42,14 +42,14 @@ const BeaconListItem: React.FC> = ({ beacon, .. const matrixClient = useContext(MatrixClientContext); const room = matrixClient.getRoom(beacon.roomId); - if (!latestLocationState || !beacon.isLive) { + if (!latestLocationState || !beacon.isLive || !room) { return null; } - const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self; - const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : undefined; + const isSelfLocation = beacon.beaconInfo?.assetType === LocationAssetType.Self; + const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : null; - const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp); + const humanizedUpdateTime = (latestLocationState.timestamp && humanizeTime(latestLocationState.timestamp)) || ""; return (
  • @@ -62,7 +62,7 @@ const BeaconListItem: React.FC> = ({ beacon, .. {/* eat events from interactive share buttons diff --git a/src/components/views/beacon/BeaconStatusTooltip.tsx b/src/components/views/beacon/BeaconStatusTooltip.tsx index a0f70504c18..d66f4a59d01 100644 --- a/src/components/views/beacon/BeaconStatusTooltip.tsx +++ b/src/components/views/beacon/BeaconStatusTooltip.tsx @@ -27,11 +27,11 @@ interface Props { beacon: Beacon; } -const useBeaconName = (beacon: Beacon): string => { +const useBeaconName = (beacon: Beacon): string | undefined => { const matrixClient = useContext(MatrixClientContext); - if (beacon.beaconInfo.assetType !== LocationAssetType.Self) { - return beacon.beaconInfo.description; + if (beacon.beaconInfo?.assetType !== LocationAssetType.Self) { + return beacon.beaconInfo?.description; } const room = matrixClient.getRoom(beacon.roomId); const member = room?.getMember(beacon.beaconInfoOwner); diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 6c91cd1f39a..818ca822426 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -23,7 +23,6 @@ import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-loca import { useLiveBeacons } from "../../../utils/beacon/useLiveBeacons"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import BaseDialog from "../dialogs/BaseDialog"; -import { IDialogProps } from "../dialogs/IDialogProps"; import Map from "../location/Map"; import ZoomButtons from "../location/ZoomButtons"; import BeaconMarker from "./BeaconMarker"; @@ -38,11 +37,12 @@ import MapFallback from "../location/MapFallback"; import { MapError } from "../location/MapError"; import { LocationShareError } from "../../../utils/location"; -interface IProps extends IDialogProps { +interface IProps { roomId: Room["roomId"]; matrixClient: MatrixClient; // open the map centered on this beacon's location initialFocusedBeacon?: Beacon; + onFinished(): void; } // track the 'focused time' as ts @@ -54,7 +54,7 @@ interface FocusedBeaconState { beacon?: Beacon; } -const getBoundsCenter = (bounds: Bounds): string | undefined => { +const getBoundsCenter = (bounds?: Bounds): string | undefined => { if (!bounds) { return; } @@ -70,10 +70,10 @@ const useMapPosition = ( { beacon, ts }: FocusedBeaconState, ): { bounds?: Bounds; - centerGeoUri: string; + centerGeoUri?: string; } => { const [bounds, setBounds] = useState(getBeaconBounds(liveBeacons)); - const [centerGeoUri, setCenterGeoUri] = useState( + const [centerGeoUri, setCenterGeoUri] = useState( beacon?.latestLocationState?.uri || getBoundsCenter(bounds), ); diff --git a/src/components/views/beacon/DialogOwnBeaconStatus.tsx b/src/components/views/beacon/DialogOwnBeaconStatus.tsx index 8c74586f355..3c456004f41 100644 --- a/src/components/views/beacon/DialogOwnBeaconStatus.tsx +++ b/src/components/views/beacon/DialogOwnBeaconStatus.tsx @@ -45,12 +45,12 @@ const DialogOwnBeaconStatus: React.FC = ({ roomId }) => { const matrixClient = useContext(MatrixClientContext); const room = matrixClient.getRoom(roomId); - if (!beacon?.isLive) { + if (!beacon?.isLive || !room) { return null; } - const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self; - const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : undefined; + const isSelfLocation = beacon.beaconInfo?.assetType === LocationAssetType.Self; + const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : null; return (
    diff --git a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx index d3577ba3ada..0104a79cae3 100644 --- a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx +++ b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx @@ -25,7 +25,7 @@ import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-loca import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import dispatcher from "../../../dispatcher/dispatcher"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; interface Props { isMinimized?: boolean; @@ -121,7 +121,7 @@ const LeftPanelLiveShareWarning: React.FC = ({ isMinimized }) => { ); const onWarningClick = relevantBeacon - ? () => { + ? (_e: ButtonEvent) => { dispatcher.dispatch({ action: Action.ViewRoom, room_id: relevantBeacon.roomId, @@ -131,7 +131,7 @@ const LeftPanelLiveShareWarning: React.FC = ({ isMinimized }) => { highlighted: true, }); } - : undefined; + : null; const label = getLabel(hasStoppingErrors, hasLocationPublishErrors); diff --git a/src/components/views/beacon/LiveTimeRemaining.tsx b/src/components/views/beacon/LiveTimeRemaining.tsx index 3ffd248ca7d..b6682d710be 100644 --- a/src/components/views/beacon/LiveTimeRemaining.tsx +++ b/src/components/views/beacon/LiveTimeRemaining.tsx @@ -40,13 +40,19 @@ const getUpdateInterval = (ms: number): number => { const useMsRemaining = (beacon: Beacon): number => { const beaconInfo = useEventEmitterState(beacon, BeaconEvent.Update, () => beacon.beaconInfo); - const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo)); + const [msRemaining, setMsRemaining] = useState(() => (beaconInfo ? getBeaconMsUntilExpiry(beaconInfo) : 0)); useEffect(() => { + if (!beaconInfo) { + return; + } setMsRemaining(getBeaconMsUntilExpiry(beaconInfo)); }, [beaconInfo]); const updateMsRemaining = useCallback(() => { + if (!beaconInfo) { + return; + } const ms = getBeaconMsUntilExpiry(beaconInfo); setMsRemaining(ms); }, [beaconInfo]); diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 7baffe3e5fd..92afa950da2 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -42,7 +42,7 @@ const OwnBeaconStatus: React.FC> = ({ beacon, stoppingInProgress, onStopSharing, onResetLocationPublishError, - } = useOwnLiveBeacons([beacon?.identifier]); + } = useOwnLiveBeacons(beacon?.identifier ? [beacon?.identifier] : []); // combine display status with errors that only occur for user's own beacons const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ? BeaconDisplayStatus.Error : displayStatus; diff --git a/src/components/views/beacon/ShareLatestLocation.tsx b/src/components/views/beacon/ShareLatestLocation.tsx index 175e1d46e2e..ef525ad8385 100644 --- a/src/components/views/beacon/ShareLatestLocation.tsx +++ b/src/components/views/beacon/ShareLatestLocation.tsx @@ -28,9 +28,9 @@ interface Props { } const ShareLatestLocation: React.FC = ({ latestLocationState }) => { - const [coords, setCoords] = useState(null); + const [coords, setCoords] = useState(); useEffect(() => { - if (!latestLocationState) { + if (!latestLocationState?.uri) { return; } const coords = parseGeoUri(latestLocationState.uri); diff --git a/src/components/views/beacon/displayStatus.ts b/src/components/views/beacon/displayStatus.ts index 51c63f95c65..48260cb672f 100644 --- a/src/components/views/beacon/displayStatus.ts +++ b/src/components/views/beacon/displayStatus.ts @@ -40,7 +40,5 @@ export const getBeaconDisplayStatus = ( if (!latestLocationState) { return BeaconDisplayStatus.Loading; } - if (latestLocationState) { - return BeaconDisplayStatus.Active; - } + return BeaconDisplayStatus.Active; }; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index bef5cdc8bf1..2cc0e094c3c 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { useContext } from "react"; import { MatrixCapabilities } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; +import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { ChevronFace } from "../../structures/ContextMenu"; @@ -34,6 +35,8 @@ import { WidgetType } from "../../../widgets/WidgetType"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; +import { ModuleRunner } from "../../../modules/ModuleRunner"; +import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; interface IProps extends React.ComponentProps { app: IApp; @@ -45,7 +48,7 @@ interface IProps extends React.ComponentProps { onEditClick?(): void; } -const WidgetContextMenu: React.FC = ({ +export const WidgetContextMenu: React.FC = ({ onFinished, app, userWidget, @@ -158,24 +161,31 @@ const WidgetContextMenu: React.FC = ({ const isLocalWidget = WidgetType.JITSI.matches(app.type); let revokeButton; if (!userWidget && !isLocalWidget && isAllowedWidget) { - const onRevokeClick = (): void => { - logger.info("Revoking permission for widget to load: " + app.eventId); - const current = SettingsStore.getValue("allowedWidgets", roomId); - if (app.eventId !== undefined) current[app.eventId] = false; - const level = SettingsStore.firstSupportedLevel("allowedWidgets"); - SettingsStore.setValue("allowedWidgets", roomId, level, current).catch((err) => { - logger.error(err); - // We don't really need to do anything about this - the user will just hit the button again. - }); - onFinished(); - }; + const opts: ApprovalOpts = { approved: undefined }; + ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app)); + + if (!opts.approved) { + const onRevokeClick = (): void => { + logger.info("Revoking permission for widget to load: " + app.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + if (app.eventId !== undefined) current[app.eventId] = false; + const level = SettingsStore.firstSupportedLevel("allowedWidgets"); + if (!level) throw new Error("level must be defined"); + SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => { + logger.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); + onFinished(); + }; - revokeButton = ; + revokeButton = ; + } } let moveLeftButton; if (showUnpin && widgetIndex > 0) { const onClick = (): void => { + if (!room) throw new Error("room must be defined"); WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1); onFinished(); }; @@ -207,5 +217,3 @@ const WidgetContextMenu: React.FC = ({ ); }; - -export default WidgetContextMenu; diff --git a/src/components/views/dialogs/AppDownloadDialog.tsx b/src/components/views/dialogs/AppDownloadDialog.tsx index 46b7e455c23..dae321e683a 100644 --- a/src/components/views/dialogs/AppDownloadDialog.tsx +++ b/src/components/views/dialogs/AppDownloadDialog.tsx @@ -25,13 +25,16 @@ import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Heading from "../typography/Heading"; import BaseDialog from "./BaseDialog"; -import { IDialogProps } from "./IDialogProps"; const fallbackAppStore = "https://apps.apple.com/app/vector/id1083446067"; const fallbackGooglePlay = "https://play.google.com/store/apps/details?id=im.vector.app"; const fallbackFDroid = "https://f-droid.org/repository/browse/?fdid=im.vector.app"; -export const AppDownloadDialog: FC = ({ onFinished }: IDialogProps) => { +interface Props { + onFinished(): void; +} + +export const AppDownloadDialog: FC = ({ onFinished }) => { const brand = SdkConfig.get("brand"); const desktopBuilds = SdkConfig.getObject("desktop_builds"); const mobileBuilds = SdkConfig.getObject("mobile_builds"); diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx index 9b9982618a9..a35971992e2 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +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. @@ -14,14 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useCallback } from "react"; import { _t } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import BaseDialog from "./BaseDialog"; -interface IProps { +export interface AskInviteAnywayDialogProps { unknownProfileUsers: Array<{ userId: string; errorText: string; @@ -31,57 +32,58 @@ interface IProps { onFinished: (success: boolean) => void; } -export default class AskInviteAnywayDialog extends React.Component { - private onInviteClicked = (): void => { - this.props.onInviteAnyways(); - this.props.onFinished(true); - }; +export default function AskInviteAnywayDialog({ + onFinished, + onGiveUp, + onInviteAnyways, + unknownProfileUsers, +}: AskInviteAnywayDialogProps): JSX.Element { + const onInviteClicked = useCallback((): void => { + onInviteAnyways(); + onFinished(true); + }, [onInviteAnyways, onFinished]); - private onInviteNeverWarnClicked = (): void => { + const onInviteNeverWarnClicked = useCallback((): void => { SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); - this.props.onInviteAnyways(); - this.props.onFinished(true); - }; + onInviteAnyways(); + onFinished(true); + }, [onInviteAnyways, onFinished]); - private onGiveUpClicked = (): void => { - this.props.onGiveUp(); - this.props.onFinished(false); - }; + const onGiveUpClicked = useCallback((): void => { + onGiveUp(); + onFinished(false); + }, [onGiveUp, onFinished]); - public render(): React.ReactNode { - const errorList = this.props.unknownProfileUsers.map((address) => ( -
  • - {address.userId}: {address.errorText} -
  • - )); + const errorList = unknownProfileUsers.map((address) => ( +
  • + {address.userId}: {address.errorText} +
  • + )); - return ( - -
    -

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

    -
      {errorList}
    -
    + return ( + +
    +

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

    +
      {errorList}
    +
    -
    - - - -
    -
    - ); - } +
    + + + +
    +
    + ); } diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 2feb7aac338..0def4f1943c 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -21,17 +21,16 @@ import FocusLock from "react-focus-lock"; import classNames from "classnames"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import Heading from "../typography/Heading"; -import { IDialogProps } from "./IDialogProps"; import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -interface IProps extends IDialogProps { +interface IProps { // Whether the dialog should have a 'close' button that will // cause the dialog to be cancelled. This should only be set // to false if there is nothing the app can sensibly do if the @@ -75,6 +74,7 @@ interface IProps extends IDialogProps { // optional Posthog ScreenName to supply during the lifetime of this dialog "screenName"?: ScreenName; + onFinished(): void; } /* @@ -86,7 +86,7 @@ interface IProps extends IDialogProps { export default class BaseDialog extends React.Component { private matrixClient: MatrixClient; - public static defaultProps = { + public static defaultProps: Partial = { hasCancel: true, fixedWidth: true, }; @@ -98,9 +98,7 @@ export default class BaseDialog extends React.Component { } private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => { - if (this.props.onKeyDown) { - this.props.onKeyDown(e); - } + this.props.onKeyDown?.(e); const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -109,13 +107,13 @@ export default class BaseDialog extends React.Component { e.stopPropagation(); e.preventDefault(); - this.props.onFinished(false); + this.props.onFinished(); break; } }; - private onCancelClick = (e: ButtonEvent): void => { - this.props.onFinished(false); + private onCancelClick = (): void => { + this.props.onFinished(); }; public render(): React.ReactNode { diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 86298fb970f..8223fde9295 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { _t } from "../../../languageHandler"; -import { IDialogProps } from "./IDialogProps"; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -27,8 +26,9 @@ import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog"; // XXX: Keep this around for re-use in future Betas -interface IProps extends IDialogProps { +interface IProps { featureId: string; + onFinished(sendFeedback?: boolean): void; } const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { @@ -49,7 +49,7 @@ const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { { - onFinished(false); + onFinished(); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Labs, diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index e1f9a11cd22..ba12f210399 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -26,19 +26,19 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; -import { IDialogProps } from "./IDialogProps"; import BaseDialog from "../dialogs/BaseDialog"; import InfoDialog from "../dialogs/InfoDialog"; import DialogButtons from "../elements/DialogButtons"; import StyledCheckbox from "../elements/StyledCheckbox"; -interface IBulkRedactDialogProps extends IDialogProps { +interface Props { matrixClient: MatrixClient; room: Room; member: RoomMember; + onFinished(redact?: boolean): void; } -const BulkRedactDialog: React.FC = (props) => { +const BulkRedactDialog: React.FC = (props) => { const { matrixClient: cli, room, member, onFinished } = props; const [keepStateEvents, setKeepStateEvents] = useState(true); diff --git a/src/components/views/dialogs/ChangelogDialog.tsx b/src/components/views/dialogs/ChangelogDialog.tsx index 85af8203b74..8321cc40f49 100644 --- a/src/components/views/dialogs/ChangelogDialog.tsx +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -27,7 +27,7 @@ interface IProps { onFinished: (success: boolean) => void; } -type State = Partial>; +type State = Partial>; interface Commit { sha: string; @@ -46,7 +46,7 @@ export default class ChangelogDialog extends React.Component { this.state = {}; } - private async fetchChanges(repo: typeof REPOS[number], oldVersion: string, newVersion: string): Promise { + private async fetchChanges(repo: (typeof REPOS)[number], oldVersion: string, newVersion: string): Promise { const url = `https://riot.im/github/repos/${repo}/compare/${oldVersion}...${newVersion}`; try { diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 60e18eb263a..250b90e61df 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -24,7 +24,7 @@ import Spinner from "../elements/Spinner"; interface IProps { redact: () => Promise; - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; } interface IState { diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 0ee73f871a2..2fa684eda84 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -39,7 +39,7 @@ interface IProps { children?: ReactNode; className?: string; roomId?: string; - onFinished: (success: boolean, reason?: string) => void; + onFinished: (success?: boolean, reason?: string) => void; } interface IState { @@ -55,7 +55,7 @@ interface IState { * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ export default class ConfirmUserActionDialog extends React.Component { - public static defaultProps = { + public static defaultProps: Partial = { danger: false, askReason: false, }; diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 0f530a4fd77..e9ab1bc3cc5 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -21,7 +21,7 @@ import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; interface IProps { - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; } export default class ConfirmWipeDeviceDialog extends React.Component { diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index 915ca9f249b..b15fb800d26 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -41,7 +41,7 @@ interface IProps { defaultName?: string; parentSpace?: Room; defaultEncrypted?: boolean; - onFinished(proceed: boolean, opts?: IOpts): void; + onFinished(proceed?: boolean, opts?: IOpts): void; } interface IState { diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 59b697621f9..fe9f04af184 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -24,9 +24,10 @@ import Modal from "../../../Modal"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import QuestionDialog from "./QuestionDialog"; -import { IDialogProps } from "./IDialogProps"; -interface IProps extends IDialogProps {} +interface IProps { + onFinished(logout?: boolean): void; +} const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { const brand = SdkConfig.get().brand; @@ -72,7 +73,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { props.onFinished(false)} > diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index bdc1d54aba8..8e8f0be7703 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -39,20 +39,20 @@ type DialogAesthetics = Partial<{ }>; interface IProps { - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; } interface IState { shouldErase: boolean; - errStr: string; + errStr: string | null; authData: any; // for UIA authEnabled: boolean; // see usages for information // A few strings that are passed to InteractiveAuth for design or are displayed // next to the InteractiveAuth component. - bodyText: string; - continueText: string; - continueKind: string; + bodyText?: string; + continueText?: string; + continueKind?: string; } export default class DeactivateAccountDialog extends React.Component { @@ -64,12 +64,6 @@ export default class DeactivateAccountDialog extends React.Component{this.state.errStr}; } diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 2b74667ba59..3fd45c17a10 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -65,7 +65,7 @@ const Tools: Record = { interface IProps { roomId: string; - onFinished(finished: boolean): void; + onFinished(finished?: boolean): void; } type ToolInfo = [label: string, tool: Tool]; diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index 1951f881bb6..0d4a66dfe72 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -20,17 +20,16 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent"; import { _t } from "../../../languageHandler"; -import { IDialogProps } from "./IDialogProps"; import QuestionDialog from "./QuestionDialog"; import { findTopAnswer } from "../messages/MPollBody"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; import { GetRelationsForEvent } from "../rooms/EventTile"; -interface IProps extends IDialogProps { +interface IProps { matrixClient: MatrixClient; event: MatrixEvent; - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; getRelationsForEvent?: GetRelationsForEvent; } diff --git a/src/components/views/dialogs/ErrorDialog.tsx b/src/components/views/dialogs/ErrorDialog.tsx index 62ce55cdf31..4a82941767a 100644 --- a/src/components/views/dialogs/ErrorDialog.tsx +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -31,7 +31,7 @@ import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; interface IProps { - onFinished: (success: boolean) => void; + onFinished: (success?: boolean) => void; title?: string; description?: React.ReactNode; button?: string; @@ -44,7 +44,7 @@ interface IState { } export default class ErrorDialog extends React.Component { - public static defaultProps = { + public static defaultProps: Partial = { focus: true, }; diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index 406df1fd8e6..84afb0a4cc0 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; -import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import Field from "../elements/Field"; @@ -44,8 +43,9 @@ import InfoDialog from "./InfoDialog"; import ChatExport from "../../../customisations/ChatExport"; import { validateNumberInRange } from "../../../utils/validate"; -interface IProps extends IDialogProps { +interface IProps { room: Room; + onFinished(doExport?: boolean): void; } interface ExportConfig { @@ -110,11 +110,14 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { const [displayCancel, setCancelWarning] = useState(false); const [exportCancelled, setExportCancelled] = useState(false); const [exportSuccessful, setExportSuccessful] = useState(false); - const [exporter, setExporter] = useStateCallback(null, async (exporter: Exporter): Promise => { - await exporter?.export().then(() => { - if (!exportCancelled) setExportSuccessful(true); - }); - }); + const [exporter, setExporter] = useStateCallback( + null, + async (exporter: Exporter | null): Promise => { + await exporter?.export().then(() => { + if (!exportCancelled) setExportSuccessful(true); + }); + }, + ); const startExport = async (): Promise => { const exportOptions = { diff --git a/src/components/views/dialogs/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index d1f950d7d62..d8ffb54d248 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -24,7 +24,6 @@ import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; import BugReportDialog from "./BugReportDialog"; import InfoDialog from "./InfoDialog"; -import { IDialogProps } from "./IDialogProps"; import { submitFeedback } from "../../../rageshake/submit-rageshake"; import { useStateToggle } from "../../../hooks/useStateToggle"; import StyledCheckbox from "../elements/StyledCheckbox"; @@ -33,8 +32,9 @@ const existingIssuesUrl = "https://github.com/SchildiChat/schildichat-desktop/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc"; const newIssueUrl = "https://github.com/SchildiChat/schildichat-desktop/issues/new/choose"; -interface IProps extends IDialogProps { +interface IProps { feature?: string; + onFinished(): void; } const FeedbackDialog: React.FC = (props: IProps) => { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index df922a5ed9f..00e32657655 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -29,7 +29,6 @@ import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import { useSettingValue } from "../../../hooks/useSettings"; import { Layout } from "../../../settings/enums/Layout"; -import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import { avatarUrlForUser } from "../../../Avatar"; import EventTile from "../rooms/EventTile"; @@ -55,13 +54,14 @@ import { RoomContextDetails } from "../rooms/RoomContextDetails"; const AVATAR_SIZE = 30; -interface IProps extends IDialogProps { +interface IProps { matrixClient: MatrixClient; // The event to forward event: MatrixEvent; // We need a permalink creator for the source room to pass through to EventTile // in case the event is a reply (even though the user can't get at the link) permalinkCreator: RoomPermalinkCreator; + onFinished(): void; } interface IEntryProps { diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx index 2bdc3b5b3f1..48e95f02443 100644 --- a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -20,17 +20,17 @@ import QuestionDialog from "./QuestionDialog"; import { _t } from "../../../languageHandler"; import Field from "../elements/Field"; import SdkConfig from "../../../SdkConfig"; -import { IDialogProps } from "./IDialogProps"; import { submitFeedback } from "../../../rageshake/submit-rageshake"; import StyledCheckbox from "../elements/StyledCheckbox"; import Modal from "../../../Modal"; import InfoDialog from "./InfoDialog"; -interface IProps extends IDialogProps { +interface IProps { title: string; subheading: string; rageshakeLabel: string; rageshakeData?: Record; + onFinished(sendFeedback?: boolean): void; } const GenericFeatureFeedbackDialog: React.FC = ({ diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx deleted file mode 100644 index 631eb1abcf5..00000000000 --- a/src/components/views/dialogs/HostSignupDialog.tsx +++ /dev/null @@ -1,280 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import classNames from "classnames"; -import { logger } from "matrix-js-sdk/src/logger"; - -import AccessibleButton from "../elements/AccessibleButton"; -import Modal from "../../../Modal"; -import QuestionDialog from "./QuestionDialog"; -import SdkConfig from "../../../SdkConfig"; -import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { HostSignupStore } from "../../../stores/HostSignupStore"; -import { OwnProfileStore } from "../../../stores/OwnProfileStore"; -import { IPostmessage, IPostmessageResponseData, PostmessageAction } from "./HostSignupDialogTypes"; -import { IConfigOptions } from "../../../IConfigOptions"; -import { SnakedObject } from "../../../utils/SnakedObject"; - -interface IProps {} - -interface IState { - completed: boolean; - error: string; - minimized: boolean; -} - -export default class HostSignupDialog extends React.PureComponent { - private iframeRef: React.RefObject = React.createRef(); - private readonly config: SnakedObject; - - public constructor(props: IProps) { - super(props); - - this.state = { - completed: false, - error: null, - minimized: false, - }; - - this.config = SdkConfig.getObject("host_signup"); - } - - private messageHandler = async (message: IPostmessage): Promise => { - if (!this.config.get("url").startsWith(message.origin)) { - return; - } - switch (message.data.action) { - case PostmessageAction.HostSignupAccountDetailsRequest: - this.onAccountDetailsRequest(); - break; - case PostmessageAction.Maximize: - this.setState({ - minimized: false, - }); - break; - case PostmessageAction.Minimize: - this.setState({ - minimized: true, - }); - break; - case PostmessageAction.SetupComplete: - this.setState({ - completed: true, - }); - break; - case PostmessageAction.CloseDialog: - return this.closeDialog(); - } - }; - - private maximizeDialog = (): void => { - this.setState({ - minimized: false, - }); - // Send this action to the iframe so it can act accordingly - this.sendMessage({ - action: PostmessageAction.Maximize, - }); - }; - - private minimizeDialog = (): void => { - this.setState({ - minimized: true, - }); - // Send this action to the iframe so it can act accordingly - this.sendMessage({ - action: PostmessageAction.Minimize, - }); - }; - - private closeDialog = async (): Promise => { - window.removeEventListener("message", this.messageHandler); - // Finally clear the flag in - return HostSignupStore.instance.setHostSignupActive(false); - }; - - private onCloseClick = async (): Promise => { - if (this.state.completed) { - // We're done, close - return this.closeDialog(); - } else { - Modal.createDialog(QuestionDialog, { - title: _t("Confirm abort of host creation"), - description: _t( - "Are you sure you wish to abort creation of the host? The process cannot be continued.", - ), - button: _t("Abort"), - onFinished: (result) => { - if (result) { - return this.closeDialog(); - } - }, - }); - } - }; - - private sendMessage = (message: IPostmessageResponseData): void => { - this.iframeRef.current.contentWindow.postMessage(message, this.config.get("url")); - }; - - private async sendAccountDetails(): Promise { - const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); - if (!openIdToken || !openIdToken.access_token) { - logger.warn("Failed to connect to homeserver for OpenID token."); - this.setState({ - completed: true, - error: _t("Failed to connect to your homeserver. Please close this dialog and try again."), - }); - return; - } - this.sendMessage({ - action: PostmessageAction.HostSignupAccountDetails, - account: { - accessToken: await MatrixClientPeg.get().getAccessToken(), - name: OwnProfileStore.instance.displayName, - openIdToken: openIdToken.access_token, - serverName: await MatrixClientPeg.get().getDomain(), - userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(), - termsAccepted: true, - }, - }); - } - - private onAccountDetailsDialogFinished = async (result: boolean): Promise => { - if (result) { - return this.sendAccountDetails(); - } - return this.closeDialog(); - }; - - private onAccountDetailsRequest = (): void => { - const cookiePolicyUrl = this.config.get("cookie_policy_url"); - const privacyPolicyUrl = this.config.get("privacy_policy_url"); - const tosUrl = this.config.get("terms_of_service_url"); - - const textComponent = ( - <> -

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

    -

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

    - - ); - Modal.createDialog(QuestionDialog, { - title: _t("You should know"), - description: textComponent, - button: _t("Continue"), - onFinished: this.onAccountDetailsDialogFinished, - }); - }; - - public componentDidMount(): void { - window.addEventListener("message", this.messageHandler); - } - - public componentWillUnmount(): void { - if (HostSignupStore.instance.isHostSignupActive) { - // Run the close dialog actions if we're still active, otherwise good to go - this.closeDialog(); - } - } - - public render(): React.ReactNode { - return ( -
    -
    - {this.state.minimized && ( -
    -
    - {_t("%(hostSignupBrand)s Setup", { - hostSignupBrand: this.config.get("brand"), - })} -
    - -
    - )} - {!this.state.minimized && ( -
    - - -
    - )} - {this.state.error &&
    {this.state.error}
    } - {!this.state.error && ( -