diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index cbb5347173..ad4f240eb9 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -43,7 +43,7 @@ jobs: - name: Get commit details id: commit if: github.event.workflow_run.event == 'pull_request' - uses: actions/github-script@v5 + uses: actions/github-script@v6 with: script: | const response = await github.rest.git.getCommit({ @@ -82,7 +82,7 @@ jobs: # Run 4 instances in Parallel runner: [1, 2, 3, 4] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: # XXX: We're checking out untrusted code in a secure context # We need to be careful to not trust anything this code outputs/may do @@ -96,7 +96,6 @@ jobs: - name: 📥 Download artifact uses: dawidd6/action-download-artifact@v2 with: - workflow: element-build-and-test.yaml run_id: ${{ github.event.workflow_run.id }} name: previewbuild path: webapp @@ -147,7 +146,7 @@ jobs: - name: Upload Artifact if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: cypress-results path: | diff --git a/.github/workflows/element-web.yaml b/.github/workflows/element-web.yaml index b1bb128457..ff8c8ddee7 100644 --- a/.github/workflows/element-web.yaml +++ b/.github/workflows/element-web.yaml @@ -17,7 +17,7 @@ jobs: name: "Build Element-Web" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -46,7 +46,7 @@ jobs: working-directory: ./element-web - name: Upload Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: previewbuild path: element-web/webapp diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index 2acfb12685..cd350fc892 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -12,7 +12,7 @@ jobs: - name: "Get modified files" id: changed_files if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' - uses: tj-actions/changed-files@v19 + uses: tj-actions/changed-files@v34 with: files: | src/i18n/strings/* diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index 4b45c95a89..1ebbe13747 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'matrix-org/matrix-react-sdk' steps: - name: Notify element-web repo that a new SDK build is on develop - uses: peter-evans/repository-dispatch@v1 + uses: peter-evans/repository-dispatch@v2 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} repository: vector-im/element-web diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 4c773f9258..36ebf978ea 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -14,7 +14,7 @@ jobs: name: "Typescript Syntax Check" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -89,7 +89,7 @@ jobs: name: "Rethemendex Check" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: ./res/css/rethemendex.sh @@ -99,7 +99,7 @@ jobs: name: "ESLint" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -116,7 +116,7 @@ jobs: name: "Style Lint" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -133,7 +133,7 @@ jobs: name: "Analyse Dead Code" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1985610ae3..9412024d46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Yarn cache uses: actions/setup-node@v3 @@ -38,7 +38,7 @@ jobs: run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}" - name: Upload Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: | @@ -49,7 +49,7 @@ jobs: name: Element Web Integration Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 650f8d585c..2cfb7ba1a8 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -91,6 +91,17 @@ const bobJoin = function(this: CryptoTestContext) { cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist"); }; +/** configure the given MatrixClient to auto-accept any invites */ +function autoJoin(client: MatrixClient) { + cy.window({ log: false }).then(async win => { + client.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => { + if (member.membership === "invite" && member.userId === client.getUserId()) { + client.joinRoom(member.roomId); + } + }); + }); +} + const handleVerificationRequest = (request: VerificationRequest): Chainable => { return cy.wrap(new Promise((resolve) => { const onShowSas = (event: ISasEvent) => { @@ -174,4 +185,22 @@ describe("Cryptography", function() { testMessages.call(this); verify.call(this); }); + + it("should allow verification when there is no existing DM", function(this: CryptoTestContext) { + cy.bootstrapCrossSigning(); + autoJoin(this.bob); + + /* we need to have a room with the other user present, so we can open the verification panel */ + let roomId: string; + cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then(_room1Id => { + roomId = _room1Id; + cy.log(`Created test room ${roomId}`); + cy.visit(`/#/room/${roomId}`); + // wait for Bob to join the room, otherwise our attempt to open his user details may race + // with his join. + cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist"); + }); + + verify.call(this); + }); }); diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts index 1058287010..32a3babced 100644 --- a/cypress/e2e/login/login.spec.ts +++ b/cypress/e2e/login/login.spec.ts @@ -21,6 +21,10 @@ import { SynapseInstance } from "../../plugins/synapsedocker"; describe("Login", () => { let synapse: SynapseInstance; + beforeEach(() => { + cy.stubDefaultServer(); + }); + afterEach(() => { cy.stopSynapse(synapse); }); diff --git a/cypress/e2e/register/register.spec.ts b/cypress/e2e/register/register.spec.ts index 98ef2bd729..dacfe08bf8 100644 --- a/cypress/e2e/register/register.spec.ts +++ b/cypress/e2e/register/register.spec.ts @@ -22,6 +22,7 @@ describe("Registration", () => { let synapse: SynapseInstance; beforeEach(() => { + cy.stubDefaultServer(); cy.visit("/#/register"); cy.startSynapse("consent").then(data => { synapse = data; diff --git a/cypress/fixtures/matrix-org-client-login.json b/cypress/fixtures/matrix-org-client-login.json new file mode 100644 index 0000000000..d7c4fde1e5 --- /dev/null +++ b/cypress/fixtures/matrix-org-client-login.json @@ -0,0 +1,48 @@ +{ + "flows": [ + { + "type": "m.login.sso", + "identity_providers": [ + { + "id": "oidc-github", + "name": "GitHub", + "icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP", + "brand": "github" + }, + { + "id": "oidc-google", + "name": "Google", + "icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz", + "brand": "google" + }, + { + "id": "oidc-gitlab", + "name": "GitLab", + "icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq", + "brand": "gitlab" + }, + { + "id": "oidc-facebook", + "name": "Facebook", + "icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG", + "brand": "facebook" + }, + { + "id": "oidc-apple", + "name": "Apple", + "icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU", + "brand": "apple" + } + ] + }, + { + "type": "m.login.token" + }, + { + "type": "m.login.password" + }, + { + "type": "m.login.application_service" + } + ] +} diff --git a/cypress/fixtures/matrix-org-client-versions.json b/cypress/fixtures/matrix-org-client-versions.json new file mode 100644 index 0000000000..0e0cfae33d --- /dev/null +++ b/cypress/fixtures/matrix-org-client-versions.json @@ -0,0 +1,39 @@ +{ + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2", + "v1.3", + "v1.4" + ], + "unstable_features": { + "org.matrix.label_based_filtering": true, + "org.matrix.e2e_cross_signing": true, + "org.matrix.msc2432": true, + "uk.half-shot.msc2666.mutual_rooms": true, + "io.element.e2ee_forced.public": false, + "io.element.e2ee_forced.private": false, + "io.element.e2ee_forced.trusted_private": false, + "org.matrix.msc3026.busy_presence": false, + "org.matrix.msc2285.stable": true, + "org.matrix.msc3827.stable": true, + "org.matrix.msc2716": false, + "org.matrix.msc3030": false, + "org.matrix.msc3440.stable": true, + "org.matrix.msc3771": true, + "org.matrix.msc3773": false, + "fi.mau.msc2815": false, + "org.matrix.msc3882": false, + "org.matrix.msc3881": false, + "org.matrix.msc3874": false, + "org.matrix.msc3886": false, + "org.matrix.msc3912": false + } + } diff --git a/cypress/fixtures/matrix-org-client-well-known.json b/cypress/fixtures/matrix-org-client-well-known.json new file mode 100644 index 0000000000..ed726e2421 --- /dev/null +++ b/cypress/fixtures/matrix-org-client-well-known.json @@ -0,0 +1,8 @@ +{ + "m.homeserver": { + "base_url": "https://matrix-client.matrix.org" + }, + "m.identity_server": { + "base_url": "https://vector.im" + } +} diff --git a/cypress/fixtures/vector-im-identity-v1.json b/cypress/fixtures/vector-im-identity-v1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cypress/fixtures/vector-im-identity-v1.json @@ -0,0 +1 @@ +{} diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index 26f0aa497e..6161b11cdf 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -78,6 +78,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): const username = Cypress._.uniqueId("userId_"); const password = Cypress._.uniqueId("password_"); return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => { + cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`); return cy.window({ log: false }).then(win => { const cli = new win.matrixcs.MatrixClient({ baseUrl: synapse.baseUrl, diff --git a/cypress/support/login.ts b/cypress/support/login.ts index 6c44158941..4e1e50456f 100644 --- a/cypress/support/login.ts +++ b/cypress/support/login.ts @@ -103,6 +103,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str return cy.registerUser(synapse, username, password, displayName).then(() => { return cy.loginUser(synapse, username, password); }).then(response => { + cy.log(`Registered test user ${username} with displayname ${displayName}`); cy.window({ log: false }).then(win => { // Seed the localStorage with the required credentials win.localStorage.setItem("mx_hs_url", synapse.baseUrl); diff --git a/cypress/support/network.ts b/cypress/support/network.ts index 73df049c6c..238c847184 100644 --- a/cypress/support/network.ts +++ b/cypress/support/network.ts @@ -20,10 +20,12 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { - // Intercept all /_matrix/ networking requests for the logged in user and fail them + // Intercept all /_matrix/ networking requests for the logged-in user and fail them goOffline(): void; // Remove intercept on all /_matrix/ networking requests goOnline(): void; + // Intercept calls to vector.im/matrix.org so a login page can be shown offline + stubDefaultServer(): void; } } } @@ -58,5 +60,29 @@ Cypress.Commands.add("goOnline", (): void => { }); }); +Cypress.Commands.add("stubDefaultServer", (): void => { + cy.log("Stubbing vector.im and matrix.org network calls"); + // We intercept vector.im & matrix.org calls so that tests don't fail when it has issues + cy.intercept("GET", "https://vector.im/_matrix/identity/api/v1", { + fixture: "vector-im-identity-v1.json", + }); + cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", { + fixture: "matrix-org-client-well-known.json", + }); + cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/versions", { + fixture: "matrix-org-client-versions.json", + }); + cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/r0/login", { + fixture: "matrix-org-client-login.json", + }); + cy.intercept("POST", "https://matrix-client.matrix.org/_matrix/client/r0/register?kind=guest", { + statusCode: 403, + body: { + errcode: "M_FORBIDDEN", + error: "Registration is not enabled on this homeserver.", + }, + }); +}); + // Needed to make this file a module export { }; diff --git a/package.json b/package.json index f9733e7890..110d4fc6eb 100644 --- a/package.json +++ b/package.json @@ -57,10 +57,10 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.6.0", + "@matrix-org/matrix-wysiwyg": "^0.8.0", "@matrix-org/react-sdk-module-api": "^0.0.3", - "@sentry/browser": "^6.11.0", - "@sentry/tracing": "^6.11.0", + "@sentry/browser": "^7.0.0", + "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@types/geojson": "^7946.0.8", "@types/ua-parser-js": "^0.7.36", @@ -72,18 +72,18 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase": "6.0.2", - "emojibase-data": "7.0.0", - "emojibase-regex": "6.0.0", + "emojibase": "6.1.0", + "emojibase-data": "7.0.1", + "emojibase-regex": "6.0.1", "escape-html": "^1.0.3", "file-saver": "^2.0.5", - "filesize": "6.1.0", - "flux": "2.1.1", + "filesize": "10.0.5", + "flux": "4.0.3", "focus-visible": "^5.2.0", "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", "highlight.js": "^11.3.1", - "html-entities": "^1.4.0", + "html-entities": "^2.0.0", "is-ip": "^3.1.0", "jszip": "^3.7.0", "katex": "^0.12.0", @@ -102,7 +102,6 @@ "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", "posthog-js": "1.12.2", - "prop-types": "^15.7.2", "qrcode": "1.4.4", "re-resizable": "^6.9.0", "react": "17.0.2", @@ -141,7 +140,7 @@ "@peculiar/webcrypto": "^1.4.1", "@percy/cli": "^1.11.0", "@percy/cypress": "^3.1.2", - "@sentry/types": "^6.10.0", + "@sentry/types": "^7.0.0", "@sinonjs/fake-timers": "^9.1.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", @@ -149,7 +148,7 @@ "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", - "@types/css-font-loading-module": "^0.0.6", + "@types/css-font-loading-module": "^0.0.7", "@types/diff-match-patch": "^1.0.32", "@types/enzyme": "^3.10.9", "@types/escape-html": "^1.0.1", @@ -160,8 +159,8 @@ "@types/katex": "^0.14.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", - "@types/node": "^14.18.28", - "@types/pako": "^1.0.1", + "@types/node": "^16", + "@types/pako": "^2.0.0", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "17.0.49", @@ -175,11 +174,11 @@ "@typescript-eslint/parser": "^5.6.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "allchange": "^1.1.0", - "axe-core": "^4.4.3", - "babel-jest": "^26.6.3", - "blob-polyfill": "^6.0.20211015", + "axe-core": "4.4.3", + "babel-jest": "^29.0.0", + "blob-polyfill": "^7.0.0", "chokidar": "^3.5.1", - "cypress": "^10.3.0", + "cypress": "^11.0.0", "cypress-axe": "^1.0.0", "cypress-real-events": "^1.7.1", "enzyme": "^3.11.0", @@ -192,10 +191,10 @@ "eslint-plugin-matrix-org": "^0.7.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^44.0.2", + "eslint-plugin-unicorn": "^45.0.0", "fetch-mock-jest": "^1.5.1", - "fs-extra": "^10.0.1", - "glob": "^7.1.6", + "fs-extra": "^11.0.0", + "glob": "^8.0.0", "jest": "^29.2.2", "jest-canvas-mock": "^2.3.0", "jest-environment-jsdom": "^29.2.2", @@ -210,9 +209,9 @@ "rimraf": "^3.0.2", "rrweb-snapshot": "1.1.7", "stylelint": "^14.9.1", - "stylelint-config-standard": "^26.0.0", + "stylelint-config-standard": "^29.0.0", "stylelint-scss": "^4.2.0", - "typescript": "4.8.4", + "typescript": "4.9.3", "walk": "^2.3.14" }, "jest": { diff --git a/res/css/views/beta/_BetaCard.pcss b/res/css/views/beta/_BetaCard.pcss index b47e7ca1b6..0f8d8a66e7 100644 --- a/res/css/views/beta/_BetaCard.pcss +++ b/res/css/views/beta/_BetaCard.pcss @@ -114,6 +114,10 @@ limitations under the License. } } } + + &:last-child { + margin-bottom: 0; + } } .mx_BetaCard_betaPill { diff --git a/res/css/views/elements/_SettingsFlag.pcss b/res/css/views/elements/_SettingsFlag.pcss index a581edae67..83c78ef39e 100644 --- a/res/css/views/elements/_SettingsFlag.pcss +++ b/res/css/views/elements/_SettingsFlag.pcss @@ -60,4 +60,8 @@ limitations under the License. font-family: $monospace-font-family !important; background-color: $rte-code-bg-color; } + + .mx_SettingsTab_microcopy_warning::before { + content: "⚠️ "; + } } diff --git a/res/css/views/rooms/_EventBubbleTile.pcss b/res/css/views/rooms/_EventBubbleTile.pcss index ca9ec513f8..6b288cd91e 100644 --- a/res/css/views/rooms/_EventBubbleTile.pcss +++ b/res/css/views/rooms/_EventBubbleTile.pcss @@ -43,7 +43,9 @@ limitations under the License. --EventTile_bubble_gap-inline: 5px; position: relative; - margin-top: var(--gutterSize); + /* Other half of the gutter is provided by margin-bottom on the last tile + of the section */ + margin-top: calc(var(--gutterSize) / 2); margin-left: var(--EventTile_bubble-margin-inline-start); font-size: $font-14px; diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 55702c787b..58e04bc17d 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -462,6 +462,11 @@ $left-gutter: 64px; &.mx_EventTile_continuation { margin-top: 2px; } + &.mx_EventTile_lastInSection { + /* Other half of the gutter is provided by margin-top on the first + tile of the section */ + margin-bottom: calc(var(--gutterSize) / 2); + } } } diff --git a/res/css/views/rooms/_ReplyTile.pcss b/res/css/views/rooms/_ReplyTile.pcss index fe6235eb1e..616f1b181f 100644 --- a/res/css/views/rooms/_ReplyTile.pcss +++ b/res/css/views/rooms/_ReplyTile.pcss @@ -28,8 +28,11 @@ limitations under the License. } > a { - display: flex; - flex-direction: column; + display: grid; + grid-template: + "sender" auto + "message" auto + / auto; text-decoration: none; color: $secondary-content; transition: color ease 0.15s; @@ -58,6 +61,7 @@ limitations under the License. /* We do reply size limiting with CSS to avoid duplicating the TextualBody component. */ .mx_EventTile_content { + grid-area: message; $reply-lines: 2; $line-height: $font-18px; @@ -102,7 +106,16 @@ limitations under the License. padding-top: 0; } + &.mx_ReplyTile_inline > a { + /* Render replies to emotes inline with the sender avatar */ + grid-template: + "sender message" auto + / max-content auto; + gap: 4px; // increase spacing + } + .mx_ReplyTile_sender { + grid-area: sender; display: flex; align-items: center; gap: 4px; diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss index 1ff29bd985..90092a35ac 100644 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss @@ -40,8 +40,9 @@ limitations under the License. display: flex; gap: $spacing-4; - i { - flex-shrink: 0; + .mx_Spinner { + flex: 0 0 14px; + padding: 1px; } span { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index c4971d24f1..99d963ac9b 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -149,14 +149,10 @@ declare global { // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas interface OffscreenCanvas { - height: number; - width: number; - getContext: HTMLCanvasElement["getContext"]; convertToBlob(opts?: { type?: string; quality?: number; }): Promise; - transferToImageBitmap(): ImageBitmap; } interface HTMLAudioElement { diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index b0d9b7ef58..1b01b906b5 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -174,12 +174,12 @@ export class DecryptionFailureTracker { * Start checking for and tracking failures. */ public start(): void { - this.checkInterval = setInterval( + this.checkInterval = window.setInterval( () => this.checkFailures(Date.now()), DecryptionFailureTracker.CHECK_INTERVAL_MS, ); - this.trackInterval = setInterval( + this.trackInterval = window.setInterval( () => this.trackFailures(), DecryptionFailureTracker.TRACK_INTERVAL_MS, ); diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 1f49c3b34d..ce1a0a26f0 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -47,6 +47,7 @@ import { removeClientInformation, } from "./utils/device/clientInformation"; import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; +import { UIFeature } from "./settings/UIFeature"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -68,6 +69,7 @@ export default class DeviceListener { private displayingToastsForDeviceIds = new Set(); private running = false; private shouldRecordClientInformation = false; + private enableBulkUnverifiedSessionsReminder = true; private deviceClientInformationSettingWatcherRef: string | undefined; public static sharedInstance() { @@ -86,6 +88,8 @@ export default class DeviceListener { MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync); MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents); this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn'); + // only configurable in config, so we don't need to watch the value + this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder); this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting( 'deviceClientInformationOptIn', null, @@ -306,6 +310,9 @@ export default class DeviceListener { // Unverified devices that have appeared since then const newUnverifiedDeviceIds = new Set(); + const isCurrentDeviceTrusted = crossSigningReady && + await (cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!)).isCrossSigningVerified(); + // as long as cross-signing isn't ready, // you can't see or dismiss any device toasts if (crossSigningReady) { @@ -313,7 +320,7 @@ export default class DeviceListener { for (const device of devices) { if (device.deviceId === cli.deviceId) continue; - const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); + const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!); if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { if (this.ourDeviceIdsAtStart.has(device.deviceId)) { oldUnverifiedDeviceIds.add(device.deviceId); @@ -329,7 +336,12 @@ export default class DeviceListener { logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(',')); // Display or hide the batch toast for old unverified sessions - if (oldUnverifiedDeviceIds.size > 0) { + // don't show the toast if the current device is unverified + if ( + oldUnverifiedDeviceIds.size > 0 + && isCurrentDeviceTrusted + && this.enableBulkUnverifiedSessionsReminder + ) { showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); } else { hideBulkUnverifiedSessionsToast(); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c94718ba46..fa91ba7321 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; import { split } from 'lodash'; import katex from 'katex'; -import { AllHtmlEntities } from 'html-entities'; +import { decode } from 'html-entities'; import { IContent } from 'matrix-js-sdk/src/models/event'; import { Optional } from 'matrix-events-sdk'; @@ -518,7 +518,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // Cheerio instance to be returned. phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { return katex.renderToString( - AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), + decode(phtml(e).attr('data-mx-maths')), { throwOnError: false, // @ts-ignore - `e` can be an Element, not just a Node diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 41098dcb4d..ef1effdaf7 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -71,13 +71,52 @@ export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual'; const CHECK_PROTOCOLS_ATTEMPTS = 3; -enum AudioID { +type MediaEventType = keyof HTMLMediaElementEventMap; +const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [ + 'error', + // The media has become empty; for example, this event is sent if the media has + // already been loaded (or partially loaded), and the HTMLMediaElement.load method + // is called to reload it. + 'emptied', + // The user agent is trying to fetch media data, but data is unexpectedly not + // forthcoming. + 'stalled', + // Media data loading has been suspended. + 'suspend', + // Playback has stopped because of a temporary lack of data + 'waiting', +]; +const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [ + 'play', + 'pause', + 'playing', + 'ended', + 'loadeddata', + 'loadedmetadata', + 'canplay', + 'canplaythrough', + 'volumechange', +]; + +const MEDIA_EVENT_TYPES = [ + ...MEDIA_ERROR_EVENT_TYPES, + ...MEDIA_DEBUG_EVENT_TYPES, +]; + +export enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', Busy = 'busyAudio', } +/* istanbul ignore next */ +const debuglog = (...args: any[]): void => { + if (SettingsStore.getValue("debug_legacy_call_handler")) { + logger.log.call(console, "LegacyCallHandler debuglog:", ...args); + } +}; + interface ThirdpartyLookupResponseFields { /* eslint-disable camelcase */ @@ -119,6 +158,7 @@ export default class LegacyCallHandler extends EventEmitter { // call with a different party to this one. private transferees = new Map(); // callId (target) -> call (transferee) private audioPromises = new Map>(); + private audioElementsWithListeners = new Map(); private supportsPstnProtocol = null; private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native @@ -176,6 +216,16 @@ export default class LegacyCallHandler extends EventEmitter { } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); + + // Add event listeners for the