diff --git a/.editorconfig b/.editorconfig index 56631484cd..98ebc4dc8f 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/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index d114217f5c..ea2c1947d2 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -5,6 +5,9 @@ on: workflows: ["Element Web - Build"] types: - completed +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }} + cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }} jobs: prepare: name: Prepare @@ -162,8 +165,9 @@ jobs: PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }} PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }} PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }} - PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }} PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} + # We manually finalize the build in the report stage + PERCY_PARALLEL_TOTAL: -1 - name: Upload Artifact if: failure() @@ -181,14 +185,35 @@ jobs: with: name: cypress-junit path: cypress/results + report: name: Report results - needs: tests + needs: + - prepare + - tests runs-on: ubuntu-latest if: always() permissions: statuses: write steps: + - name: Finalize Percy + if: needs.prepare.outputs.percy_enable == '1' + run: npx -p @percy/cli percy build:finalize + env: + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} + + - name: Skip Percy required check + if: needs.prepare.outputs.percy_enable != '1' + uses: Sibz/github-status-action@v1 + with: + authToken: ${{ secrets.GITHUB_TOKEN }} + state: success + description: Percy skipped + context: percy/matrix-react-sdk + sha: ${{ github.event.workflow_run.head_sha }} + target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + - uses: Sibz/github-status-action@v1 with: authToken: ${{ secrets.GITHUB_TOKEN }} @@ -196,6 +221,7 @@ jobs: context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) sha: ${{ github.event.workflow_run.head_sha }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + testrail: name: Report results to testrail needs: diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index cf3e1e2c88..592268d885 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -6,7 +6,5 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }} jobs: action: uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop - with: - labels: "T-Defect,T-Enhancement,T-Task" secrets: ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ebc17831a8..55fb131d97 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,10 @@ on: types: [upstream-sdk-notify] workflow_call: inputs: + disable_coverage: + type: boolean + required: false + description: "Specify true to skip generating and uploading coverage for tests" matrix-js-sdk-sha: type: string required: false @@ -39,16 +43,21 @@ jobs: id: cpu-cores uses: SimenB/github-actions-cpu-cores@v1 - - name: Run tests with coverage and metrics + - name: Load metrics reporter + id: metrics if: github.ref == 'refs/heads/develop' - run: "yarn coverage --ci --reporters github-actions '--reporters=/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}" + run: | + echo "extra-reporter='--reporters=/test/slowReporter.js'" >> $GITHUB_OUTPUT - - name: Run tests with coverage - if: github.ref != 'refs/heads/develop' - run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}" + - name: Run tests + run: | + yarn ${{ inputs.disable_coverage != 'true' && 'coverage' || 'test' }} \ + --ci \ + --reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \ + --max-workers ${{ steps.cpu-cores.outputs.count }} - name: Upload Artifact - if: inputs.matrix-js-sdk-sha == '' + if: inputs.disable_coverage != 'true' uses: actions/upload-artifact@v3 with: name: coverage diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index 13e3c56aba..20f748494a 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( + 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/location/location.spec.ts b/cypress/e2e/location/location.spec.ts index 0d512705a0..b716fe543b 100644 --- a/cypress/e2e/location/location.spec.ts +++ b/cypress/e2e/location/location.spec.ts @@ -27,7 +27,7 @@ describe("Location sharing", () => { }; const submitShareLocation = (): void => { - cy.get('[data-test-id="location-picker-submit-button"]').click(); + cy.get('[data-testid="location-picker-submit-button"]').click(); }; beforeEach(() => { diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index bef1cd0393..aa5a94a6dd 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", () => { @@ -185,11 +185,12 @@ describe("Timeline", () => { .should("have.css", "margin-inline-start", "104px") .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 }); + ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }"; + cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", { + percyCSS, + }); cy.checkA11y(); }); @@ -213,8 +214,7 @@ describe("Timeline", () => { 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; }"; + const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp { 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,14 +223,20 @@ 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", () => { @@ -329,7 +335,12 @@ describe("Timeline", () => { cy.wait("@mxc"); cy.checkA11y(); + + // Exclude timestamp from snapshot + const percyCSS = + ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { 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 5c016b406a..27986af10e 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 f5e30a58fc..b0f5c9f7c7 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,10 @@ declare global { } Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => { + if (!options?.allowSpinners) { + // Await spinners to vanish + cy.get(".mx_Spinner").should("not.exist"); + } cy.percySnapshot(name, { domTransformation: (documentClone) => scope(documentClone, subject.selector), ...options, diff --git a/package.json b/package.json index a016a9b337..17b40b5789 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.4.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", @@ -92,7 +92,7 @@ "lodash": "^4.17.20", "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", - "matrix-events-sdk": "0.0.1", + "matrix-events-sdk": "2.0.0", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", @@ -111,7 +111,7 @@ "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "^2.3.2", + "sanitize-html": "2.8.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", "url": "^0.11.0", @@ -167,9 +167,8 @@ "@types/react": "17.0.49", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "17.0.17", - "@types/react-test-renderer": "^17.0.1", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "^2.3.1", + "@types/sanitize-html": "2.8.0", "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", "@types/zxcvbn": "^4.4.0", @@ -212,7 +211,6 @@ "postcss-scss": "^4.0.4", "prettier": "2.8.0", "raw-loader": "^4.0.2", - "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.4", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index b589bd6636..0d56e1b512 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -18,6 +18,7 @@ @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss"; +@import "./components/views/dialogs/polls/_PollListItemEnded.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; @import "./components/views/elements/_FilterTabGroup.pcss"; @import "./components/views/elements/_LearnMore.pcss"; diff --git a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss new file mode 100644 index 0000000000..6518052ab6 --- /dev/null +++ b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss @@ -0,0 +1,60 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PollListItemEnded { + width: 100%; + display: flex; + flex-direction: column; + color: $primary-content; +} + +.mx_PollListItemEnded_title { + display: grid; + justify-content: left; + align-items: center; + grid-gap: $spacing-8; + grid-template-columns: min-content 1fr min-content; + grid-template-rows: auto; +} + +.mx_PollListItemEnded_icon { + height: 14px; + width: 14px; + color: $quaternary-content; + padding-left: $spacing-8; +} + +.mx_PollListItemEnded_date { + font-size: $font-12px; + color: $secondary-content; +} + +.mx_PollListItemEnded_question { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mx_PollListItemEnded_answers { + display: grid; + grid-gap: $spacing-8; + margin-top: $spacing-12; +} + +.mx_PollListItemEnded_voteCount { + // 6px to match PollOption padding + margin: $spacing-8 0 0 6px; +} diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss index e5a97b7f2b..da4c66d6cf 100644 --- a/res/css/components/views/polls/_PollOption.pcss +++ b/res/css/components/views/polls/_PollOption.pcss @@ -18,7 +18,6 @@ limitations under the License. border: 1px solid $quinary-content; border-radius: 8px; padding: 6px 12px; - max-width: 550px; background-color: $background; .mx_StyledRadioButton_content, diff --git a/res/css/views/dialogs/polls/_PollHistoryList.pcss b/res/css/views/dialogs/polls/_PollHistoryList.pcss index 6a0a003ce1..ee6f0254f7 100644 --- a/res/css/views/dialogs/polls/_PollHistoryList.pcss +++ b/res/css/views/dialogs/polls/_PollHistoryList.pcss @@ -32,6 +32,10 @@ limitations under the License. grid-gap: $spacing-20; padding-right: $spacing-64; margin: $spacing-32 0; + + &.mx_PollHistoryList_list_ENDED { + grid-gap: $spacing-32; + } } .mx_PollHistoryList_noResults { @@ -42,3 +46,14 @@ limitations under the License. justify-content: center; color: $secondary-content; } + +.mx_PollHistoryList_loading { + color: $secondary-content; + text-align: center; + + // center in all free space + // when there are no results + &.mx_PollHistoryList_noResultsYet { + margin: auto auto; + } +} diff --git a/res/css/views/elements/_CopyableText.pcss b/res/css/views/elements/_CopyableText.pcss index e6b3b1ebf9..edd1cdf716 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,11 +37,15 @@ 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; + right: 0; /* center to first line */ - position: relative; top: 0.15em; + background-color: $background; &::before { content: ""; diff --git a/res/css/views/messages/_LegacyCallEvent.pcss b/res/css/views/messages/_LegacyCallEvent.pcss index 873f5d4b5b..8d0f5abbc3 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/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index 193bd9382a..e7f3118d57 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -70,4 +70,5 @@ limitations under the License. display: grid; grid-gap: $spacing-16; margin-bottom: $spacing-8; + max-width: 550px; } diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 5d8d947854..b1ee5795d0 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<[boolean, IAuthData | Error | null] | undefined> { try { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (this.bind) { @@ -282,7 +282,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(); diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index e05858bb16..382214a1b6 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -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/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 34fffffc50..b5358b4930 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -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"), diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index b28a7f8038..04b20adc8e 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -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/PosthogTrackers.ts b/src/PosthogTrackers.ts index a82b78c1dd..c03a20216c 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/SdkConfig.ts b/src/SdkConfig.ts index 5aaab6e2f4..fab5cb29c6 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -26,7 +26,6 @@ export const DEFAULTS: IConfigOptions = { brand: "Element", 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: { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 66f673445d..6628b95fc1 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, diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index c769faf155..73933a23a9 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -27,6 +27,7 @@ import { timeout } from "../utils/promise"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import SpaceProvider from "./SpaceProvider"; import { TimelineRenderingType } from "../contexts/RoomContext"; +import { filterBoolean } from "../utils/arrays"; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -55,7 +56,7 @@ const PROVIDER_COMPLETION_TIMEOUT = 3000; export interface IProviderCompletions { completions: ICompletion[]; provider: AutocompleteProvider; - command: ICommand; + command: Partial; } export default class Autocompleter { @@ -98,8 +99,8 @@ export default class Autocompleter { ); // map then filter to maintain the index for the map-operation, for this.providers to line up - return completionsList - .map((completions, i) => { + return filterBoolean( + completionsList.map((completions, i) => { if (!completions || !completions.length) return; return { @@ -112,7 +113,7 @@ export default class Autocompleter { */ command: this.providers[i].getCurrentCommand(query, selection, force), }; - }) - .filter(Boolean) as IProviderCompletions[]; + }), + ); } } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 9c0acff4cd..b434eb6e57 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -32,6 +32,7 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji"; import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; +import { filterBoolean } from "../utils/arrays"; const LIMIT = 20; @@ -94,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider { shouldMatchWordsOnly: true, }); - this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))) as IEmoji[]; + this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode)))); } public async getCompletions( diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index cbd7d885c7..7ee30cb3ec 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -46,7 +46,7 @@ interface IState { export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; private unmounted = false; - private dispatcherRef: string = null; + private dispatcherRef: string | null = null; public constructor(props: IProps, context: typeof MatrixClientContext) { super(props, context); @@ -64,7 +64,7 @@ export default class EmbeddedPage extends React.PureComponent { let res: Response; try { - res = await fetch(this.props.url, { method: "GET" }); + res = await fetch(this.props.url!, { method: "GET" }); } catch (err) { if (this.unmounted) return; logger.warn(`Error loading page: ${err}`); @@ -84,7 +84,7 @@ export default class EmbeddedPage extends React.PureComponent { if (this.props.replaceMap) { Object.keys(this.props.replaceMap).forEach((key) => { - body = body.split(key).join(this.props.replaceMap[key]); + body = body.split(key).join(this.props.replaceMap![key]); }); } @@ -123,8 +123,7 @@ export default class EmbeddedPage extends React.PureComponent { const client = this.context || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; const className = this.props.className; - const classes = classnames({ - [className]: true, + const classes = classnames(className, { [`${className}_guest`]: isGuest, [`${className}_loggedIn`]: !!client, }); diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx index e8a8fa5e28..2f6bc13d79 100644 --- a/src/components/structures/FileDropTarget.tsx +++ b/src/components/structures/FileDropTarget.tsx @@ -40,6 +40,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { const onDragEnter = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); + if (!ev.dataTransfer) return; setState((state) => ({ // We always increment the counter no matter the types, because dragging is @@ -49,7 +50,8 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { // https://docs.w3cub.com/dom/datatransfer/types // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file dragging: - ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file") + ev.dataTransfer!.types.includes("Files") || + ev.dataTransfer!.types.includes("application/x-moz-file") ? true : state.dragging, })); @@ -68,6 +70,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { const onDragOver = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); + if (!ev.dataTransfer) return; ev.dataTransfer.dropEffect = "none"; @@ -82,6 +85,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { const onDrop = (ev: DragEvent): void => { ev.stopPropagation(); ev.preventDefault(); + if (!ev.dataTransfer) return; onFileDrop(ev.dataTransfer); setState((state) => ({ diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 1b53b7d293..fa3ce1a754 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -66,7 +66,7 @@ class FilePanel extends React.Component { private onRoomTimeline = ( ev: MatrixEvent, - room: Room | null, + room: Room | undefined, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData, @@ -78,7 +78,7 @@ class FilePanel extends React.Component { client.decryptEventIfNeeded(ev); if (ev.isBeingDecrypted()) { - this.decryptingEvents.add(ev.getId()); + this.decryptingEvents.add(ev.getId()!); } else { this.addEncryptedLiveEvent(ev); } @@ -86,7 +86,7 @@ class FilePanel extends React.Component { private onEventDecrypted = (ev: MatrixEvent, err?: any): void => { if (ev.getRoomId() !== this.props.roomId) return; - const eventId = ev.getId(); + const eventId = ev.getId()!; if (!this.decryptingEvents.delete(eventId)) return; if (err) return; @@ -103,7 +103,7 @@ class FilePanel extends React.Component { return; } - if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) { + if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { this.state.timelineSet.addEventToTimeline(ev, timeline, false); } } diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 13fc132516..bb2fbc068d 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -56,15 +56,15 @@ const getOwnProfile = ( userId: string, ): { displayName: string; - avatarUrl: string; + avatarUrl?: string; } => ({ displayName: OwnProfileStore.instance.displayName || userId, - avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), + avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE) ?? undefined, }); const UserWelcomeTop: React.FC = () => { const cli = useContext(MatrixClientContext); - const userId = cli.getUserId(); + const userId = cli.getUserId()!; const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId)); useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => { setOwnProfile(getOwnProfile(userId)); diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index fd83f0aea2..77113052f1 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -33,7 +33,7 @@ export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); type InteractiveAuthCallbackSuccess = ( success: true, - response: IAuthData, + response?: IAuthData, extra?: { emailSid?: string; clientSecret?: string }, ) => void; type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void; @@ -94,7 +94,7 @@ interface IState { export default class InteractiveAuthComponent extends React.Component { private readonly authLogic: InteractiveAuth; - private readonly intervalId: number = null; + private readonly intervalId: number | null = null; private readonly stageComponent = createRef(); private unmounted = false; @@ -103,10 +103,7 @@ export default class InteractiveAuthComponent extends React.Component { } >(); - let lastTopHeader; - let firstBottomHeader; + let lastTopHeader: HTMLDivElement | undefined; + let firstBottomHeader: HTMLDivElement | undefined; for (const sublist of sublists) { const header = sublist.querySelector(".mx_RoomSublist_stickable"); + if (!header) continue; // this should never occur header.style.removeProperty("display"); // always clear display:none first // When an element is <=40% off screen, make it take over @@ -196,7 +197,7 @@ export default class LeftPanel extends React.Component { // cause a no-op update, as adding/removing properties that are/aren't there cause // layout updates. for (const header of targetStyles.keys()) { - const style = targetStyles.get(header); + const style = targetStyles.get(header)!; if (style.makeInvisible) { // we will have already removed the 'display: none', so add it back. @@ -324,7 +325,7 @@ export default class LeftPanel extends React.Component { } private renderSearchDialExplore(): React.ReactNode { - let dialPadButton = null; + let dialPadButton: JSX.Element | undefined; // If we have dialer support, show a button to bring up the dial pad // to start a new call @@ -338,7 +339,7 @@ export default class LeftPanel extends React.Component { ); } - let rightButton: JSX.Element; + let rightButton: JSX.Element | undefined; if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) { rightButton = ; } else if (this.state.activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms)) { diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 5365352921..4abd4d8d19 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -35,7 +35,7 @@ const CONNECTING_STATES = [ CallState.CreateAnswer, ]; -const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing]; +const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing, CallState.Ended]; export enum CustomCallState { Missed = "missed", @@ -72,7 +72,7 @@ export function buildLegacyCallEventGroupers( export default class LegacyCallEventGrouper extends EventEmitter { private events: Set = new Set(); - private call: MatrixCall; + private call: MatrixCall | null = null; public state: CallState | CustomCallState; public constructor() { @@ -111,7 +111,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { } public get hangupReason(): string | null { - return this.hangup?.getContent()?.reason; + return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null; } public get rejectParty(): string { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 66f681476b..43c82b61ff 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -226,8 +226,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; @@ -296,9 +294,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(); @@ -439,7 +434,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(); } @@ -1987,13 +1982,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); @@ -2037,7 +2026,7 @@ export default class MatrixChat extends React.PureComponent { view = ( ); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index ba7562e648..c2fb700f36 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -114,8 +114,11 @@ 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"; 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"); @@ -483,6 +486,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 => { @@ -503,6 +507,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), @@ -1903,6 +1957,7 @@ export class RoomView extends React.Component { loading={loading} joining={this.state.joining} oobData={this.props.oobData} + roomId={this.state.roomId} /> @@ -1932,7 +1987,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} /> @@ -1969,6 +2024,7 @@ export class RoomView extends React.Component { error={this.state.roomLoadError} joining={this.state.joining} rejecting={this.state.rejecting} + roomId={this.state.roomId} /> ); @@ -1998,6 +2054,7 @@ export class RoomView extends React.Component { canPreview={false} joining={this.state.joining} room={this.state.room} + roomId={this.state.roomId} /> @@ -2090,6 +2147,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/ViewSource.tsx b/src/components/structures/ViewSource.tsx index faf445cef5..c9191a23a8 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -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/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 243d56cee1..c2c7a0d913 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, @@ -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/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index 9119743378..651921b233 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -65,7 +65,7 @@ export default class CaptchaForm extends React.Component{this.state.errorText}; } diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 2731503e38..0426c08f86 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/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index bb630b5fc2..727505551a 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/Welcome.tsx b/src/components/views/auth/Welcome.tsx index adcbdad25a..003a6cc509 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; if (pagesConfig) { pageUrl = pagesConfig.get("welcome_url"); } diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index a2fa704e8e..6c91cd1f39 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -158,12 +158,12 @@ const BeaconViewDialog: React.FC = ({ initialFocusedBeacon, roomId, matr )} {mapDisplayError && } {!centerGeoUri && !mapDisplayError && ( - + {_t("No live locations")} {_t("Close")} @@ -179,7 +179,7 @@ const BeaconViewDialog: React.FC = ({ initialFocusedBeacon, roomId, matr setSidebarOpen(true)} - data-test-id="beacon-view-dialog-open-sidebar" + data-testid="beacon-view-dialog-open-sidebar" className="mx_BeaconViewDialog_viewListButton" > diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index bef5cdc8bf..2cc0e094c3 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)); - revokeButton = ; + 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 = ; + } } 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/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx index 9b9982618a..a35971992e 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/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index af3fb65f6f..84ecc97863 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -36,7 +36,7 @@ interface IProps { interface IState { shouldLoadBackupStatus: boolean; loading: boolean; - backupInfo: IKeyBackupInfo; + backupInfo: IKeyBackupInfo | null; error?: string; } @@ -55,7 +55,6 @@ export default class LogoutDialog extends React.Component { shouldLoadBackupStatus: shouldLoadBackupStatus, loading: shouldLoadBackupStatus, backupInfo: null, - error: null, }; if (shouldLoadBackupStatus) { @@ -103,14 +102,20 @@ export default class LogoutDialog extends React.Component { // A key backup exists for this account, but the creating device is not // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) - Modal.createDialog(RestoreKeyBackupDialog, null, null, /* priority = */ false, /* static = */ true); + Modal.createDialog( + RestoreKeyBackupDialog, + undefined, + undefined, + /* priority = */ false, + /* static = */ true, + ); } else { Modal.createDialogAsync( import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise< ComponentType<{}> >, - null, - null, + undefined, + undefined, /* priority = */ false, /* static = */ true, ); diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index 7ddfb5d8ba..25f4110ec1 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -20,7 +20,7 @@ import React, { useContext } from "react"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { useNotificationState } from "../../../../hooks/useRoomNotificationState"; -import { _t } from "../../../../languageHandler"; +import { _t, _td } from "../../../../languageHandler"; import { determineUnreadState } from "../../../../RoomNotifs"; import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; @@ -39,22 +39,38 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme

    {_t("Room status")}

    • - {_t("Room unread status: ")} - {humanReadableNotificationColor(color)} - {count > 0 && ( - <> - {_t(", count:")} {count} - + {_t( + "Room unread status: %(status)s, count: %(count)s", + { + status: humanReadableNotificationColor(color), + count, + }, + { + strong: (sub) => {sub}, + }, )}
    • - {_t("Notification state is")} {notificationState} + {_t( + "Notification state is %(notificationState)s", + { + notificationState, + }, + { + strong: (sub) => {sub}, + }, + )}
    • - {_t("Room is ")} - - {cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")} - + {_t( + cli.isRoomEncrypted(room.roomId!) + ? _td("Room is encrypted ✅") + : _td("Room is not encrypted 🚨"), + {}, + { + strong: (sub) => {sub}, + }, + )}
    diff --git a/src/components/views/dialogs/polls/PollHistoryDialog.tsx b/src/components/views/dialogs/polls/PollHistoryDialog.tsx index e5525fbbaf..bd6c9fae8a 100644 --- a/src/components/views/dialogs/polls/PollHistoryDialog.tsx +++ b/src/components/views/dialogs/polls/PollHistoryDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; @@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog"; import { IDialogProps } from "../IDialogProps"; import { PollHistoryList } from "./PollHistoryList"; import { PollHistoryFilter } from "./types"; -import { usePolls } from "./usePollHistory"; +import { usePollsWithRelations } from "./usePollHistory"; +import { useFetchPastPolls } from "./fetchPastPolls"; type PollHistoryDialogProps = Pick & { roomId: string; @@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri const filterPolls = (filter: PollHistoryFilter) => (poll: Poll): boolean => - (filter === "ACTIVE") !== poll.isEnded; + // exclude polls while they are still loading + // to avoid jitter in list + !poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded; + const filterAndSortPolls = (polls: Map, filter: PollHistoryFilter): MatrixEvent[] => { return [...polls.values()] .filter(filterPolls(filter)) @@ -43,18 +47,24 @@ const filterAndSortPolls = (polls: Map, filter: PollHistoryFilter) }; export const PollHistoryDialog: React.FC = ({ roomId, matrixClient, onFinished }) => { - const { polls } = usePolls(roomId, matrixClient); + const room = matrixClient.getRoom(roomId)!; + const { isLoading } = useFetchPastPolls(room, matrixClient); + const { polls } = usePollsWithRelations(roomId, matrixClient); const [filter, setFilter] = useState("ACTIVE"); - const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter)); - useEffect(() => { - setPollStartEvents(filterAndSortPolls(polls, filter)); - }, [filter, polls]); + const pollStartEvents = filterAndSortPolls(polls, filter); + const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses); return (
    - +
    ); diff --git a/src/components/views/dialogs/polls/PollHistoryList.tsx b/src/components/views/dialogs/polls/PollHistoryList.tsx index 7c8714aeac..c686b82ebf 100644 --- a/src/components/views/dialogs/polls/PollHistoryList.tsx +++ b/src/components/views/dialogs/polls/PollHistoryList.tsx @@ -15,19 +15,41 @@ limitations under the License. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import classNames from "classnames"; +import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; -import PollListItem from "./PollListItem"; import { _t } from "../../../../languageHandler"; import { FilterTabGroup } from "../../elements/FilterTabGroup"; +import InlineSpinner from "../../elements/InlineSpinner"; import { PollHistoryFilter } from "./types"; +import { PollListItem } from "./PollListItem"; +import { PollListItemEnded } from "./PollListItemEnded"; + +const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => ( +
    + + {_t("Loading polls")} +
    +); type PollHistoryListProps = { pollStartEvents: MatrixEvent[]; + polls: Map; filter: PollHistoryFilter; onFilterChange: (filter: PollHistoryFilter) => void; + isLoading?: boolean; }; -export const PollHistoryList: React.FC = ({ pollStartEvents, filter, onFilterChange }) => { +export const PollHistoryList: React.FC = ({ + pollStartEvents, + polls, + filter, + isLoading, + onFilterChange, +}) => { return (
    @@ -39,19 +61,30 @@ export const PollHistoryList: React.FC = ({ pollStartEvent { id: "ENDED", label: "Past polls" }, ]} /> - {!!pollStartEvents.length ? ( -
      - {pollStartEvents.map((pollStartEvent) => ( - - ))} + {!!pollStartEvents.length && ( +
        + {pollStartEvents.map((pollStartEvent) => + filter === "ACTIVE" ? ( + + ) : ( + + ), + )} + {isLoading && }
      - ) : ( + )} + {!pollStartEvents.length && !isLoading && ( {filter === "ACTIVE" ? _t("There are no active polls in this room") : _t("There are no past polls in this room")} )} + {!pollStartEvents.length && isLoading && }
    ); }; diff --git a/src/components/views/dialogs/polls/PollListItem.tsx b/src/components/views/dialogs/polls/PollListItem.tsx index 49df399bd7..7a4ace08b0 100644 --- a/src/components/views/dialogs/polls/PollListItem.tsx +++ b/src/components/views/dialogs/polls/PollListItem.tsx @@ -25,7 +25,7 @@ interface Props { event: MatrixEvent; } -const PollListItem: React.FC = ({ event }) => { +export const PollListItem: React.FC = ({ event }) => { const pollEvent = event.unstableExtensibleEvent as unknown as PollStartEvent; if (!pollEvent) { return null; @@ -39,5 +39,3 @@ const PollListItem: React.FC = ({ event }) => { ); }; - -export default PollListItem; diff --git a/src/components/views/dialogs/polls/PollListItemEnded.tsx b/src/components/views/dialogs/polls/PollListItemEnded.tsx new file mode 100644 index 0000000000..8a6b2c9595 --- /dev/null +++ b/src/components/views/dialogs/polls/PollListItemEnded.tsx @@ -0,0 +1,127 @@ +/* +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, { useEffect, useState } from "react"; +import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; +import { MatrixEvent, Poll, PollEvent } from "matrix-js-sdk/src/matrix"; +import { Relations } from "matrix-js-sdk/src/models/relations"; + +import { Icon as PollIcon } from "../../../../../res/img/element-icons/room/composer/poll.svg"; +import { _t } from "../../../../languageHandler"; +import { formatLocalDateShort } from "../../../../DateUtils"; +import { allVotes, collectUserVotes, countVotes } from "../../messages/MPollBody"; +import { PollOption } from "../../polls/PollOption"; +import { Caption } from "../../typography/Caption"; + +interface Props { + event: MatrixEvent; + poll: Poll; +} + +type EndedPollState = { + winningAnswers: { + answer: PollAnswerSubevent; + voteCount: number; + }[]; + totalVoteCount: number; +}; +const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollState => { + const userVotes = collectUserVotes(allVotes(responseRelations)); + const votes = countVotes(userVotes, poll.pollEvent); + const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote, 0); + const winCount = Math.max(...votes.values()); + + return { + totalVoteCount, + winningAnswers: poll.pollEvent.answers + .filter((answer) => votes.get(answer.id) === winCount) + .map((answer) => ({ + answer, + voteCount: votes.get(answer.id) || 0, + })), + }; +}; + +/** + * Get deduplicated and validated poll responses + * Will use cached responses from Poll instance when existing + * Updates on changes to Poll responses (paging relations or from sync) + * Returns winning answers and total vote count + */ +const usePollVotes = (poll: Poll): Partial => { + const [results, setResults] = useState({ totalVoteCount: 0 }); + + useEffect(() => { + const getResponses = async (): Promise => { + const responseRelations = await poll.getResponses(); + setResults(getWinningAnswers(poll, responseRelations)); + }; + const onPollResponses = (responseRelations: Relations): void => + setResults(getWinningAnswers(poll, responseRelations)); + poll.on(PollEvent.Responses, onPollResponses); + + getResponses(); + + return () => { + poll.off(PollEvent.Responses, onPollResponses); + }; + }, [poll]); + + return results; +}; + +/** + * Render an ended poll with the winning answer and vote count + * @param event - the poll start MatrixEvent + * @param poll - Poll instance + */ +export const PollListItemEnded: React.FC = ({ event, poll }) => { + const pollEvent = poll.pollEvent; + const { winningAnswers, totalVoteCount } = usePollVotes(poll); + if (!pollEvent) { + return null; + } + const formattedDate = formatLocalDateShort(event.getTs()); + + return ( +
  • +
    + + {pollEvent.question.text} + {formattedDate} +
    + {!!winningAnswers?.length && ( +
    + {winningAnswers?.map(({ answer, voteCount }) => ( + + ))} +
    + )} +
    + {_t("Final result based on %(count)s votes", { count: totalVoteCount })} +
    +
  • + ); +}; diff --git a/src/components/views/dialogs/polls/fetchPastPolls.ts b/src/components/views/dialogs/polls/fetchPastPolls.ts new file mode 100644 index 0000000000..1d045d3d07 --- /dev/null +++ b/src/components/views/dialogs/polls/fetchPastPolls.ts @@ -0,0 +1,129 @@ +/* +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 { useEffect, useState } from "react"; +import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix"; +import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter"; +import { logger } from "matrix-js-sdk/src/logger"; + +/** + * Page timeline backwards until either: + * - event older than endOfHistoryPeriodTimestamp is encountered + * - end of timeline is reached + * @param timelineSet - timelineset to page + * @param matrixClient - client + * @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until + * @returns void + */ +const pagePolls = async ( + timelineSet: EventTimelineSet, + matrixClient: MatrixClient, + endOfHistoryPeriodTimestamp: number, +): Promise => { + const liveTimeline = timelineSet.getLiveTimeline(); + const events = liveTimeline.getEvents(); + const oldestEventTimestamp = events[0]?.getTs() || Date.now(); + const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS); + + if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) { + return; + } + + await matrixClient.paginateEventTimeline(liveTimeline, { + backwards: true, + }); + + return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp); +}; + +const ONE_DAY_MS = 60000 * 60 * 24; +/** + * Fetches timeline history for given number of days in past + * @param timelineSet - timelineset to page + * @param matrixClient - client + * @param historyPeriodDays - number of days of history to fetch, from current day + * @returns isLoading - true while fetching history + */ +const useTimelineHistory = ( + timelineSet: EventTimelineSet | null, + matrixClient: MatrixClient, + historyPeriodDays: number, +): { isLoading: boolean } => { + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!timelineSet) { + return; + } + const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays; + + const doFetchHistory = async (): Promise => { + setIsLoading(true); + try { + await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp); + } catch (error) { + logger.error("Failed to fetch room polls history", error); + } finally { + setIsLoading(false); + } + }; + doFetchHistory(); + }, [timelineSet, historyPeriodDays, matrixClient]); + + return { isLoading }; +}; + +const filterDefinition: IFilterDefinition = { + room: { + timeline: { + types: [M_POLL_START.name, M_POLL_START.altName], + }, + }, +}; + +/** + * Fetch poll start events in the last N days of room history + * @param room - room to fetch history for + * @param matrixClient - client + * @param historyPeriodDays - number of days of history to fetch, from current day + * @returns isLoading - true while fetching history + */ +export const useFetchPastPolls = ( + room: Room, + matrixClient: MatrixClient, + historyPeriodDays = 30, +): { isLoading: boolean } => { + const [timelineSet, setTimelineSet] = useState(null); + + useEffect(() => { + const filter = new Filter(matrixClient.getSafeUserId()); + filter.setDefinition(filterDefinition); + const getFilteredTimelineSet = async (): Promise => { + const filterId = await matrixClient.getOrCreateFilter(`POLL_HISTORY_FILTER_${room.roomId}}`, filter); + filter.filterId = filterId; + const timelineSet = room.getOrCreateFilteredTimelineSet(filter); + setTimelineSet(timelineSet); + }; + + getFilteredTimelineSet(); + }, [room, matrixClient]); + + const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays); + + return { isLoading }; +}; diff --git a/src/components/views/dialogs/polls/usePollHistory.ts b/src/components/views/dialogs/polls/usePollHistory.ts index 1da2b4ee1d..dafb241f19 100644 --- a/src/components/views/dialogs/polls/usePollHistory.ts +++ b/src/components/views/dialogs/polls/usePollHistory.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { useEffect, useState } from "react"; import { Poll, PollEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/client"; @@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter"; /** * Get poll instances from a room + * Updates to include new polls * @param roomId - id of room to retrieve polls for * @param matrixClient - client * @returns {Map} - Map of Poll instances @@ -37,9 +39,58 @@ export const usePolls = ( throw new Error("Cannot find room"); } - const polls = useEventEmitterState(room, PollEvent.New, () => room.polls); - - // @TODO(kerrya) watch polls for end events, trigger refiltering + // copy room.polls map so changes can be detected + const polls = useEventEmitterState(room, PollEvent.New, () => new Map(room.polls)); return { polls }; }; + +/** + * Get all poll instances from a room + * Fetch their responses (using cached poll responses) + * Updates on: + * - new polls added to room + * - new responses added to polls + * - changes to poll ended state + * @param roomId - id of room to retrieve polls for + * @param matrixClient - client + * @returns {Map} - Map of Poll instances + */ +export const usePollsWithRelations = ( + roomId: string, + matrixClient: MatrixClient, +): { + polls: Map; +} => { + const { polls } = usePolls(roomId, matrixClient); + const [pollsWithRelations, setPollsWithRelations] = useState>(polls); + + useEffect(() => { + const onPollUpdate = async (): Promise => { + // trigger rerender by creating a new poll map + setPollsWithRelations(new Map(polls)); + }; + if (polls) { + for (const poll of polls.values()) { + // listen to changes in responses and end state + poll.on(PollEvent.End, onPollUpdate); + poll.on(PollEvent.Responses, onPollUpdate); + // trigger request to get all responses + // if they are not already in cache + poll.getResponses(); + } + setPollsWithRelations(polls); + } + // unsubscribe + return () => { + if (polls) { + for (const poll of polls.values()) { + poll.off(PollEvent.End, onPollUpdate); + poll.off(PollEvent.Responses, onPollUpdate); + } + } + }; + }, [polls, setPollsWithRelations]); + + return { polls: pollsWithRelations }; +}; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index d8911876f5..24a0fb7b52 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -93,6 +93,7 @@ import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom"; import { shouldShowFeedback } from "../../../../utils/Feedback"; import RoomAvatar from "../../avatars/RoomAvatar"; import { useFeatureEnabled } from "../../../../hooks/useSettings"; +import { filterBoolean } from "../../../../utils/arrays"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons @@ -173,13 +174,13 @@ const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResul publicRoom, section: Section.PublicRooms, filter: [Filter.PublicRooms], - query: [ + query: filterBoolean([ publicRoom.room_id.toLowerCase(), publicRoom.canonical_alias?.toLowerCase(), publicRoom.name?.toLowerCase(), sanitizeHtml(publicRoom.topic?.toLowerCase() ?? "", { allowedTags: [] }), ...(publicRoom.aliases?.map((it) => it.toLowerCase()) || []), - ].filter(Boolean) as string[], + ]), }); const toRoomResult = (room: Room): IRoomResult => { diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 7bb1fe216b..e6bf53426b 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -23,6 +23,7 @@ import classNames from "classnames"; import { MatrixCapabilities } from "matrix-widget-api"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { logger } from "matrix-js-sdk/src/logger"; +import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; import AccessibleButton from "./AccessibleButton"; import { _t } from "../../../languageHandler"; @@ -36,7 +37,7 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu"; import PersistedElement, { getPersistKey } from "./PersistedElement"; import { WidgetType } from "../../../widgets/WidgetType"; import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget"; -import WidgetContextMenu from "../context_menus/WidgetContextMenu"; +import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import WidgetAvatar from "../avatars/WidgetAvatar"; import LegacyCallHandler from "../../../LegacyCallHandler"; import { IApp } from "../../../stores/WidgetStore"; @@ -50,6 +51,7 @@ import { Action } from "../../../dispatcher/actions"; import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { ModuleRunner } from "../../../modules/ModuleRunner"; interface IProps { app: IApp; @@ -162,6 +164,9 @@ export default class AppTile extends React.Component { private hasPermissionToLoad = (props: IProps): boolean => { if (this.usingLocalWidget()) return true; if (!props.room) return true; // user widgets always have permissions + const opts: ApprovalOpts = { approved: undefined }; + ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(this.props.app)); + if (opts.approved) return true; const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false); diff --git a/src/components/views/elements/EditableTextContainer.tsx b/src/components/views/elements/EditableTextContainer.tsx index 6e3132e226..e2f4298062 100644 --- a/src/components/views/elements/EditableTextContainer.tsx +++ b/src/components/views/elements/EditableTextContainer.tsx @@ -32,7 +32,7 @@ interface IProps { /* callback to update the value. Called with a single argument: the new * value. */ - onSubmit?: (value: string) => Promise<{} | void>; + onSubmit: (value: string) => Promise<{} | void>; /* should the input submit when focus is lost? */ blurToSubmit?: boolean; @@ -40,7 +40,7 @@ interface IProps { interface IState { busy: boolean; - errorString: string; + errorString: string | null; value: string; } @@ -72,7 +72,7 @@ export default class EditableTextContainer extends React.Component = ({ brandClass = `mx_SSOButton_brand_${brandName}`; icon = {brandName}; } else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) { - const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24); + const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24) ?? undefined; icon = {idp.name}; } diff --git a/src/components/views/elements/SearchWarning.tsx b/src/components/views/elements/SearchWarning.tsx index fec5eee37f..14ffcbd510 100644 --- a/src/components/views/elements/SearchWarning.tsx +++ b/src/components/views/elements/SearchWarning.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 { logger } from "matrix-js-sdk/src/logger"; import EventIndexPeg from "../../../indexing/EventIndexPeg"; @@ -31,13 +31,13 @@ export enum WarningKind { } interface IProps { - isRoomEncrypted: boolean; + isRoomEncrypted?: boolean; kind: WarningKind; } export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element { - if (!isRoomEncrypted) return null; - if (EventIndexPeg.get()) return null; + if (!isRoomEncrypted) return <>; + if (EventIndexPeg.get()) return <>; if (EventIndexPeg.error) { return ( @@ -69,8 +69,8 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El const brand = SdkConfig.get("brand"); const desktopBuilds = SdkConfig.getObject("desktop_builds"); - let text = null; - let logo = null; + let text: ReactNode | undefined; + let logo: JSX.Element | undefined; if (desktopBuilds.get("available")) { logo = ; const buildUrl = desktopBuilds.get("url"); @@ -116,7 +116,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El // for safety if (!text) { logger.warn("Unknown desktop builds warning kind: ", kind); - return null; + return <>; } return ( diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index e9e98ca615..0af6417145 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -26,7 +26,7 @@ interface IResult { text: string; } -interface IRule { +interface IRule { key: string; final?: boolean; skip?(this: T, data: Data, derivedData: D): boolean; @@ -90,14 +90,12 @@ export default function withValidation({ { value, focused, allowEmpty = true }: IFieldState, ): Promise { if (!value && allowEmpty) { - return { - valid: null, - feedback: null, - }; + return {}; } const data = { value, allowEmpty }; - const derivedData: D | undefined = deriveData ? await deriveData.call(this, data) : undefined; + // We know that if deriveData is set then D will not be undefined + const derivedData: D = (await deriveData?.call(this, data)) as D; const results: IResult[] = []; let valid = true; @@ -149,10 +147,7 @@ export default function withValidation({ // Hide feedback when not focused if (!focused) { - return { - valid, - feedback: null, - }; + return { valid }; } let details; diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index 4efeb77839..f4ffce911b 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -38,7 +38,7 @@ interface IProps { id: string; name: string; emojis: IEmoji[]; - selectedEmojis: Set; + selectedEmojis?: Set; heightBefore: number; viewportHeight: number; scrollTop: number; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 2e4a6dc0de..1d8c233696 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -26,6 +26,7 @@ import Search from "./Search"; import Preview from "./Preview"; import QuickReactions from "./QuickReactions"; import Category, { ICategory, CategoryKey } from "./Category"; +import { filterBoolean } from "../../../utils/arrays"; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -62,13 +63,12 @@ class EmojiPicker extends React.Component { this.state = { filter: "", - previewEmoji: null, scrollTop: 0, viewportHeight: 280, }; // Convert recent emoji characters to emoji data, removing unknowns and duplicates - this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); + this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode)))); this.memoizedDataByCategory = { recent: this.recentlyUsed, ...DATA_BY_CATEGORY, @@ -230,9 +230,9 @@ class EmojiPicker extends React.Component { }); }; - private onHoverEmojiEnd = (emoji: IEmoji): void => { + private onHoverEmojiEnd = (): void => { this.setState({ - previewEmoji: null, + previewEmoji: undefined, }); }; diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index 05460d2b70..6b14906948 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -42,9 +42,7 @@ interface IState { class QuickReactions extends React.Component { public constructor(props: IProps) { super(props); - this.state = { - hover: null, - }; + this.state = {}; } private onMouseEnter = (emoji: IEmoji): void => { @@ -55,7 +53,7 @@ class QuickReactions extends React.Component { private onMouseLeave = (): void => { this.setState({ - hover: null, + hover: undefined, }); }; diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 6dceaf2e40..207767ecf9 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -77,8 +77,8 @@ class ReactionPicker extends React.Component { if (!this.props.reactions) { return {}; } - const userId = MatrixClientPeg.get().getUserId(); - const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || new Set(); + const userId = MatrixClientPeg.get().getUserId()!; + const myAnnotations = this.props.reactions.getAnnotationsBySender()?.[userId] ?? new Set(); return Object.fromEntries( [...myAnnotations] .filter((event) => !event.isRedacted()) @@ -97,9 +97,9 @@ class ReactionPicker extends React.Component { this.props.onFinished(); const myReactions = this.getReactions(); if (myReactions.hasOwnProperty(reaction)) { - if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return; + if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return false; - MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId(), myReactions[reaction]); + MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]); dis.dispatch({ action: Action.FocusAComposer, context: this.context.timelineRenderingType, @@ -107,7 +107,7 @@ class ReactionPicker extends React.Component { // Tell the emoji picker not to bump this in the more frequently used list. return false; } else { - MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), EventType.Reaction, { + MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, { "m.relates_to": { rel_type: RelationType.Annotation, event_id: this.props.mxEvent.getId(), diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index ae0636aeec..edd6b2c4fc 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -32,7 +32,7 @@ class Search extends React.PureComponent { public componentDidMount(): void { // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout - window.setTimeout(() => this.inputRef.current.focus(), 0); + window.setTimeout(() => this.inputRef.current?.focus(), 0); } private onKeyDown = (ev: React.KeyboardEvent): void => { diff --git a/src/components/views/location/EnableLiveShare.tsx b/src/components/views/location/EnableLiveShare.tsx index 6b7e0036a9..62a44b12b9 100644 --- a/src/components/views/location/EnableLiveShare.tsx +++ b/src/components/views/location/EnableLiveShare.tsx @@ -29,7 +29,7 @@ interface Props { export const EnableLiveShare: React.FC = ({ onSubmit }) => { const [isEnabled, setEnabled] = useState(false); return ( -
    +
    {_t("Live location sharing")} @@ -43,13 +43,13 @@ export const EnableLiveShare: React.FC = ({ onSubmit }) => { )}

    = ({ timeout, onChange }) => { return ( { )} = ({ error, isMinimised, className, onFinished, onClick }) => (
    diff --git a/src/components/views/location/ShareDialogButtons.tsx b/src/components/views/location/ShareDialogButtons.tsx index 398d274c8d..e282660215 100644 --- a/src/components/views/location/ShareDialogButtons.tsx +++ b/src/components/views/location/ShareDialogButtons.tsx @@ -19,6 +19,7 @@ import React from "react"; import AccessibleButton from "../elements/AccessibleButton"; import { Icon as BackIcon } from "../../../../res/img/element-icons/caret-left.svg"; import { Icon as CloseIcon } from "../../../../res/img/element-icons/cancel-rounded.svg"; +import { _t } from "../../../languageHandler"; interface Props { onCancel: () => void; @@ -32,7 +33,8 @@ const ShareDialogButtons: React.FC = ({ onBack, onCancel, displayBack }) {displayBack && ( @@ -41,7 +43,8 @@ const ShareDialogButtons: React.FC = ({ onBack, onCancel, displayBack }) )} diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 54d8033dd0..b9cb345680 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -73,7 +73,7 @@ export default class DateSeparator extends React.Component { } public componentWillUnmount(): void { - SettingsStore.unwatchSetting(this.settingWatcherRef); + if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef); } private onContextMenuOpenClick = (e: React.MouseEvent): void => { @@ -89,7 +89,7 @@ export default class DateSeparator extends React.Component { private closeMenu = (): void => { this.setState({ - contextMenuPosition: null, + contextMenuPosition: undefined, }); }; @@ -181,7 +181,7 @@ export default class DateSeparator extends React.Component { }; private renderJumpToDateMenu(): React.ReactElement { - let contextMenu: JSX.Element; + let contextMenu: JSX.Element | undefined; if (this.state.contextMenuPosition) { contextMenu = ( > { - public render(): ReactNode { - return
    {_t("Unable to decrypt message")}
    ; - } +function getErrorMessage(mxEvent?: MatrixEvent): string { + return mxEvent?.isEncryptedDisabledForUnverifiedDevices + ? _t("The sender has blocked you from receiving this message") + : _t("Unable to decrypt message"); +} + +// A placeholder element for messages that could not be decrypted +export function DecryptionFailureBody({ mxEvent }: Partial): JSX.Element { + return
    {getErrorMessage(mxEvent)}
    ; } diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 622772d289..4678b1a2e0 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -191,6 +191,13 @@ export default class LegacyCallEvent extends React.PureComponent {this.props.timestamp}
    ); + } else if (hangupReason === CallErrorCode.AnsweredElsewhere) { + return ( +
    + {_t("Answered elsewhere")} + {this.props.timestamp} +
    + ); } let reason; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index c65a0de418..62c53af518 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -182,12 +182,14 @@ export default class MPollBody extends React.Component { private addListeners(): void { this.state.poll?.on(PollEvent.Responses, this.onResponsesChange); this.state.poll?.on(PollEvent.End, this.onRelationsChange); + this.state.poll?.on(PollEvent.UndecryptableRelations, this.render.bind(this)); } private removeListeners(): void { if (this.state.poll) { this.state.poll.off(PollEvent.Responses, this.onResponsesChange); this.state.poll.off(PollEvent.End, this.onRelationsChange); + this.state.poll.off(PollEvent.UndecryptableRelations, this.render.bind(this)); } } @@ -297,7 +299,9 @@ export default class MPollBody extends React.Component { const showResults = poll.isEnded || (disclosed && myVote !== undefined); let totalText: string; - if (poll.isEnded) { + if (showResults && poll.undecryptableRelationsCount) { + totalText = _t("Due to decryption errors, some votes may not be counted"); + } else if (poll.isEnded) { totalText = _t("Final result based on %(count)s votes", { count: totalVotes }); } else if (!disclosed) { totalText = _t("Results will be visible when the poll is ended"); @@ -384,7 +388,7 @@ export function allVotes(voteRelations: Relations): Array { * @param {string?} selected Local echo selected option for the userId * @returns a Map of user ID to their vote info */ -function collectUserVotes( +export function collectUserVotes( userResponses: Array, userId?: string | null | undefined, selected?: string | null | undefined, @@ -405,7 +409,7 @@ function collectUserVotes( return userVotes; } -function countVotes(userVotes: Map, pollStart: PollStartEvent): Map { +export function countVotes(userVotes: Map, pollStart: PollStartEvent): Map { const collected = new Map(); for (const response of userVotes.values()) { diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index 2ae6a73e86..bf8272a58f 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -21,7 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { _t } from "../../../languageHandler"; import { textForEvent } from "../../../TextForEvent"; +import { Caption } from "../typography/Caption"; import { IBodyProps } from "./IBodyProps"; import MPollBody from "./MPollBody"; @@ -105,5 +107,10 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro ); } - return ; + return ( +
    + {_t("Ended a poll")} + +
    + ); }); diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index b7a8f60831..a32747cd2e 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -41,7 +41,7 @@ import { MPollEndBody } from "./MPollEndBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; -import DecryptionFailureBody from "./DecryptionFailureBody"; +import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast"; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 37d9a6f97a..b184fd7ba5 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -40,7 +40,7 @@ import { E2EStatus } from "../../../utils/ShieldUtils"; import RoomContext from "../../../contexts/RoomContext"; import { UIComponent, UIFeature } from "../../../settings/UIFeature"; import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; -import WidgetContextMenu from "../context_menus/WidgetContextMenu"; +import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import { useRoomMemberCount } from "../../../hooks/useRoomMembers"; import { useFeatureEnabled } from "../../../hooks/useSettings"; import { usePinnedEvents } from "./PinnedMessagesCard"; diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 44c5393638..0e26ebcb6c 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -24,7 +24,7 @@ import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; import { useWidgets } from "./RoomSummaryCard"; import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; -import WidgetContextMenu from "../context_menus/WidgetContextMenu"; +import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import UIStore from "../../../stores/UIStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 3dd13ba40f..8c219023bd 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -399,7 +399,7 @@ export default class AliasSettings extends React.Component { return (
    @@ -450,7 +450,7 @@ export default class AliasSettings extends React.Component { /> {this.props.mxEvent.isRedacted() ? ( ) : this.props.mxEvent.isDecryptionFailure() ? ( - + ) : ( MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent) )} diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx index f7a0926120..83f7b179ea 100644 --- a/src/components/views/rooms/ExtraTile.tsx +++ b/src/components/views/rooms/ExtraTile.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020, 2021 The Matrix.org Foundation C.I.C. +Copyright 2020 - 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. @@ -21,8 +21,9 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../ import NotificationBadge from "./NotificationBadge"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import { ButtonEvent } from "../elements/AccessibleButton"; +import useHover from "../../../hooks/useHover"; -interface IProps { +interface ExtraTileProps { isMinimized: boolean; isSelected: boolean; displayName: string; @@ -31,83 +32,68 @@ interface IProps { onClick: (ev: ButtonEvent) => void; } -interface IState { - hover: boolean; -} +export default function ExtraTile({ + isSelected, + isMinimized, + notificationState, + displayName, + onClick, + avatar, +}: ExtraTileProps): JSX.Element { + const [, { onMouseOver, onMouseLeave }] = useHover(() => false); -export default class ExtraTile extends React.Component { - public constructor(props: IProps) { - super(props); + // XXX: We copy classes because it's easier + const classes = classNames({ + mx_ExtraTile: true, + mx_RoomTile: true, + mx_RoomTile_selected: isSelected, + mx_RoomTile_minimized: isMinimized, + }); - this.state = { - hover: false, - }; + let badge: JSX.Element | null = null; + if (notificationState) { + badge = ; } - private onTileMouseEnter = (): void => { - this.setState({ hover: true }); - }; + let name = displayName; + if (typeof name !== "string") name = ""; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - private onTileMouseLeave = (): void => { - this.setState({ hover: false }); - }; + const nameClasses = classNames({ + mx_RoomTile_title: true, + mx_RoomTile_titleHasUnreadEvents: notificationState?.isUnread, + }); - public render(): React.ReactElement { - // XXX: We copy classes because it's easier - const classes = classNames({ - mx_ExtraTile: true, - mx_RoomTile: true, - mx_RoomTile_selected: this.props.isSelected, - mx_RoomTile_minimized: this.props.isMinimized, - }); + let nameContainer: JSX.Element | null = ( +
    +
    + {name} +
    +
    + ); + if (isMinimized) nameContainer = null; - let badge; - if (this.props.notificationState) { - badge = ; - } + let Button = RovingAccessibleButton; + if (isMinimized) { + Button = RovingAccessibleTooltipButton; + } - let name = this.props.displayName; - if (typeof name !== "string") name = ""; - name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - - const nameClasses = classNames({ - mx_RoomTile_title: true, - mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread, - }); - - let nameContainer = ( -
    -
    - {name} + return ( + - - ); - } + + ); } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 3857f4bee6..86e0256275 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -241,7 +241,7 @@ export class MessageComposer extends React.Component { private waitForOwnMember(): void { // If we have the member already, do that - const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); + const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!); if (me) { this.setState({ me }); return; @@ -250,14 +250,14 @@ export class MessageComposer extends React.Component { // The members should already be loading, and loadMembersIfNeeded // will return the promise for the existing operation this.props.room.loadMembersIfNeeded().then(() => { - const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); + const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!); this.setState({ me }); }); } public componentWillUnmount(): void { VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); - dis.unregister(this.dispatcherRef); + if (this.dispatcherRef) dis.unregister(this.dispatcherRef); UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); @@ -268,12 +268,12 @@ export class MessageComposer extends React.Component { private onTombstoneClick = (ev: ButtonEvent): void => { ev.preventDefault(); - const replacementRoomId = this.context.tombstone.getContent()["replacement_room"]; + const replacementRoomId = this.context.tombstone?.getContent()["replacement_room"]; const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId); - let createEventId = null; + let createEventId: string | undefined; if (replacementRoom) { const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, ""); - if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); + if (createEvent?.getId()) createEventId = createEvent.getId(); } const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")]; @@ -408,7 +408,7 @@ export class MessageComposer extends React.Component { private onRecordingEndingSoon = ({ secondsLeft }: { secondsLeft: number }): void => { this.setState({ recordingTimeLeftSeconds: secondsLeft }); - window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000); + window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: undefined }), 3000); }; private setStickerPickerOpen = (isStickerPickerOpen: boolean): void => { diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index af7b5fc85d..319787109d 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -38,7 +38,7 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P // Don't show a badge if we don't need to if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) { - return null; + return <>; } const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); @@ -54,8 +54,8 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount, mx_NotificationBadge_highlighted: color >= NotificationColor.Red, mx_NotificationBadge_dot: isEmptyBadge, - mx_NotificationBadge_2char: symbol?.length > 0 && symbol?.length < 3, - mx_NotificationBadge_3char: symbol?.length > 2, + mx_NotificationBadge_2char: symbol && symbol.length > 0 && symbol.length < 3, + mx_NotificationBadge_3char: symbol && symbol.length > 2, }); if (props.onClick) { diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 006b0670df..29ca93eaf4 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -62,7 +62,7 @@ export default class PinnedEventTile extends React.Component { eventId: string, relationType: RelationType | string, eventType: EventType | string, - ): Relations => { + ): Relations | undefined => { if (eventId === this.props.event.getId()) { return this.relations.get(relationType)?.get(eventType); } @@ -71,7 +71,7 @@ export default class PinnedEventTile extends React.Component { public render(): React.ReactNode { const sender = this.props.event.getSender(); - let unpinButton = null; + let unpinButton: JSX.Element | undefined; if (this.props.onUnpinClicked) { unpinButton = ( 0) { remText = ( diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index 1ba0e9a19f..1a47719f58 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -133,7 +133,7 @@ export default class ReadReceiptMarker extends React.PureComponent
    +
    diff --git a/test/utils/location/parseGeoUri-test.ts b/test/utils/location/parseGeoUri-test.ts index 7e7a9020a8..027d0b3e85 100644 --- a/test/utils/location/parseGeoUri-test.ts +++ b/test/utils/location/parseGeoUri-test.ts @@ -35,9 +35,9 @@ describe("parseGeoUri", () => { longitude: 16.3695, altitude: 183, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -45,11 +45,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:48.198634,16.371648;crs=wgs84;u=40")).toEqual({ latitude: 48.198634, longitude: 16.371648, - altitude: undefined, + altitude: null, accuracy: 40, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -57,11 +57,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:90,-22.43;crs=WGS84")).toEqual({ latitude: 90, longitude: -22.43, - altitude: undefined, + altitude: null, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -69,11 +69,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:90,46")).toEqual({ latitude: 90, longitude: 46, - altitude: undefined, + altitude: null, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -81,11 +81,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:66,30;u=6.500;FOo=this%2dthat")).toEqual({ latitude: 66, longitude: 30, - altitude: undefined, + altitude: null, accuracy: 6.5, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -93,11 +93,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:66.0,30;u=6.5;foo=this-that>")).toEqual({ latitude: 66.0, longitude: 30, - altitude: undefined, + altitude: null, accuracy: 6.5, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -105,11 +105,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:70,20;foo=1.00;bar=white")).toEqual({ latitude: 70, longitude: 20, - altitude: undefined, + altitude: null, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -117,11 +117,11 @@ describe("parseGeoUri", () => { expect(parseGeoUri("geo:-7.5,20")).toEqual({ latitude: -7.5, longitude: 20, - altitude: undefined, + altitude: null, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); @@ -131,9 +131,9 @@ describe("parseGeoUri", () => { longitude: -20, altitude: 0, accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, + altitudeAccuracy: null, + heading: null, + speed: null, }); }); }); diff --git a/test/utils/permalinks/Permalinks-test.ts b/test/utils/permalinks/Permalinks-test.ts index 7d27d04673..0a50327c2c 100644 --- a/test/utils/permalinks/Permalinks-test.ts +++ b/test/utils/permalinks/Permalinks-test.ts @@ -91,7 +91,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should gracefully handle invalid MXIDs", () => { @@ -112,8 +112,8 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(3); - expect(creator.serverCandidates[0]).toBe("pl_95"); + expect(creator.serverCandidates!.length).toBe(3); + expect(creator.serverCandidates![0]).toBe("pl_95"); // we don't check the 2nd and 3rd servers because that is done by the next test }); @@ -128,15 +128,15 @@ describe("Permalinks", function () { ]); const creator = new RoomPermalinkCreator(room, null); creator.load(); - expect(creator.serverCandidates[0]).toBe("pl_95"); + expect(creator.serverCandidates![0]).toBe("pl_95"); member95.membership = "left"; // @ts-ignore illegal private property creator.onRoomStateUpdate(); - expect(creator.serverCandidates[0]).toBe("pl_75"); + expect(creator.serverCandidates![0]).toBe("pl_75"); member95.membership = "join"; // @ts-ignore illegal private property creator.onRoomStateUpdate(); - expect(creator.serverCandidates[0]).toBe("pl_95"); + expect(creator.serverCandidates![0]).toBe("pl_95"); }); it("should pick candidate servers based on user population", function () { @@ -152,10 +152,10 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(3); - expect(creator.serverCandidates[0]).toBe("first"); - expect(creator.serverCandidates[1]).toBe("second"); - expect(creator.serverCandidates[2]).toBe("third"); + expect(creator.serverCandidates!.length).toBe(3); + expect(creator.serverCandidates![0]).toBe("first"); + expect(creator.serverCandidates![1]).toBe("second"); + expect(creator.serverCandidates![2]).toBe("third"); }); it("should pick prefer candidate servers with higher power levels", function () { @@ -168,10 +168,10 @@ describe("Permalinks", function () { ]); const creator = new RoomPermalinkCreator(room); creator.load(); - expect(creator.serverCandidates.length).toBe(3); - expect(creator.serverCandidates[0]).toBe("first"); - expect(creator.serverCandidates[1]).toBe("second"); - expect(creator.serverCandidates[2]).toBe("third"); + expect(creator.serverCandidates!.length).toBe(3); + expect(creator.serverCandidates![0]).toBe("first"); + expect(creator.serverCandidates![1]).toBe("second"); + expect(creator.serverCandidates![2]).toBe("third"); }); it("should pick a maximum of 3 candidate servers", function () { @@ -186,7 +186,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(3); + expect(creator.serverCandidates!.length).toBe(3); }); it("should not consider IPv4 hosts", function () { @@ -195,7 +195,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should not consider IPv6 hosts", function () { @@ -204,7 +204,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should not consider IPv4 hostnames with ports", function () { @@ -213,7 +213,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should not consider IPv6 hostnames with ports", function () { @@ -222,7 +222,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should work with hostnames with ports", function () { @@ -232,8 +232,8 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(1); - expect(creator.serverCandidates[0]).toBe("example.org:8448"); + expect(creator.serverCandidates!.length).toBe(1); + expect(creator.serverCandidates![0]).toBe("example.org:8448"); }); it("should not consider servers explicitly denied by ACLs", function () { @@ -252,7 +252,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should not consider servers not allowed by ACLs", function () { @@ -271,7 +271,7 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(0); + expect(creator.serverCandidates!.length).toBe(0); }); it("should consider servers not explicitly banned by ACLs", function () { @@ -290,8 +290,8 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(1); - expect(creator.serverCandidates[0]).toEqual("evilcorp.com"); + expect(creator.serverCandidates!.length).toBe(1); + expect(creator.serverCandidates![0]).toEqual("evilcorp.com"); }); it("should consider servers not disallowed by ACLs", function () { @@ -310,8 +310,8 @@ describe("Permalinks", function () { const creator = new RoomPermalinkCreator(room); creator.load(); expect(creator.serverCandidates).toBeTruthy(); - expect(creator.serverCandidates.length).toBe(1); - expect(creator.serverCandidates[0]).toEqual("evilcorp.com"); + expect(creator.serverCandidates!.length).toBe(1); + expect(creator.serverCandidates![0]).toEqual("evilcorp.com"); }); it("should generate an event permalink for room IDs with no candidate servers", function () { diff --git a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx index cd26723226..988d2b52e6 100644 --- a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx +++ b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx @@ -12,7 +12,6 @@ limitations under the License. */ import React from "react"; -import { Container } from "react-dom"; import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { render, RenderResult } from "@testing-library/react"; @@ -33,7 +32,7 @@ describe("VoiceBroadcastHeader", () => { let client: MatrixClient; let room: Room; const sender = new RoomMember(roomId, userId); - let container: Container; + let container: RenderResult["container"]; const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast?: boolean, buffering?: boolean): RenderResult => { return render( diff --git a/yarn.lock b/yarn.lock index dc7f346b89..4ec4f4ccca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1598,10 +1598,10 @@ version "3.2.14" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" -"@matrix-org/react-sdk-module-api@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.3.tgz#a7ac1b18a72d18d08290b81fa33b0d8d00a77d2b" - integrity sha512-jQmLhVIanuX0g7Jx1OIqlzs0kp72PfSpv3umi55qVPYcAPQmO252AUs0vncatK8O4e013vohdnNhly19a/kmLQ== +"@matrix-org/react-sdk-module-api@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.4.tgz#da71fc2e4c8143e87b5c2bc067ccbc0c146816fe" + integrity sha512-4gcgef3Ne9+Ae0bAErK1Swo9FxTZBDEogX/Iu2kcLWWROOKMjmeWL2PkM83ylsxZ32YY6a6ndRqV/SwRmDeJxg== dependencies: "@babel/runtime" "^7.17.9" @@ -2336,13 +2336,6 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-test-renderer@^17.0.1": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf" - integrity sha512-+F1KONQTBHDBBhbHuT2GNydeMpPuviduXIVJRB7Y4nma4NR5DrTJfMMZ+jbhEHbpwL+Uqhs1WXh4KHiyrtYTPg== - dependencies: - "@types/react" "^17" - "@types/react-transition-group@^4.4.0": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -2364,12 +2357,12 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sanitize-html@^2.3.1": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9" - integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ== +"@types/sanitize-html@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.8.0.tgz#c53d3114d832734fc299568a3458a49f9edc1eef" + integrity sha512-Uih6caOm3DsBYnVGOYn0A9NoTNe1c4aPStmHC/YA2JrpP9kx//jzaRcIklFvSpvVQEcpl/ZCr4DgISSf/YxTvg== dependencies: - htmlparser2 "^6.0.0" + htmlparser2 "^8.0.0" "@types/scheduler@*": version "0.16.2" @@ -2443,13 +2436,13 @@ integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== "@typescript-eslint/eslint-plugin@^5.35.1": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a" - integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ== + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz#24b8b4a952f3c615fe070e3c461dd852b5056734" + integrity sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw== dependencies: - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/type-utils" "5.51.0" - "@typescript-eslint/utils" "5.51.0" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/type-utils" "5.53.0" + "@typescript-eslint/utils" "5.53.0" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2459,71 +2452,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271" - integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA== + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.53.0.tgz#a1f2b9ae73b83181098747e96683f1b249ecab52" + integrity sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ== dependencies: - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/typescript-estree" "5.53.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990" - integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== +"@typescript-eslint/scope-manager@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef" + integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== dependencies: - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/visitor-keys" "5.51.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/visitor-keys" "5.53.0" -"@typescript-eslint/type-utils@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988" - integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ== +"@typescript-eslint/type-utils@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz#41665449935ba9b4e6a1ba6e2a3f4b2c31d6cf97" + integrity sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw== dependencies: - "@typescript-eslint/typescript-estree" "5.51.0" - "@typescript-eslint/utils" "5.51.0" + "@typescript-eslint/typescript-estree" "5.53.0" + "@typescript-eslint/utils" "5.53.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90" - integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== +"@typescript-eslint/types@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" + integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== -"@typescript-eslint/typescript-estree@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de" - integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== +"@typescript-eslint/typescript-estree@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" + integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== dependencies: - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/visitor-keys" "5.51.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/visitor-keys" "5.53.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47" - integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== +"@typescript-eslint/utils@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.53.0.tgz#e55eaad9d6fffa120575ffaa530c7e802f13bce8" + integrity sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/scope-manager" "5.53.0" + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/typescript-estree" "5.53.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87" - integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== +"@typescript-eslint/visitor-keys@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" + integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== dependencies: - "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/types" "5.53.0" eslint-visitor-keys "^3.3.0" "@wojtekmaj/enzyme-adapter-react-17@^0.8.0": @@ -2591,6 +2584,11 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-errors@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-3.0.0.tgz#e54f299f3a3d30fe144161e5f0d8d51196c527bc" + integrity sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ== + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -2616,7 +2614,7 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^8.6.2: +ajv@^8.11.2, ajv@^8.6.2: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3846,15 +3844,6 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -3864,7 +3853,7 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: +domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== @@ -3876,13 +3865,6 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" @@ -3890,15 +3872,6 @@ domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^2.5.2: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - domutils@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" @@ -3980,11 +3953,6 @@ enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" @@ -5137,17 +5105,7 @@ html-tags@^3.2.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== -htmlparser2@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.1: +htmlparser2@^8.0.0, htmlparser2@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA== @@ -6532,6 +6490,14 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== +matrix-events-sdk@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-2.0.0.tgz#f5f8dafbe4eae07fdbb628627f430ca5b1fd8c7a" + integrity sha512-UZbifYIO2o9+sNn4YuGjhMof/88TG68PyecKnH/pt8V3MFq0RZsbBUe+3/k5ZeVcEtr0pQLmcKB7d8aQVsVO/w== + dependencies: + ajv "^8.11.2" + ajv-errors "^3.0.0" + "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "23.3.0" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/108f1573249b733870d287e11ce38531a73e15b0" @@ -7534,7 +7500,7 @@ react-shallow-renderer@^16.13.1: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" -react-test-renderer@^17.0.0, react-test-renderer@^17.0.2: +react-test-renderer@^17.0.0: version "17.0.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== @@ -7862,14 +7828,14 @@ sanitize-filename@^1.6.3: dependencies: truncate-utf8-bytes "^1.0.0" -sanitize-html@^2.3.2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.3.tgz#166c868444ee4f9fd7352ac8c63fa86c343fc2bd" - integrity sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw== +sanitize-html@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.8.0.tgz#651d1d0e5b2d61b4ec6147cc46f6d6680eef98ce" + integrity sha512-ZsGyc6avnqgvEm3eMKrcy8xa7WM1MrGrfkGsUgQee2CU+vg3PCfNCexXwBDF/6dEPvaQ4k/QqRjnYKHL8xgNjg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^6.0.0" + htmlparser2 "^8.0.0" is-plain-object "^5.0.0" parse-srcset "^1.0.2" postcss "^8.3.11"