Merge branch 'develop' into staging

This commit is contained in:
RiotRobot 2023-03-07 11:12:54 +00:00
commit 47b5ff55cc
542 changed files with 10290 additions and 10153 deletions

View file

@ -23,4 +23,4 @@ indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{yml,yaml}] [*.{yml,yaml}]
indent_size = 2 indent_size = 4

View file

@ -165,10 +165,31 @@ module.exports = {
}, },
{ {
files: ["test/**/*.{ts,tsx}", "cypress/**/*.ts"], files: ["test/**/*.{ts,tsx}", "cypress/**/*.ts"],
extends: ["plugin:matrix-org/jest"],
rules: { rules: {
// We don't need super strict typing in test utilities // We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-member-accessibility": "off",
// Jest/Cypress specific
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// TODO: There are many tests with invalid expects that should be fixed,
// https://github.com/vector-im/element-web/issues/24709
"jest/valid-expect": "off",
// TODO: There are many cases to refactor away,
// https://github.com/vector-im/element-web/issues/24710
"jest/no-conditional-expect": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
},
],
}, },
}, },
{ {
@ -176,6 +197,11 @@ module.exports = {
parserOptions: { parserOptions: {
project: ["./cypress/tsconfig.json"], project: ["./cypress/tsconfig.json"],
}, },
rules: {
// Cypress "promises" work differently - disable some related rules
"jest/valid-expect-in-promise": "off",
"jest/no-done-callback": "off",
},
}, },
], ],
settings: { settings: {

View file

@ -5,6 +5,9 @@ on:
workflows: ["Element Web - Build"] workflows: ["Element Web - Build"]
types: types:
- completed - completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
jobs: jobs:
prepare: prepare:
name: Prepare name: Prepare
@ -119,7 +122,7 @@ jobs:
path: webapp path: webapp
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@v5.0.2 uses: cypress-io/github-action@v5.1.0
with: with:
# The built-in Electron runner seems to grind to a halt trying # The built-in Electron runner seems to grind to a halt trying
# to run the tests, so use chrome. # to run the tests, so use chrome.
@ -162,8 +165,9 @@ jobs:
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }} PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }} PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }} PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
# We manually finalize the build in the report stage
PERCY_PARALLEL_TOTAL: -1
- name: Upload Artifact - name: Upload Artifact
if: failure() if: failure()
@ -181,14 +185,35 @@ jobs:
with: with:
name: cypress-junit name: cypress-junit
path: cypress/results path: cypress/results
report: report:
name: Report results name: Report results
needs: tests needs:
- prepare
- tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
permissions: permissions:
statuses: write statuses: write
steps: 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 - uses: Sibz/github-status-action@v1
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
@ -196,6 +221,7 @@ jobs:
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
testrail: testrail:
name: Report results to testrail name: Report results to testrail
needs: needs:

View file

@ -39,6 +39,12 @@ jobs:
env: env:
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- name: Jest Cache
uses: actions/cache@v3
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1
@ -54,7 +60,8 @@ jobs:
yarn ${{ inputs.disable_coverage != 'true' && 'coverage' || 'test' }} \ yarn ${{ inputs.disable_coverage != 'true' && 'coverage' || 'test' }} \
--ci \ --ci \
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \ --reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
--max-workers ${{ steps.cpu-cores.outputs.count }} --max-workers ${{ steps.cpu-cores.outputs.count }} \
--cacheDirectory /tmp/jest_cache
- name: Upload Artifact - name: Upload Artifact
if: inputs.disable_coverage != 'true' if: inputs.disable_coverage != 'true'

View file

@ -2,17 +2,6 @@ Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/
===================================================================================================== =====================================================================================================
## ✨ Features ## ✨ Features
* Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner.
* Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald.
* Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt.
* Poll history: detail screen ([\#10172](https://github.com/matrix-org/matrix-react-sdk/pull/10172)). Contributed by @kerryarchibald.
* Provide a more detailed error message than "No known servers" ([\#6048](https://github.com/matrix-org/matrix-react-sdk/pull/6048)). Fixes vector-im/element-web#13247. Contributed by @aaronraimist.
* Say when a call was answered from a different device ([\#10224](https://github.com/matrix-org/matrix-react-sdk/pull/10224)).
* Widget permissions customizations using module api ([\#10121](https://github.com/matrix-org/matrix-react-sdk/pull/10121)). Contributed by @maheichyk.
* Fix copy button icon overlapping with copyable text ([\#10227](https://github.com/matrix-org/matrix-react-sdk/pull/10227)). Contributed by @Adesh-Pandey.
* Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk.
* The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)).
* Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)).
* Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros. * Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros.
* Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald. * Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald.
* Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald. * Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald.
@ -25,11 +14,7 @@ Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/
* Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes ## 🐛 Bug Fixes
* Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)).
* Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)).
* Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33. * Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33.
* Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009.
* Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul.
* Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt. * Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt.
* Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald. * Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald.
* Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne. * Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne.

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022 The Matrix.org Foundation C.I.C. Copyright 2022-2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -61,4 +61,31 @@ describe("Create Room", () => {
cy.contains(".mx_RoomHeader_nametext", name); cy.contains(".mx_RoomHeader_nametext", name);
cy.contains(".mx_RoomHeader_topic", topic); cy.contains(".mx_RoomHeader_topic", topic);
}); });
it("should create a room with a long room name, which is displayed with ellipsis", () => {
let roomId: string;
const LONG_ROOM_NAME =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
"aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
"officia deserunt mollit anim id est laborum.";
cy.createRoom({ name: LONG_ROOM_NAME }).then((_roomId) => {
roomId = _roomId;
cy.visit("/#/room/" + roomId);
});
// Wait until the room name is set
cy.get(".mx_RoomHeader_nametext").contains("Lorem ipsum");
// Make sure size of buttons on RoomHeader (except .mx_RoomHeader_name) are specified
// and the buttons are not compressed
// TODO: use a same class name
cy.get(".mx_RoomHeader_button").should("have.css", "height", "32px").should("have.css", "width", "32px");
cy.get(".mx_HeaderButtons > .mx_RightPanel_headerButton")
.should("have.css", "height", "32px")
.should("have.css", "width", "32px");
cy.get(".mx_RoomHeader").percySnapshotElement("Room header with a long room name");
});
}); });

View file

@ -118,7 +118,12 @@ describe("Decryption Failure Bar", () => {
"Verify this device to access all messages", "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", "Resend key requests").should("not.exist");
cy.contains(".mx_DecryptionFailureBar_button", "Verify").click(); cy.contains(".mx_DecryptionFailureBar_button", "Verify").click();
@ -146,8 +151,11 @@ describe("Decryption Failure Bar", () => {
"Open another device to load encrypted messages", "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", "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"); cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest");
@ -155,8 +163,11 @@ describe("Decryption Failure Bar", () => {
cy.wait("@keyRequest"); cy.wait("@keyRequest");
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); 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", "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", "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(); cy.contains(".mx_DecryptionFailureBar_button", "Reset").click();
@ -196,7 +209,12 @@ describe("Decryption Failure Bar", () => {
"Some messages could not be decrypted", "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").should("exist");
cy.get(".mx_DecryptionFailureBar .mx_Spinner").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.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist"); cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist");

View file

@ -20,7 +20,7 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { MatrixClient } from "../../global"; import { MatrixClient } from "../../global";
import Chainable = Cypress.Chainable; import Chainable = Cypress.Chainable;
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }"; const hidePercyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
describe("Polls", () => { describe("Polls", () => {
let homeserver: HomeserverInstance; let homeserver: HomeserverInstance;
@ -133,7 +133,7 @@ describe("Polls", () => {
.as("pollId"); .as("pollId");
cy.get<string>("@pollId").then((pollId) => { cy.get<string>("@pollId").then((pollId) => {
getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS }); getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hidePercyCSS });
// Bot votes 'Maybe' in the poll // Bot votes 'Maybe' in the poll
botVoteForOption(bot, roomId, pollId, pollParams.options[2]); botVoteForOption(bot, roomId, pollId, pollParams.options[2]);

View file

@ -356,7 +356,7 @@ describe("Sliding Sync", () => {
}); });
// Regression test for https://github.com/vector-im/element-web/issues/21462 // Regression test for https://github.com/vector-im/element-web/issues/21462
it("should not cancel replies when permalinks are clicked ", () => { it("should not cancel replies when permalinks are clicked", () => {
cy.get<string>("@roomId").then((roomId) => { cy.get<string>("@roomId").then((roomId) => {
// we require a first message as you cannot click the permalink text with the avatar in the way // we require a first message as you cannot click the permalink text with the avatar in the way
return cy return cy

View file

@ -24,7 +24,7 @@ import Timeoutable = Cypress.Timeoutable;
import Withinable = Cypress.Withinable; import Withinable = Cypress.Withinable;
import Shadow = Cypress.Shadow; import Shadow = Cypress.Shadow;
export enum Filter { enum Filter {
People = "people", People = "people",
PublicRooms = "public_rooms", PublicRooms = "public_rooms",
} }
@ -297,27 +297,28 @@ describe("Spotlight", () => {
// TODO: We currently cant test finding rooms on other homeservers/other protocols // TODO: We currently cant test finding rooms on other homeservers/other protocols
// We obviously dont have federation or bridges in cypress tests // We obviously dont have federation or bridges in cypress tests
/* it.skip("should find unknown public rooms on other homeservers", () => {
const room3Name = "Matrix HQ"; cy.openSpotlightDialog()
const room3Id = "#matrix:matrix.org"; .within(() => {
cy.spotlightFilter(Filter.PublicRooms);
it("should find unknown public rooms on other homeservers", () => { cy.spotlightSearch().clear().type(room3Name);
cy.openSpotlightDialog().within(() => { cy.get("[aria-haspopup=true][role=button]").click();
cy.spotlightFilter(Filter.PublicRooms); })
cy.spotlightSearch().clear().type(room3Name); .then(() => {
cy.get("[aria-haspopup=true][role=button]").click(); cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org")
}).then(() => { .next("[role=menuitemradio]")
cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org") .click();
.next("[role=menuitemradio]") cy.wait(3_600_000);
.click(); })
cy.wait(3_600_000); .then(() =>
}).then(() => cy.spotlightDialog().within(() => { cy.spotlightDialog().within(() => {
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name); cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", room3Id); cy.spotlightResults().eq(0).should("contain", room3Id);
})); }),
);
}); });
*/
it("should find known people", () => { it("should find known people", () => {
cy.openSpotlightDialog() cy.openSpotlightDialog()
.within(() => { .within(() => {

View file

@ -18,6 +18,8 @@ limitations under the License.
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { MatrixClient } from "../../global"; import { MatrixClient } from "../../global";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
describe("Threads", () => { describe("Threads", () => {
let homeserver: HomeserverInstance; let homeserver: HomeserverInstance;
@ -54,9 +56,25 @@ describe("Threads", () => {
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
}); });
// Around 200 characters
const MessageLong =
"Hello there. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt " +
"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi";
// --MessageTimestamp-color = #acacac = rgb(172, 172, 172)
// See: _MessageTimestamp.pcss
const MessageTimestampColor = "rgb(172, 172, 172)";
// User sends message // User sends message
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Check the colour of timestamp on the main timeline
cy.get(".mx_RoomView_body .mx_EventTile_last .mx_EventTile_line .mx_MessageTimestamp").should(
"have.css",
"color",
MessageTimestampColor,
);
// Wait for message to send, get its ID and save as @threadId // Wait for message to send, get its ID and save as @threadId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.invoke("attr", "data-scroll-tokens") .invoke("attr", "data-scroll-tokens")
@ -65,7 +83,8 @@ describe("Threads", () => {
// Bot starts thread // Bot starts thread
cy.get<string>("@threadId").then((threadId) => { cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, { bot.sendMessage(roomId, threadId, {
body: "Hello there", // Send a message long enough to be wrapped to check if avatars inside the ReadReceiptGroup are visible
body: MessageLong,
msgtype: "m.text", msgtype: "m.text",
}); });
}); });
@ -75,9 +94,41 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there");
cy.get(".mx_RoomView_body .mx_ThreadSummary").click(); cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
cy.get(".mx_ThreadView .mx_EventTile[data-layout='group'].mx_EventTile_last").within(() => {
// Wait until the messages are rendered
cy.get(".mx_EventTile_line .mx_MTextBody").should("have.text", MessageLong);
// Make sure the avatar inside ReadReceiptGroup is visible on the group layout
cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible");
});
// Enable the bubble layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last").within(() => {
// TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout
// See: https://github.com/vector-im/element-web/issues/23569
cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("exist");
// Make sure the avatar inside ReadReceiptGroup is visible on bubble layout
// TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout
// See: https://github.com/vector-im/element-web/issues/23569
// cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible");
});
// Re-enable the group layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
// User responds in thread // User responds in thread
cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}"); cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}");
// Check the colour of timestamp on EventTile in a thread (mx_ThreadView)
cy.get(".mx_ThreadView .mx_EventTile_last[data-layout='group'] .mx_EventTile_line .mx_MessageTimestamp").should(
"have.css",
"color",
MessageTimestampColor,
);
// User asserts summary was updated correctly // User asserts summary was updated correctly
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
@ -130,6 +181,10 @@ describe("Threads", () => {
cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => { cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => {
cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot"); cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot");
cy.get(".mx_ThreadSummary_content").should("contain", "How are things?"); cy.get(".mx_ThreadSummary_content").should("contain", "How are things?");
// Check the colour of timestamp on thread list
cy.get(".mx_EventTile_details .mx_MessageTimestamp").should("have.css", "color", MessageTimestampColor);
// User opens thread via threads list // User opens thread via threads list
cy.get(".mx_EventTile_line").click(); cy.get(".mx_EventTile_line").click();
}); });

View file

@ -159,8 +159,8 @@ describe("Timeline", () => {
".mx_GenericEventListSummary_summary", ".mx_GenericEventListSummary_summary",
"created and configured the room.", "created and configured the room.",
).should("exist"); ).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", () => { it("should add inline start margin to an event line on IRC layout", () => {
@ -179,20 +179,128 @@ describe("Timeline", () => {
// Check the event line has margin instead of inset property // Check the event line has margin instead of inset property
// cf. _EventTile.pcss // cf. _EventTile.pcss
// --EventTile_irc_line_info-margin-inline-start // --EventTile_irc_line_info-margin-inline-start
// = calc(var(--name-width) + 10px + var(--icon-width)) // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
// = 80 + 10 + 14 = 104px // = 80 + 14 + 5 = 99px
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
.should("have.css", "margin-inline-start", "104px") .should("have.css", "margin-inline-start", "99px")
.should("have.css", "inset-inline-start", "0px"); .should("have.css", "inset-inline-start", "0px");
cy.get(".mx_Spinner").should("not.exist"); // Exclude timestamp and read marker from snapshot
// Exclude timestamp from snapshot const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
const percyCSS = cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }"; percyCSS,
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS }); });
cy.checkA11y(); cy.checkA11y();
}); });
it("should align generic event list summary with messages and emote on IRC layout", () => {
// This test aims to check:
// 1. Alignment of collapsed GELS (generic event list summary) and messages
// 2. Alignment of expanded GELS and messages
// 3. Alignment of expanded GELS and placeholder of deleted message
// 4. Alignment of expanded GELS, placeholder of deleted message, and emote
// Exclude timestamp from snapshot of mx_MainSplit
const percyCSS = ".mx_MainSplit .mx_MessageTimestamp { visibility: hidden !important; }";
cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Send messages
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello again, Mr. Bot{enter}");
// Make sure the second message was sent
cy.get(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
// 1. Alignment of collapsed GELS (generic event list summary) and messages
// Check inline start spacing of collapsed GELS
// See: _EventTile.pcss
// .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line
// = var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 2 * var(--right-padding)
// = 80 + 14 + 46 + 2 * 5
// = 150px
cy.get(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line").should(
"have.css",
"padding-inline-start",
"150px",
);
// Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px
// --right-padding should be applied
cy.get(".mx_EventTile > *").should("have.css", "margin-right", "5px");
// --name-width width zero inline end margin should be applied
cy.get(".mx_EventTile .mx_DisambiguatedProfile")
.should("have.css", "width", "80px")
.should("have.css", "margin-inline-end", "0px");
// --icon-width should be applied
cy.get(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").should("have.css", "width", "14px");
// $MessageTimestamp_width should be applied
cy.get(".mx_EventTile > a").should("have.css", "min-width", "46px");
// Record alignment of collapsed GELS and messages on messagePanel
cy.get(".mx_MainSplit").percySnapshotElement("Collapsed GELS and messages on IRC layout", { percyCSS });
// 2. Alignment of expanded GELS and messages
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
// Check inline start spacing of info line on expanded GELS
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
// See: _EventTile.pcss
// --EventTile_irc_line_info-margin-inline-start
// = 80 + 14 + 1 * 5
.should("have.css", "margin-inline-start", "99px");
// Record alignment of expanded GELS and messages on messagePanel
cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and messages on IRC layout", { percyCSS });
// 3. Alignment of expanded GELS and placeholder of deleted message
// Delete the second (last) message
cy.get(".mx_RoomView_MessageList > .mx_EventTile_last").realHover();
cy.get(".mx_RoomView_MessageList > .mx_EventTile_last .mx_MessageActionBar_optionsButton", {
timeout: 1000,
})
.should("exist")
.realHover()
.click({ force: false });
cy.get(".mx_IconizedContextMenu_item[aria-label=Remove]").should("be.visible").click({ force: false });
// Confirm deletion
cy.get(".mx_Dialog_buttons button[data-testid=dialog-primary-button]")
.should("have.text", "Remove")
.click({ force: false });
// Make sure the dialog was closed and the second (last) message was redacted
cy.get(".mx_Dialog").should("not.exist");
cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody").should("be.visible");
cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
// Record alignment of expanded GELS and placeholder of deleted message on messagePanel
cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and with placeholder of deleted message", {
percyCSS,
});
// 4. Alignment of expanded GELS, placeholder of deleted message, and emote
// Send a emote
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("/me says hello to Mr. Bot{enter}");
// Check inline start margin of its avatar
// Here --right-padding is for the avatar on the message line
// See: _IRCLayout.pcss
// .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar
// = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
// = 80 + 14 + 1 * 5
cy.get(".mx_EventTile_emote .mx_EventTile_avatar").should("have.css", "margin-left", "99px");
// Make sure emote was sent
cy.get(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent").should("be.visible");
// Record alignment of expanded GELS, placeholder of deleted message, and emote
cy.get(".mx_MainSplit").percySnapshotElement(
"Expanded GELS and with emote and placeholder of deleted message",
{
percyCSS,
},
);
});
it("should set inline start padding to a hidden event line", () => { it("should set inline start padding to a hidden event line", () => {
sendEvent(roomId); sendEvent(roomId);
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
@ -212,9 +320,8 @@ describe("Timeline", () => {
// Click timestamp to highlight hidden event line // Click timestamp to highlight hidden event line
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
// Exclude timestamp from snapshot // Exclude timestamp and read marker from snapshot
const percyCSS = const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
// should not add inline start padding to a hidden event line on IRC layout // should not add inline start padding to a hidden event line on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@ -223,17 +330,30 @@ describe("Timeline", () => {
"padding-inline-start", "padding-inline-start",
"0px", "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 // should add inline start padding to a hidden event line on modern layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line") 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 // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
.should("have.css", "padding-inline-start", "84px"); .should("have.css", "padding-inline-start", "84px");
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", {
percyCSS,
});
}); });
it("should click top left of view source event toggle", () => { it("should click view source event toggle", () => {
// This test checks:
// 1. clickability of top left of view source event toggle
// 2. clickability of view source toggle on IRC layout
// Exclude timestamp from snapshot
const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
sendEvent(roomId); sendEvent(roomId);
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
@ -249,8 +369,47 @@ describe("Timeline", () => {
}); });
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist");
// 1. clickability of top left of view source event toggle
// Click top left of the event toggle, which should not be covered by MessageActionBar's safe area // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
cy.get(".mx_EventTile:not(:first-child) .mx_ViewSourceEvent") cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent")
.should("exist")
.realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
});
// Make sure the expand toggle works
cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded")
.should("be.visible")
.realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle")
// Check size and position of toggle on expanded view source event
// See: _ViewSourceEvent.pcss
.should("have.css", "height", "12px") // --ViewSourceEvent_toggle-size
.should("have.css", "align-self", "flex-end")
// Click again to collapse the source
.click("topLeft", { force: false });
});
// Make sure the collapse toggle works
cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded").should("not.exist");
// 2. clickability of view source toggle on IRC layout
// Enable IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Hover the view source toggle on IRC layout
cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
.should("exist")
.realHover()
.percySnapshotElement("Hovered hidden event line on IRC layout", { percyCSS });
// Click view source event toggle
cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
.should("exist") .should("exist")
.realHover() .realHover()
.within(() => { .within(() => {
@ -258,7 +417,7 @@ describe("Timeline", () => {
}); });
// Make sure the expand toggle worked // Make sure the expand toggle worked
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible"); cy.get(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded").should("be.visible");
}); });
it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => { it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => {
@ -329,7 +488,11 @@ describe("Timeline", () => {
cy.wait("@mxc"); cy.wait("@mxc");
cy.checkA11y(); cy.checkA11y();
// Exclude timestamp and read marker from snapshot
const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", { cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
percyCSS,
widths: [800, 400], widths: [800, 400],
}); });
}); });

View file

@ -133,6 +133,7 @@ describe("Stickers", () => {
type: "m.stickerpicker", type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME, name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl, url: stickerPickerUrl,
creatorUserId: "@userId",
}, },
id: STICKER_PICKER_WIDGET_ID, id: STICKER_PICKER_WIDGET_ID,
}, },

View file

@ -22,6 +22,7 @@ declare global {
namespace Cypress { namespace Cypress {
interface SnapshotOptions extends PercySnapshotOptions { interface SnapshotOptions extends PercySnapshotOptions {
domTransformation?: (documentClone: Document) => void; domTransformation?: (documentClone: Document) => void;
allowSpinners?: boolean;
} }
interface Chainable { interface Chainable {
@ -38,6 +39,12 @@ declare global {
} }
Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => { Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => {
if (!options?.allowSpinners) {
// Await spinners to vanish
cy.get(".mx_Spinner", { log: false }).should("not.exist");
// But like really no more spinners please
cy.get(".mx_Spinner", { log: false }).should("not.exist");
}
cy.percySnapshot(name, { cy.percySnapshot(name, {
domTransformation: (documentClone) => scope(documentClone, subject.selector), domTransformation: (documentClone) => scope(documentClone, subject.selector),
...options, ...options,

View file

@ -54,11 +54,15 @@
"test:cypress:open": "cypress open", "test:cypress:open": "cypress open",
"coverage": "yarn test --coverage" "coverage": "yarn test --coverage"
}, },
"resolutions": {
"@types/react-dom": "17.0.19",
"@types/react": "17.0.53"
},
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.4.0", "@matrix-org/analytics-events": "^0.5.0",
"@matrix-org/matrix-wysiwyg": "^1.1.1", "@matrix-org/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/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0", "@sentry/tracing": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
@ -75,7 +79,7 @@
"emojibase-regex": "6.0.1", "emojibase-regex": "6.0.1",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "10.0.5", "filesize": "10.0.6",
"flux": "4.0.3", "flux": "4.0.3",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"gfm.css": "^1.1.2", "gfm.css": "^1.1.2",
@ -100,18 +104,18 @@
"pako": "^2.0.3", "pako": "^2.0.3",
"parse5": "^6.0.1", "parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"posthog-js": "1.36.0", "posthog-js": "1.50.3",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "17.0.2", "react": "17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.2.0", "react-blurhash": "^0.3.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-focus-lock": "^2.5.1", "react-focus-lock": "^2.5.1",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0", "rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sanitize-html": "2.8.0", "sanitize-html": "2.10.0",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
@ -143,12 +147,10 @@
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4", "@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/css-font-loading-module": "^0.0.7", "@types/css-font-loading-module": "^0.0.7",
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
"@types/enzyme": "^3.10.9",
"@types/escape-html": "^1.0.1", "@types/escape-html": "^1.0.1",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
@ -164,9 +166,9 @@
"@types/pako": "^2.0.0", "@types/pako": "^2.0.0",
"@types/parse5": "^6.0.0", "@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5", "@types/qrcode": "^1.3.5",
"@types/react": "17.0.49", "@types/react": "17.0.53",
"@types/react-beautiful-dnd": "^13.0.0", "@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "17.0.17", "@types/react-dom": "17.0.19",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.8.0", "@types/sanitize-html": "2.8.0",
"@types/tar-js": "^0.3.2", "@types/tar-js": "^0.3.2",
@ -174,7 +176,6 @@
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.6.0", "@typescript-eslint/parser": "^5.6.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"allchange": "^1.1.0", "allchange": "^1.1.0",
"axe-core": "4.4.3", "axe-core": "4.4.3",
"babel-jest": "^29.0.0", "babel-jest": "^29.0.0",
@ -184,15 +185,14 @@
"cypress-axe": "^1.0.0", "cypress-axe": "^1.0.0",
"cypress-multi-reporters": "^1.6.1", "cypress-multi-reporters": "^1.6.1",
"cypress-real-events": "^1.7.1", "cypress-real-events": "^1.7.1",
"enzyme": "^3.11.0", "eslint": "8.35.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.10.0", "eslint-plugin-matrix-org": "1.1.0",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0", "eslint-plugin-unicorn": "^45.0.0",
@ -209,20 +209,17 @@
"mocha-junit-reporter": "^2.2.0", "mocha-junit-reporter": "^2.2.0",
"node-fetch": "2", "node-fetch": "2",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"prettier": "2.8.0", "prettier": "2.8.4",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^3.0.2", "rimraf": "^4.0.0",
"stylelint": "^14.9.1", "stylelint": "^15.0.0",
"stylelint-config-prettier": "^9.0.4", "stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0", "stylelint-config-standard": "^30.0.0",
"stylelint-scss": "^4.2.0", "stylelint-scss": "^4.2.0",
"typescript": "4.9.3", "typescript": "4.9.5",
"walk": "^2.3.14" "walk": "^2.3.14"
}, },
"jest": { "jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"testEnvironment": "jsdom", "testEnvironment": "jsdom",
"testMatch": [ "testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)" "<rootDir>/test/**/*-test.[jt]s?(x)"

View file

@ -17,6 +17,7 @@
@import "./components/views/beacon/_ShareLatestLocation.pcss"; @import "./components/views/beacon/_ShareLatestLocation.pcss";
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
@import "./components/views/context_menus/_KebabContextMenu.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss";
@import "./components/views/dialogs/polls/_PollDetailHeader.pcss";
@import "./components/views/dialogs/polls/_PollListItem.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss"; @import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/elements/_FilterDropdown.pcss"; @import "./components/views/elements/_FilterDropdown.pcss";
@ -135,7 +136,6 @@
@import "./views/dialogs/_FeedbackDialog.pcss"; @import "./views/dialogs/_FeedbackDialog.pcss";
@import "./views/dialogs/_ForwardDialog.pcss"; @import "./views/dialogs/_ForwardDialog.pcss";
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss"; @import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
@import "./views/dialogs/_HostSignupDialog.pcss";
@import "./views/dialogs/_IncomingSasDialog.pcss"; @import "./views/dialogs/_IncomingSasDialog.pcss";
@import "./views/dialogs/_InviteDialog.pcss"; @import "./views/dialogs/_InviteDialog.pcss";
@import "./views/dialogs/_JoinRuleDropdown.pcss"; @import "./views/dialogs/_JoinRuleDropdown.pcss";

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export interface IDialogProps { .mx_PollDetailHeader {
onFinished(...args: any): void; // override accessiblebutton style
font-size: $font-15px !important;
}
.mx_PollDetailHeader_icon {
height: 15px;
width: 15px;
margin-right: $spacing-8;
vertical-align: middle;
} }

View file

@ -16,12 +16,17 @@ limitations under the License.
.mx_PollListItem { .mx_PollListItem {
width: 100%; width: 100%;
}
.mx_PollListItem_content {
width: 100%;
display: grid; display: grid;
justify-content: left; justify-content: left;
align-items: center; align-items: center;
grid-gap: $spacing-8; grid-gap: $spacing-8;
grid-template-columns: auto auto auto; grid-template-columns: auto auto auto;
grid-template-rows: auto; grid-template-rows: auto;
cursor: pointer;
color: $primary-content; color: $primary-content;
} }

View file

@ -16,9 +16,14 @@ limitations under the License.
.mx_PollListItemEnded { .mx_PollListItemEnded {
width: 100%; width: 100%;
}
.mx_PollListItemEnded_content {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: $primary-content; color: $primary-content;
cursor: pointer;
} }
.mx_PollListItemEnded_title { .mx_PollListItemEnded_title {

View file

@ -48,6 +48,9 @@ limitations under the License.
align-items: center; align-items: center;
} }
/* See: mx_RoomHeader_button, of which this is a copy.
* TODO: factor out a common component to avoid this duplication.
*/
.mx_RightPanel_headerButton { .mx_RightPanel_headerButton {
cursor: pointer; cursor: pointer;
flex: 0 0 auto; flex: 0 0 auto;

View file

@ -111,12 +111,6 @@ $activeBorderColor: $primary-content;
.mx_SpaceItem_new { .mx_SpaceItem_new {
position: relative; position: relative;
.mx_BetaDot {
position: absolute;
left: 33px;
top: -5px;
}
} }
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {

View file

@ -142,12 +142,8 @@ limitations under the License.
justify-content: center; justify-content: center;
} }
&.mx_UserMenu_contextMenu_guestPrompts,
&.mx_UserMenu_contextMenu_hostingLink {
padding-top: 0;
}
&.mx_UserMenu_contextMenu_guestPrompts { &.mx_UserMenu_contextMenu_guestPrompts {
padding-top: 0;
display: inline-block; display: inline-block;
> span { > span {
@ -190,10 +186,6 @@ limitations under the License.
mask-image: url("$(res)/img/element-icons/roomlist/dnd-cross.svg"); mask-image: url("$(res)/img/element-icons/roomlist/dnd-cross.svg");
} }
.mx_UserMenu_iconHosting::before {
mask-image: url("$(res)/img/element-icons/brands/element.svg");
}
.mx_UserMenu_iconBell::before { .mx_UserMenu_iconBell::before {
mask-image: url("$(res)/img/element-icons/notifications.svg"); mask-image: url("$(res)/img/element-icons/notifications.svg");
} }

View file

@ -29,6 +29,11 @@ limitations under the License.
contain: content; contain: content;
.mx_Waveform,
.mx_RecordingPlayback_timelineLayoutMiddle {
min-width: 0; /* Prevent a blowout */
}
/* Waveforms are present in live recording only */ /* Waveforms are present in live recording only */
.mx_Waveform { .mx_Waveform {
.mx_Waveform_bar { .mx_Waveform_bar {

View file

@ -15,8 +15,8 @@ limitations under the License.
*/ */
.mx_BetaCard { .mx_BetaCard {
margin-bottom: 20px; margin-bottom: $spacing-20;
padding: 24px; padding: $spacing-24;
background-color: $system; background-color: $system;
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
@ -25,7 +25,7 @@ limitations under the License.
.mx_BetaCard_columns { .mx_BetaCard_columns {
display: flex; display: flex;
flex-flow: wrap; flex-flow: wrap;
gap: 20px; gap: $spacing-20;
justify-content: center; justify-content: center;
.mx_BetaCard_columns_description { .mx_BetaCard_columns_description {
@ -36,11 +36,11 @@ limitations under the License.
font-size: $font-18px; font-size: $font-18px;
line-height: $font-22px; line-height: $font-22px;
color: $primary-content; color: $primary-content;
margin: 4px 0 14px; margin: $spacing-4 0 14px; // TODO: use a spacing variable
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 12px; column-gap: $spacing-12;
} }
.mx_BetaCard_caption { .mx_BetaCard_caption {
@ -78,7 +78,7 @@ limitations under the License.
line-height: $font-15px; line-height: $font-15px;
> h4 { > h4 {
margin: 12px 0 0; margin: $spacing-12 0 0;
} }
> p { > p {
@ -102,13 +102,13 @@ limitations under the License.
.mx_BetaCard_relatedSettings { .mx_BetaCard_relatedSettings {
.mx_SettingsFlag { .mx_SettingsFlag {
margin: 16px 0 0; margin: $spacing-16 0 0;
font-size: $font-15px; font-size: $font-15px;
line-height: $font-24px; line-height: $font-24px;
color: $primary-content; color: $primary-content;
.mx_SettingsFlag_microcopy { .mx_SettingsFlag_microcopy {
margin-top: 4px; margin-top: $spacing-4;
font-size: $font-12px; font-size: $font-12px;
line-height: $font-15px; line-height: $font-15px;
} }
@ -122,10 +122,10 @@ limitations under the License.
.mx_BetaCard_betaPill { .mx_BetaCard_betaPill {
background-color: $accent-alt; background-color: $accent-alt;
padding: 4px 10px; padding: $spacing-4 10px; // TODO: use a spacing variable
border-radius: 8px; border-radius: 8px;
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; font-size: $font-12px;
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
line-height: 15px; line-height: 15px;
color: $button-primary-fg-color; color: $button-primary-fg-color;
@ -137,64 +137,3 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
} }
$pulse-color: $accent-alt;
$dot-size: 12px;
.mx_BetaDot {
border-radius: 50%;
margin: 10px;
height: $dot-size;
width: $dot-size;
transform: scale(1);
background: rgba($pulse-color, 1);
animation: mx_Beta_bluePulse 2s infinite;
animation-iteration-count: 20;
position: relative;
pointer-events: none;
&::after {
content: "";
position: absolute;
width: inherit;
height: inherit;
top: 0;
left: 0;
transform: scale(1);
transform-origin: center center;
animation-name: mx_Beta_bluePulse_shadow;
animation-duration: inherit;
animation-iteration-count: inherit;
border-radius: 50%;
background: rgba($pulse-color, 1);
}
}
@keyframes mx_Beta_bluePulse {
0% {
transform: scale(0.95);
}
70% {
transform: scale(1);
}
100% {
transform: scale(0.95);
}
}
@keyframes mx_Beta_bluePulse_shadow {
0% {
opacity: 0.7;
}
70% {
transform: scale(2.2);
opacity: 0;
}
100% {
opacity: 0;
}
}

View file

@ -1,132 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_HostSignupDialog {
width: 90vw;
max-width: 580px;
height: 80vh;
max-height: 600px;
/* Ensure dialog borders are always white as the HostSignupDialog */
/* does not yet support dark mode or theming in general. */
/* In the future we might want to pass the theme to the called */
/* iframe, should some hosting provider have that need. */
background-color: #ffffff;
.mx_HostSignupDialog_info {
text-align: center;
.mx_HostSignupDialog_content_top {
margin-bottom: 24px;
}
.mx_HostSignupDialog_paragraphs {
text-align: left;
padding-left: 25%;
padding-right: 25%;
}
.mx_HostSignupDialog_buttons {
margin-bottom: 24px;
display: flex;
justify-content: center;
button {
padding: 12px;
margin: 0 16px;
}
}
.mx_HostSignupDialog_footer {
display: flex;
justify-content: center;
align-items: baseline;
img {
padding-right: 5px;
}
}
}
iframe {
width: 100%;
height: 100%;
border: none;
background-color: #fff;
min-height: 540px;
}
}
.mx_HostSignupDialog_text_dark {
color: $primary-content;
}
.mx_HostSignupDialog_text_light {
color: $secondary-content;
}
.mx_HostSignup_maximize_button {
mask: url("$(res)/img/element-icons/maximise-expand.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
top: 10px;
right: 10px;
}
.mx_HostSignup_minimize_button {
mask: url("$(res)/img/element-icons/minimise-collapse.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
top: 10px;
right: 25px;
}
.mx_HostSignupDialog_minimized {
position: fixed;
bottom: 80px;
right: 26px;
width: 314px;
height: 217px;
overflow: hidden;
&.mx_Dialog {
padding: 12px;
}
.mx_Dialog_title {
text-align: left !important;
padding-left: 20px;
font-size: $font-15px;
}
iframe {
width: 100%;
height: 100%;
border: none;
background-color: #fff;
}
}

View file

@ -452,3 +452,8 @@ limitations under the License.
.mx_InviteDialog_identityServer { .mx_InviteDialog_identityServer {
margin-top: 1em; /* TODO: Use a spacing variable */ margin-top: 1em; /* TODO: Use a spacing variable */
} }
.mx_InviteDialog_oneThreepid {
font-size: $font-12px;
margin: $spacing-8 0;
}

View file

@ -41,10 +41,20 @@ limitations under the License.
.mx_PollHistoryList_noResults { .mx_PollHistoryList_noResults {
height: 100%; height: 100%;
width: 100%; width: 100%;
box-sizing: border-box;
padding: 0 $spacing-64;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
line-height: $font-24px;
color: $secondary-content; color: $secondary-content;
.mx_PollHistoryList_loadMorePolls {
margin-top: $spacing-16;
}
} }
.mx_PollHistoryList_loading { .mx_PollHistoryList_loading {
@ -57,3 +67,7 @@ limitations under the License.
margin: auto auto; margin: auto auto;
} }
} }
.mx_PollHistoryList_loadMorePolls {
width: max-content;
}

View file

@ -23,11 +23,12 @@ limitations under the License.
max-width: 100%; max-width: 100%;
&.mx_CopyableText_border { &.mx_CopyableText_border {
overflow: auto;
border-radius: 5px; border-radius: 5px;
border: solid 1px $light-fg-color; border: solid 1px $light-fg-color;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 10px; margin-top: 10px;
padding: 10px; padding: 10px 0 10px 10px;
} }
.mx_CopyableText_copyButton { .mx_CopyableText_copyButton {
@ -36,7 +37,8 @@ limitations under the License.
width: 1em; width: 1em;
height: 1em; height: 1em;
cursor: pointer; cursor: pointer;
margin-left: 20px; padding-left: 12px;
padding-right: 10px;
display: block; display: block;
/* If the copy button is used within a scrollable div, make it stick to the right while scrolling */ /* If the copy button is used within a scrollable div, make it stick to the right while scrolling */
position: sticky; position: sticky;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
.mx_ReplyChain { .mx_ReplyChain {
margin: 0 0 $spacing-8 0; margin: 0; // Reset default blockquote margin
padding-left: 10px; // TODO: Use a spacing variable padding-left: 10px; // TODO: Use a spacing variable
border-left: 2px solid var(--username-color); // TODO: Use a spacing variable border-left: 2px solid var(--username-color); // TODO: Use a spacing variable
border-radius: 2px; // TODO: Use a spacing variable border-radius: 2px; // TODO: Use a spacing variable

View file

@ -160,6 +160,7 @@ limitations under the License.
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
color: $secondary-content; color: $secondary-content;
font-size: $font-12px;
gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */ gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */
margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */ margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */
word-break: break-word; word-break: break-word;
@ -168,6 +169,7 @@ limitations under the License.
.mx_LegacyCallEvent_content_button { .mx_LegacyCallEvent_content_button {
@mixin LegacyCallButton; @mixin LegacyCallButton;
padding: 0 $spacing-12; padding: 0 $spacing-12;
font-size: inherit;
span::before { span::before {
mask-size: 16px; mask-size: 16px;

View file

@ -16,8 +16,9 @@ limitations under the License.
.mx_MessageTimestamp { .mx_MessageTimestamp {
--MessageTimestamp-max-width: 80px; --MessageTimestamp-max-width: 80px;
--MessageTimestamp-color: $event-timestamp-color;
color: $event-timestamp-color; color: var(--MessageTimestamp-color);
font-size: $font-10px; font-size: $font-10px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
display: block; /* enable the width setting below */ display: block; /* enable the width setting below */

View file

@ -29,32 +29,34 @@ limitations under the License.
pre { pre {
line-height: 1.2; line-height: 1.2;
margin: 3.5px 0; margin: 3.5px 0; /* TODO: use a variable */
} }
.mx_ViewSourceEvent_toggle { .mx_ViewSourceEvent_toggle {
--ViewSourceEvent_toggle-size: 12px;
visibility: hidden; visibility: hidden;
/* override styles from AccessibleButton */ /* override styles from AccessibleButton */
border-radius: 0; border-radius: 0;
/* icon */ /* icon */
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 0 center; mask-position: 0 center;
mask-size: auto 12px; mask-size: auto var(--ViewSourceEvent_toggle-size);
width: 12px; width: var(--ViewSourceEvent_toggle-size);
min-width: 12px; min-width: var(--ViewSourceEvent_toggle-size);
background-color: $accent; background-color: $accent;
mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
.mx_EventTile:hover & {
visibility: visible;
}
} }
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
align-self: flex-end;
height: var(--ViewSourceEvent_toggle-size);
mask-position: 0 bottom; mask-position: 0 bottom;
margin-bottom: 5px; margin-bottom: 5px; /* TODO: use a variable */
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
} }
} }
.mx_EventTile:hover {
.mx_ViewSourceEvent_toggle {
visibility: visible;
}
}

View file

@ -130,7 +130,7 @@ limitations under the License.
} }
.mx_MessageTimestamp { .mx_MessageTimestamp {
color: $event-timestamp-color; color: var(--MessageTimestamp-color); /* TODO: check whether needed or not */
} }
.mx_BaseCard_footer { .mx_BaseCard_footer {

View file

@ -267,10 +267,18 @@ $left-gutter: 64px;
.mx_EventTileBubble { .mx_EventTileBubble {
margin-inline: auto; margin-inline: auto;
} }
.mx_ReplyChain {
margin-bottom: $spacing-8;
}
} }
&[data-layout="irc"] { &[data-layout="irc"] {
--EventTile_irc_line_info-margin-inline-start: calc(var(--name-width) + 10px + var(--icon-width)); /* add --right-padding value of MessageTimestamp only */
/* stylelint-disable-next-line declaration-colon-space-after */
--EventTile_irc_line_info-margin-inline-start: calc(
var(--name-width) + var(--icon-width) + 1 * var(--right-padding)
);
.mx_EventTile_msgOption { .mx_EventTile_msgOption {
.mx_ReadReceiptGroup { .mx_ReadReceiptGroup {
@ -291,6 +299,10 @@ $left-gutter: 64px;
} }
} }
.mx_ReplyChain {
margin: 0;
}
.mx_ReplyTile .mx_EventTileBubble { .mx_ReplyTile .mx_EventTileBubble {
left: unset; /* Cancel the value specified above for the tile inside ReplyTile */ left: unset; /* Cancel the value specified above for the tile inside ReplyTile */
} }
@ -483,20 +495,12 @@ $left-gutter: 64px;
} }
&[data-layout="irc"] { &[data-layout="irc"] {
.mx_EventTile_line .mx_RedactedBody {
padding-left: 24px; /* 25px - 1px */
&::before {
left: var(--right-padding);
}
}
/* Apply only collapsed events block */ /* Apply only collapsed events block */
> .mx_EventTile_line { > .mx_EventTile_line {
/* 15 px of padding */ /* add --right-padding value of MessageTimestamp and avatar only */
/* stylelint-disable-next-line declaration-colon-space-after */ /* stylelint-disable-next-line declaration-colon-space-after */
padding-left: calc( padding-left: calc(
var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 3 * var(--right-padding) var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 2 * var(--right-padding)
); );
} }
} }
@ -1248,6 +1252,10 @@ $left-gutter: 64px;
padding-block: var(--MatrixChat_useCompactLayout_line-spacing-block); padding-block: var(--MatrixChat_useCompactLayout_line-spacing-block);
} }
.mx_ReplyChain {
margin-bottom: $spacing-4;
}
&.mx_EventTile_info { &.mx_EventTile_info {
padding-top: 0; /* same as the padding for non-compact .mx_EventTile.mx_EventTile_info */ padding-top: 0; /* same as the padding for non-compact .mx_EventTile.mx_EventTile_info */
font-size: $font-13px; font-size: $font-13px;

View file

@ -130,12 +130,17 @@ $irc-line-height: $font-18px;
.mx_TextualEvent, .mx_TextualEvent,
.mx_ViewSourceEvent, .mx_ViewSourceEvent,
.mx_MTextBody { .mx_MTextBody {
display: inline-block;
/* add a 1px padding top and bottom because our larger /* add a 1px padding top and bottom because our larger
emoji font otherwise gets cropped by anti-zalgo */ emoji font otherwise gets cropped by anti-zalgo */
padding: var(--EventTile_irc_line-padding-block) 0; padding: var(--EventTile_irc_line-padding-block) 0;
} }
.mx_EventTile_e2eIcon,
.mx_TextualEvent,
.mx_MTextBody {
display: inline-block;
}
.mx_ReplyTile { .mx_ReplyTile {
.mx_MTextBody { .mx_MTextBody {
display: -webkit-box; /* Enable -webkit-line-clamp */ display: -webkit-box; /* Enable -webkit-line-clamp */
@ -154,7 +159,8 @@ $irc-line-height: $font-18px;
.mx_EventTile_emote { .mx_EventTile_emote {
.mx_EventTile_avatar { .mx_EventTile_avatar {
margin-left: calc(var(--name-width) + var(--icon-width) + var(--right-padding)); /* add --right-padding value of MessageTimestamp only */
margin-left: calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding));
} }
} }
@ -177,7 +183,6 @@ $irc-line-height: $font-18px;
} }
.mx_ReplyChain { .mx_ReplyChain {
margin: 0;
.mx_DisambiguatedProfile { .mx_DisambiguatedProfile {
order: unset; order: unset;
width: unset; width: unset;

View file

@ -191,12 +191,13 @@ limitations under the License.
} }
.mx_RoomHeader_button { .mx_RoomHeader_button {
position: relative; cursor: pointer;
flex: 0 0 auto;
margin-left: 1px; margin-left: 1px;
margin-right: 1px; margin-right: 1px;
cursor: pointer;
height: 32px; height: 32px;
width: 32px; width: 32px;
position: relative;
border-radius: 100%; border-radius: 100%;
&::before { &::before {

View file

@ -88,17 +88,17 @@ limitations under the License.
border-radius: 2px; border-radius: 2px;
} }
code { code:not(pre *) {
font-family: $monospace-font-family !important; font-family: $monospace-font-family !important;
background-color: $inlinecode-background-color; background-color: $inlinecode-background-color;
border: 1px solid $inlinecode-border-color; border: 1px solid $inlinecode-border-color;
border-radius: 4px; border-radius: 4px;
padding: $spacing-2; padding: $spacing-2;
}
code:empty { &:empty {
border: unset; border: unset;
padding: unset; padding: unset;
}
} }
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
.mx_IntegrationManager { .mx_IntegrationManager {
.mx_Dialog { .mx_Dialog {
box-sizing: border-box; box-sizing: border-box;
padding: 0;
width: 60%; width: 60%;
height: 70%; height: 70%;
overflow: hidden; overflow: hidden;

View file

@ -28,9 +28,4 @@ limitations under the License.
margin-bottom: $spacing-16; margin-bottom: $spacing-16;
} }
} }
/* prevent the access token from overflowing the text box */
div .mx_CopyableText {
overflow: scroll;
}
} }

View file

@ -31,19 +31,19 @@ const %%ComponentName%%: React.FC<Props> = () => {
export default %%ComponentName%%; export default %%ComponentName%%;
`, `,
TEST: ` TEST: `
import React from 'react'; import React from "react";
import { mount } from 'enzyme'; import { render } from "@testing-library/react";
import %%ComponentName%% from '%%RelativeComponentPath%%'; import %%ComponentName%% from '%%RelativeComponentPath%%';
describe('<%%ComponentName%% />', () => { describe("<%%ComponentName%% />", () => {
const defaultProps = {}; const defaultProps = {};
const getComponent = (props = {}) => const getComponent = (props = {}) =>
mount(<%%ComponentName%% {...defaultProps} {...props} />); render(<%%ComponentName%% {...defaultProps} {...props} />);
it('renders', () => { it("matches snapshot", () => {
const component = getComponent(); const { asFragment } = getComponent();
expect(component).toBeTruthy(); expect(asFragment()).toMatchSnapshot()();
}); });
}); });
`, `,

View file

@ -48,16 +48,6 @@ export type RecursivePartial<T> = {
: T[P]; : T[P];
}; };
// Inspired by https://stackoverflow.com/a/60206860
export type KeysWithObjectShape<Input> = {
[P in keyof Input]: Input[P] extends object
? // Arrays are counted as objects - exclude them
Input[P] extends Array<unknown>
? never
: P
: never;
}[keyof Input];
export type KeysStartingWith<Input extends object, Str extends string> = { export type KeysStartingWith<Input extends object, Str extends string> = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X

View file

@ -49,7 +49,7 @@ export type Binding = {
*/ */
export default class AddThreepid { export default class AddThreepid {
private sessionId: string; private sessionId: string;
private submitUrl: string; private submitUrl?: string;
private clientSecret: string; private clientSecret: string;
private bind: boolean; private bind: boolean;
@ -93,7 +93,7 @@ export default class AddThreepid {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
return MatrixClientPeg.get() return MatrixClientPeg.get()
.requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken) .requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken)
.then( .then(
@ -155,7 +155,7 @@ export default class AddThreepid {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
return MatrixClientPeg.get() return MatrixClientPeg.get()
.requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken) .requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken)
.then( .then(
@ -184,7 +184,7 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> { public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
try { try {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
if (this.bind) { if (this.bind) {
@ -202,7 +202,7 @@ export default class AddThreepid {
// The spec has always required this to use UI auth but synapse briefly // The spec has always required this to use UI auth but synapse briefly
// implemented it without, so this may just succeed and that's OK. // implemented it without, so this may just succeed and that's OK.
return; return [true];
} catch (e) { } catch (e) {
if (e.httpStatus !== 401 || !e.data || !e.data.flows) { if (e.httpStatus !== 401 || !e.data || !e.data.flows) {
// doesn't look like an interactive-auth failure // doesn't look like an interactive-auth failure
@ -213,8 +213,7 @@ export default class AddThreepid {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), title: _t("Use Single Sign On to continue"),
body: _t( body: _t(
"Confirm adding this email address by using " + "Confirm adding this email address by using Single Sign On to prove your identity.",
"Single Sign On to prove your identity.",
), ),
continueText: _t("Single Sign On"), continueText: _t("Single Sign On"),
continueKind: "primary", continueKind: "primary",
@ -226,19 +225,16 @@ export default class AddThreepid {
continueKind: "primary", continueKind: "primary",
}, },
}; };
const { finished } = Modal.createDialog<[boolean, IAuthData | Error | null]>( const { finished } = Modal.createDialog(InteractiveAuthDialog, {
InteractiveAuthDialog, title: _t("Add Email Address"),
{ matrixClient: MatrixClientPeg.get(),
title: _t("Add Email Address"), authData: e.data,
matrixClient: MatrixClientPeg.get(), makeRequest: this.makeAddThreepidOnlyRequest,
authData: e.data, aestheticsForStagePhases: {
makeRequest: this.makeAddThreepidOnlyRequest, [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
aestheticsForStagePhases: { [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
}, },
); });
return finished; return finished;
} }
} }
@ -260,6 +256,7 @@ export default class AddThreepid {
} }
throw err; throw err;
} }
return [];
} }
/** /**
@ -282,7 +279,7 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> { public async haveMsisdnToken(msisdnToken: string): Promise<any[] | undefined> {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
@ -333,7 +330,7 @@ export default class AddThreepid {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), title: _t("Use Single Sign On to continue"),
body: _t( body: _t(
"Confirm adding this phone number by using " + "Single Sign On to prove your identity.", "Confirm adding this phone number by using Single Sign On to prove your identity.",
), ),
continueText: _t("Single Sign On"), continueText: _t("Single Sign On"),
continueKind: "primary", continueKind: "primary",

View file

@ -18,16 +18,16 @@ import React, { ComponentType } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
import BaseDialog from "./components/views/dialogs/BaseDialog"; import BaseDialog from "./components/views/dialogs/BaseDialog";
import DialogButtons from "./components/views/elements/DialogButtons"; import DialogButtons from "./components/views/elements/DialogButtons";
import Spinner from "./components/views/elements/Spinner"; import Spinner from "./components/views/elements/Spinner";
type AsyncImport<T> = { default: T }; type AsyncImport<T> = { default: T };
interface IProps extends IDialogProps { interface IProps {
// A promise which resolves with the real component // A promise which resolves with the real component
prom: Promise<ComponentType | AsyncImport<ComponentType>>; prom: Promise<ComponentType<any> | AsyncImport<ComponentType<any>>>;
onFinished(): void;
} }
interface IState { interface IState {
@ -71,7 +71,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
} }
private onWrapperCancelClick = (): void => { private onWrapperCancelClick = (): void => {
this.props.onFinished(false); this.props.onFinished();
}; };
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -139,14 +139,18 @@ export function getInitialLetter(name: string): string | undefined {
export function avatarUrlForRoom( export function avatarUrlForRoom(
room: Room | null, room: Room | null,
width: number, width?: number,
height: number, height?: number,
resizeMethod?: ResizeMethod, resizeMethod?: ResizeMethod,
): string | null { ): string | null {
if (!room) return null; // null-guard if (!room) return null; // null-guard
if (room.getMxcAvatarUrl()) { if (room.getMxcAvatarUrl()) {
return mediaFromMxc(room.getMxcAvatarUrl() || undefined).getThumbnailOfSourceHttp(width, height, resizeMethod); const media = mediaFromMxc(room.getMxcAvatarUrl() ?? undefined);
if (width !== undefined && height !== undefined) {
return media.getThumbnailOfSourceHttp(width, height, resizeMethod);
}
return media.srcHttp;
} }
// space rooms cannot be DMs so skip the rest // space rooms cannot be DMs so skip the rest
@ -160,7 +164,11 @@ export function avatarUrlForRoom(
// If there are only two members in the DM use the avatar of the other member // If there are only two members in the DM use the avatar of the other member
const otherMember = room.getAvatarFallbackMember(); const otherMember = room.getAvatarFallbackMember();
if (otherMember?.getMxcAvatarUrl()) { if (otherMember?.getMxcAvatarUrl()) {
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); const media = mediaFromMxc(otherMember.getMxcAvatarUrl());
if (width !== undefined && height !== undefined) {
return media.getThumbnailOfSourceHttp(width, height, resizeMethod);
}
return media.srcHttp;
} }
return null; return null;
} }

View file

@ -389,7 +389,7 @@ export default class ContentMessages {
} }
if (tooBigFiles.length > 0) { if (tooBigFiles.length > 0) {
const { finished } = Modal.createDialog<[boolean]>(UploadFailureDialog, { const { finished } = Modal.createDialog(UploadFailureDialog, {
badFiles: tooBigFiles, badFiles: tooBigFiles,
totalFiles: files.length, totalFiles: files.length,
contentMessages: this, contentMessages: this,
@ -407,7 +407,7 @@ export default class ContentMessages {
const loopPromiseBefore = promBefore; const loopPromiseBefore = promBefore;
if (!uploadAll) { if (!uploadAll) {
const { finished } = Modal.createDialog<[boolean, boolean]>(UploadConfirmDialog, { const { finished } = Modal.createDialog(UploadConfirmDialog, {
file, file,
currentIndex: i, currentIndex: i,
totalFiles: okFiles.length, totalFiles: okFiles.length,
@ -546,7 +546,7 @@ export default class ContentMessages {
if (upload.cancelled) throw new UploadCanceledError(); if (upload.cancelled) throw new UploadCanceledError();
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; 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")) { if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
sendRoundTripMetric(matrixClient, roomId, response.event_id); sendRoundTripMetric(matrixClient, roomId, response.event_id);

View file

@ -148,19 +148,6 @@ export interface IConfigOptions {
analytics_owner?: string; // defaults to `brand` analytics_owner?: string; // defaults to `brand`
privacy_policy_url?: string; // location for cookie policy privacy_policy_url?: string; // location for cookie policy
// Server hosting upsell options
hosting_signup_link?: string; // slightly different from `host_signup`
host_signup?: {
brand?: string; // acts as the enabled flag too (truthy == show)
// Required-ness denotes when `brand` is truthy
cookie_policy_url: string;
privacy_policy_url: string;
terms_of_service_url: string;
url: string;
domains?: string[];
};
enable_presence_by_hs_url?: Record<string, boolean>; // <HomeserverName, Enabled> enable_presence_by_hs_url?: Record<string, boolean>; // <HomeserverName, Enabled>
terms_and_conditions_links?: { url: string; text: string }[]; terms_and_conditions_links?: { url: string; text: string }[];

View file

@ -58,7 +58,7 @@ import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import Resend from "./Resend"; import Resend from "./Resend";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes"; import { InviteKind } from "./components/views/dialogs/InviteDialogTypes";
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload"; import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
import { findDMForUser } from "./utils/dm/findDMForUser"; import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
@ -737,7 +737,7 @@ export default class LegacyCallHandler extends EventEmitter {
); );
if (!stats) { if (!stats) {
logger.debug( logger.debug(
"Call statistics are undefined. The call has " + "probably failed before a peerConn was established", "Call statistics are undefined. The call has probably failed before a peerConn was established",
); );
return; return;
} }
@ -1024,13 +1024,12 @@ export default class LegacyCallHandler extends EventEmitter {
} }
public answerCall(roomId: string): void { public answerCall(roomId: string): void {
const call = this.calls.get(roomId);
this.stopRingingIfPossible(call.callId);
// no call to answer // no call to answer
if (!this.calls.has(roomId)) return; if (!this.calls.has(roomId)) return;
const call = this.calls.get(roomId)!;
this.stopRingingIfPossible(call.callId);
if (this.getAllActiveCalls().length > 1) { if (this.getAllActiveCalls().length > 1) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: _t("Too Many Calls"), title: _t("Too Many Calls"),
@ -1215,7 +1214,7 @@ export default class LegacyCallHandler extends EventEmitter {
call.setRemoteOnHold(true); call.setRemoteOnHold(true);
dis.dispatch<OpenInviteDialogPayload>({ dis.dispatch<OpenInviteDialogPayload>({
action: Action.OpenInviteDialog, action: Action.OpenInviteDialog,
kind: KIND_CALL_TRANSFER, kind: InviteKind.CallTransfer,
call, call,
analyticsName: "Transfer Call", analyticsName: "Transfer Call",
className: "mx_InviteDialog_transferWrapper", className: "mx_InviteDialog_transferWrapper",

View file

@ -265,7 +265,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
.then(() => { .then(() => {
const lazyLoadEnabled = e.value; const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) { if (lazyLoadEnabled) {
return new Promise((resolve) => { return new Promise<void>((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, { Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve, onFinished: resolve,
}); });
@ -275,7 +275,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
// between LL/non-LL version on same host. // between LL/non-LL version on same host.
// as disabling LL when previously enabled // as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app) // is a strong indicator of this (/develop & /app)
return new Promise((resolve) => { return new Promise<void>((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, { Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve, onFinished: resolve,
host: window.location.host, host: window.location.host,
@ -287,7 +287,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
return MatrixClientPeg.get().store.deleteAllData(); return MatrixClientPeg.get().store.deleteAllData();
}) })
.then(() => { .then(() => {
PlatformPeg.get().reload(); PlatformPeg.get()?.reload();
}); });
} }
} }
@ -519,7 +519,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
stopMatrixClient(); stopMatrixClient();
const pickleKey = const pickleKey =
credentials.userId && credentials.deviceId credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) ? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
: null; : null;
if (pickleKey) { if (pickleKey) {

View file

@ -40,6 +40,7 @@ import { SlidingSyncManager } from "./SlidingSyncManager";
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog"; import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
import { SettingLevel } from "./settings/SettingLevel"; import { SettingLevel } from "./settings/SettingLevel";
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string; homeserverUrl: string;
@ -166,7 +167,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
} }
try { try {
const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time"), 10); const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time")!, 10);
const diff = Date.now() - registrationTime; const diff = Date.now() - registrationTime;
return diff / 36e5 <= hours; return diff / 36e5 <= hours;
} catch (e) { } catch (e) {
@ -176,7 +177,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
public userRegisteredAfter(timestamp: Date): boolean { public userRegisteredAfter(timestamp: Date): boolean {
try { try {
const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time"), 10); const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time")!, 10);
return timestamp.getTime() <= registrationTime; return timestamp.getTime() <= registrationTime;
} catch (e) { } catch (e) {
return false; return false;
@ -237,6 +238,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
// Connect the matrix client to the dispatcher and setting handlers // Connect the matrix client to the dispatcher and setting handlers
MatrixActionCreators.start(this.matrixClient); MatrixActionCreators.start(this.matrixClient);
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
MatrixClientBackedController.matrixClient = this.matrixClient;
return opts; return opts;
} }
@ -292,7 +294,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
} }
public getCredentials(): IMatrixClientCreds { public getCredentials(): IMatrixClientCreds {
let copiedCredentials = this.currentClientCreds; let copiedCredentials: IMatrixClientCreds | null = this.currentClientCreds;
if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) { if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) {
// cached credentials belong to a different user - don't use them // cached credentials belong to a different user - don't use them
copiedCredentials = null; copiedCredentials = null;

View file

@ -64,7 +64,7 @@ export default class MediaDeviceHandler extends EventEmitter {
* *
* @return Promise<IMediaDevices> The available media devices * @return Promise<IMediaDevices> The available media devices
*/ */
public static async getDevices(): Promise<IMediaDevices> { public static async getDevices(): Promise<IMediaDevices | undefined> {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
const output: Record<MediaDeviceKindEnum, MediaDeviceInfo[]> = { const output: Record<MediaDeviceKindEnum, MediaDeviceInfo[]> = {

View file

@ -27,34 +27,35 @@ import AsyncWrapper from "./AsyncWrapper";
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
export interface IModal<T extends any[]> { // Type which accepts a React Component which looks like a Modal (accepts an onFinished prop)
export type ComponentType = React.ComponentType<{
onFinished?(...args: any): void;
}>;
// Generic type which returns the props of the Modal component with the onFinished being optional.
export type ComponentProps<C extends ComponentType> = Omit<React.ComponentProps<C>, "onFinished"> &
Partial<Pick<React.ComponentProps<C>, "onFinished">>;
export interface IModal<C extends ComponentType> {
elem: React.ReactNode; elem: React.ReactNode;
className?: string; className?: string;
beforeClosePromise?: Promise<boolean>; beforeClosePromise?: Promise<boolean>;
closeReason?: string; closeReason?: string;
onBeforeClose?(reason?: string): Promise<boolean>; onBeforeClose?(reason?: string): Promise<boolean>;
onFinished?(...args: T): void; onFinished: ComponentProps<C>["onFinished"];
close(...args: T): void; close(...args: Parameters<ComponentProps<C>["onFinished"]>): void;
hidden?: boolean; hidden?: boolean;
} }
export interface IHandle<T extends any[]> { export interface IHandle<C extends ComponentType> {
finished: Promise<T>; finished: Promise<Parameters<ComponentProps<C>["onFinished"]>>;
close(...args: T): void; close(...args: Parameters<ComponentProps<C>["onFinished"]>): void;
} }
interface IProps<T extends any[]> { interface IOptions<C extends ComponentType> {
onFinished?(...args: T): void; onBeforeClose?: IModal<C>["onBeforeClose"];
// TODO improve typing here once all Modals are TS and we can exhaustively check the props
[key: string]: any;
} }
interface IOptions<T extends any[]> {
onBeforeClose?: IModal<T>["onBeforeClose"];
}
type ParametersWithoutFirst<T extends (...args: any) => any> = T extends (a: any, ...args: infer P) => any ? P : never;
export enum ModalManagerEvent { export enum ModalManagerEvent {
Opened = "opened", Opened = "opened",
} }
@ -111,18 +112,30 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
return !!this.priorityModal || !!this.staticModal || this.modals.length > 0; return !!this.priorityModal || !!this.staticModal || this.modals.length > 0;
} }
public createDialog<T extends any[]>( public createDialog<C extends ComponentType>(
Element: React.ComponentType<any>, Element: C,
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]> props?: ComponentProps<C>,
): IHandle<T> { className?: string,
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest); isPriorityModal = false,
isStaticModal = false,
options: IOptions<C> = {},
): IHandle<C> {
return this.createDialogAsync<C>(
Promise.resolve(Element),
props,
className,
isPriorityModal,
isStaticModal,
options,
);
} }
public appendDialog<T extends any[]>( public appendDialog<C extends ComponentType>(
Element: React.ComponentType, Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["appendDialogAsync"]> props?: ComponentProps<C>,
): IHandle<T> { className?: string,
return this.appendDialogAsync<T>(Promise.resolve(Element), ...rest); ): IHandle<C> {
return this.appendDialogAsync<C>(Promise.resolve(Element), props, className);
} }
public closeCurrentModal(reason: string): void { public closeCurrentModal(reason: string): void {
@ -134,15 +147,15 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
modal.close(); modal.close();
} }
private buildModal<T extends any[]>( private buildModal<C extends ComponentType>(
prom: Promise<React.ComponentType>, prom: Promise<React.ComponentType>,
props?: IProps<T>, props?: ComponentProps<C>,
className?: string, className?: string,
options?: IOptions<T>, options?: IOptions<C>,
): { ): {
modal: IModal<T>; modal: IModal<C>;
closeDialog: IHandle<T>["close"]; closeDialog: IHandle<C>["close"];
onFinishedProm: IHandle<T>["finished"]; onFinishedProm: IHandle<C>["finished"];
} { } {
const modal = { const modal = {
onFinished: props?.onFinished, onFinished: props?.onFinished,
@ -151,10 +164,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// these will be set below but we need an object reference to pass to getCloseFn before we can do that // these will be set below but we need an object reference to pass to getCloseFn before we can do that
elem: null, elem: null,
} as IModal<T>; } as IModal<C>;
// never call this from onFinished() otherwise it will loop // never call this from onFinished() otherwise it will loop
const [closeDialog, onFinishedProm] = this.getCloseFn<T>(modal, props); const [closeDialog, onFinishedProm] = this.getCloseFn<C>(modal, props);
// don't attempt to reuse the same AsyncWrapper for different dialogs, // don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused. // otherwise we'll get confused.
@ -168,13 +181,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
return { modal, closeDialog, onFinishedProm }; return { modal, closeDialog, onFinishedProm };
} }
private getCloseFn<T extends any[]>( private getCloseFn<C extends ComponentType>(
modal: IModal<T>, modal: IModal<C>,
props?: IProps<T>, props?: ComponentProps<C>,
): [IHandle<T>["close"], IHandle<T>["finished"]] { ): [IHandle<C>["close"], IHandle<C>["finished"]] {
const deferred = defer<T>(); const deferred = defer<Parameters<ComponentProps<C>["onFinished"]>>();
return [ return [
async (...args: T): Promise<void> => { async (...args: Parameters<ComponentProps<C>["onFinished"]>): Promise<void> => {
if (modal.beforeClosePromise) { if (modal.beforeClosePromise) {
await modal.beforeClosePromise; await modal.beforeClosePromise;
} else if (modal.onBeforeClose) { } else if (modal.onBeforeClose) {
@ -249,16 +262,16 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog
* @returns {object} Object with 'close' parameter being a function that will close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog
*/ */
public createDialogAsync<T extends any[]>( public createDialogAsync<C extends ComponentType>(
prom: Promise<React.ComponentType>, prom: Promise<C>,
props?: IProps<T>, props?: ComponentProps<C>,
className?: string, className?: string,
isPriorityModal = false, isPriorityModal = false,
isStaticModal = false, isStaticModal = false,
options: IOptions<T> = {}, options: IOptions<C> = {},
): IHandle<T> { ): IHandle<C> {
const beforeModal = this.getCurrentModal(); const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<T>(prom, props, className, options); const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, options);
if (isPriorityModal) { if (isPriorityModal) {
// XXX: This is destructive // XXX: This is destructive
this.priorityModal = modal; this.priorityModal = modal;
@ -278,13 +291,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
}; };
} }
private appendDialogAsync<T extends any[]>( private appendDialogAsync<C extends ComponentType>(
prom: Promise<React.ComponentType>, prom: Promise<React.ComponentType>,
props?: IProps<T>, props?: ComponentProps<C>,
className?: string, className?: string,
): IHandle<T> { ): IHandle<C> {
const beforeModal = this.getCurrentModal(); const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<T>(prom, props, className, {}); const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, {});
this.modals.push(modal); this.modals.push(modal);

View file

@ -54,8 +54,8 @@ export default class PosthogTrackers {
} }
private view: Views = Views.LOADING; private view: Views = Views.LOADING;
private pageType?: PageType = null; private pageType?: PageType;
private override?: ScreenName = null; private override?: ScreenName;
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void { public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
this.view = view; this.view = view;
@ -66,7 +66,7 @@ export default class PosthogTrackers {
private trackPage(durationMs?: number): void { private trackPage(durationMs?: number): void {
const screenName = 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<ScreenEvent>({ PosthogAnalytics.instance.trackEvent<ScreenEvent>({
eventName: "$pageview", eventName: "$pageview",
$current_url: screenName, $current_url: screenName,
@ -85,7 +85,7 @@ export default class PosthogTrackers {
public clearOverride(screenName: ScreenName): void { public clearOverride(screenName: ScreenName): void {
if (screenName !== this.override) return; if (screenName !== this.override) return;
this.override = null; this.override = undefined;
this.trackPage(); this.trackPage();
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ComponentProps } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
@ -29,7 +29,7 @@ import InviteDialog from "./components/views/dialogs/InviteDialog";
import BaseAvatar from "./components/views/avatars/BaseAvatar"; import BaseAvatar from "./components/views/avatars/BaseAvatar";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { KIND_DM, KIND_INVITE } from "./components/views/dialogs/InviteDialogTypes"; import { InviteKind } from "./components/views/dialogs/InviteDialogTypes";
import { Member } from "./utils/direct-messages"; import { Member } from "./utils/direct-messages";
export interface IInviteResult { export interface IInviteResult {
@ -64,7 +64,7 @@ export function showStartChatInviteDialog(initialText = ""): void {
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createDialog( Modal.createDialog(
InviteDialog, InviteDialog,
{ kind: KIND_DM, initialText }, { kind: InviteKind.Dm, initialText },
/*className=*/ "mx_InviteDialog_flexWrapper", /*className=*/ "mx_InviteDialog_flexWrapper",
/*isPriority=*/ false, /*isPriority=*/ false,
/*isStatic=*/ true, /*isStatic=*/ true,
@ -76,10 +76,10 @@ export function showRoomInviteDialog(roomId: string, initialText = ""): void {
Modal.createDialog( Modal.createDialog(
InviteDialog, InviteDialog,
{ {
kind: KIND_INVITE, kind: InviteKind.Invite,
initialText, initialText,
roomId, roomId,
}, } as Omit<ComponentProps<typeof InviteDialog>, "onFinished">,
/*className=*/ "mx_InviteDialog_flexWrapper", /*className=*/ "mx_InviteDialog_flexWrapper",
/*isPriority=*/ false, /*isPriority=*/ false,
/*isStatic=*/ true, /*isStatic=*/ true,

View file

@ -19,14 +19,12 @@ import { Optional } from "matrix-events-sdk";
import { SnakedObject } from "./utils/SnakedObject"; import { SnakedObject } from "./utils/SnakedObject";
import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions"; import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions";
import { KeysWithObjectShape } from "./@types/common";
// see element-web config.md for docs, or the IConfigOptions interface for dev docs // see element-web config.md for docs, or the IConfigOptions interface for dev docs
export const DEFAULTS: IConfigOptions = { export const DEFAULTS: IConfigOptions = {
brand: "Element", brand: "Element",
integrations_ui_url: "https://scalar.vector.im/", integrations_ui_url: "https://scalar.vector.im/",
integrations_rest_url: "https://scalar.vector.im/api", integrations_rest_url: "https://scalar.vector.im/api",
bug_report_endpoint_url: null,
uisi_autorageshake_app: "element-auto-uisi", uisi_autorageshake_app: "element-auto-uisi",
jitsi: { jitsi: {
@ -79,10 +77,10 @@ export default class SdkConfig {
return SdkConfig.fallback.get(key, altCaseName); return SdkConfig.fallback.get(key, altCaseName);
} }
public static getObject<K extends KeysWithObjectShape<IConfigOptions>>( public static getObject<K extends keyof IConfigOptions>(
key: K, key: K,
altCaseName?: string, altCaseName?: string,
): Optional<SnakedObject<IConfigOptions[K]>> { ): Optional<SnakedObject<NonNullable<IConfigOptions[K]>>> {
const val = SdkConfig.get(key, altCaseName); const val = SdkConfig.get(key, altCaseName);
if (val !== null && val !== undefined) { if (val !== null && val !== undefined) {
return new SnakedObject(val); return new SnakedObject(val);

View file

@ -22,13 +22,13 @@ import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib"; import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib";
import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ComponentType } from "react";
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "./Modal"; import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
import { isSecureBackupRequired } from "./utils/WellKnownUtils"; import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import AccessSecretStorageDialog from "./components/views/dialogs/security/AccessSecretStorageDialog"; import AccessSecretStorageDialog, { KeyParams } from "./components/views/dialogs/security/AccessSecretStorageDialog";
import RestoreKeyBackupDialog from "./components/views/dialogs/security/RestoreKeyBackupDialog"; import RestoreKeyBackupDialog from "./components/views/dialogs/security/RestoreKeyBackupDialog";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
@ -83,8 +83,6 @@ async function confirmToDismiss(): Promise<boolean> {
return !sure; return !sure;
} }
type KeyParams = { passphrase: string; recoveryKey: string };
function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise<Uint8Array> { function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise<Uint8Array> {
return async ({ passphrase, recoveryKey }): Promise<Uint8Array> => { return async ({ passphrase, recoveryKey }): Promise<Uint8Array> => {
if (passphrase) { if (passphrase) {
@ -333,7 +331,7 @@ export async function accessSecretStorage(func = async (): Promise<void> => {},
// passphrase creation. // passphrase creation.
const { finished } = Modal.createDialogAsync( const { finished } = Modal.createDialogAsync(
import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise< import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise<
ComponentType<{}> typeof CreateSecretStorageDialog
>, >,
{ {
forceReset, forceReset,

View file

@ -198,7 +198,7 @@ function reject(error?: any): RunResult {
return { error }; return { error };
} }
function success(promise?: Promise<any>): RunResult { function success(promise: Promise<any> = Promise.resolve()): RunResult {
return { promise }; return { promise };
} }
@ -221,7 +221,7 @@ export const Commands = [
command: "spoiler", command: "spoiler",
args: "<message>", args: "<message>",
description: _td("Sends the given message as a spoiler"), description: _td("Sends the given message as a spoiler"),
runFn: function (roomId, message) { runFn: function (roomId, message = "") {
return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`)); return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -282,7 +282,7 @@ export const Commands = [
command: "plain", command: "plain",
args: "<message>", args: "<message>",
description: _td("Sends a message as plain text, without interpreting it as markdown"), 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)); return successSync(ContentHelpers.makeTextMessage(messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -291,7 +291,7 @@ export const Commands = [
command: "html", command: "html",
args: "<message>", args: "<message>",
description: _td("Sends a message as html, without interpreting it as markdown"), 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)); return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -551,7 +551,7 @@ export const Commands = [
) { ) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) { if (defaultIdentityServerUrl) {
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Use an identity server"), title: _t("Use an identity server"),
description: ( description: (
<p> <p>

View file

@ -191,7 +191,7 @@ export async function dialogTermsInteractionCallback(
): Promise<string[]> { ): Promise<string[]> {
logger.log("Terms that need agreement", policiesAndServicePairs); logger.log("Terms that need agreement", policiesAndServicePairs);
const { finished } = Modal.createDialog<[boolean, string[]]>( const { finished } = Modal.createDialog(
TermsDialog, TermsDialog,
{ {
policiesAndServicePairs, policiesAndServicePairs,

View file

@ -154,7 +154,7 @@ export enum KeyBindingAction {
ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility", ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility",
} }
type KeyboardShortcutSetting = IBaseSetting<KeyCombo>; type KeyboardShortcutSetting = Omit<IBaseSetting<KeyCombo>, "supportedLevels">;
// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager
export type IKeyboardShortcuts = Partial<Record<KeyBindingAction, KeyboardShortcutSetting>>; export type IKeyboardShortcuts = Partial<Record<KeyBindingAction, KeyboardShortcutSetting>>;

View file

@ -27,7 +27,7 @@ import { Action } from "../../../../dispatcher/actions";
import { SettingLevel } from "../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../settings/SettingLevel";
interface IProps { interface IProps {
onFinished: (success: boolean) => void; onFinished: (success?: boolean) => void;
} }
interface IState { interface IState {

View file

@ -27,17 +27,18 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from "../../../../components/views/elements/Field"; import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import { IIndexStats } from "../../../../indexing/BaseEventIndexManager"; import { IIndexStats } from "../../../../indexing/BaseEventIndexManager";
interface IProps extends IDialogProps {} interface IProps {
onFinished(): void;
}
interface IState { interface IState {
eventIndexSize: number; eventIndexSize: number;
eventCount: number; eventCount: number;
crawlingRoomsCount: number; crawlingRoomsCount: number;
roomCount: number; roomCount: number;
currentRoom: string; currentRoom: string | null;
crawlerSleepTime: number; crawlerSleepTime: number;
} }
@ -60,7 +61,8 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
public updateCurrentRoom = async (room: Room): Promise<void> => { public updateCurrentRoom = async (room: Room): Promise<void> => {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
let stats: IIndexStats; if (!eventIndex) return;
let stats: IIndexStats | undefined;
try { try {
stats = await eventIndex.getStats(); stats = await eventIndex.getStats();
@ -70,7 +72,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
return; return;
} }
let currentRoom = null; let currentRoom: string | null = null;
if (room) currentRoom = room.name; if (room) currentRoom = room.name;
const roomStats = eventIndex.crawlingRooms(); const roomStats = eventIndex.crawlingRooms();
@ -78,8 +80,8 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
const roomCount = roomStats.totalRooms.size; const roomCount = roomStats.totalRooms.size;
this.setState({ this.setState({
eventIndexSize: stats.size, eventIndexSize: stats?.size ?? 0,
eventCount: stats.eventCount, eventCount: stats?.eventCount ?? 0,
crawlingRoomsCount: crawlingRoomsCount, crawlingRoomsCount: crawlingRoomsCount,
roomCount: roomCount, roomCount: roomCount,
currentRoom: currentRoom, currentRoom: currentRoom,
@ -99,7 +101,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
let crawlingRoomsCount = 0; let crawlingRoomsCount = 0;
let roomCount = 0; let roomCount = 0;
let eventCount = 0; let eventCount = 0;
let currentRoom = null; let currentRoom: string | null = null;
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
@ -108,8 +110,10 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
try { try {
const stats = await eventIndex.getStats(); const stats = await eventIndex.getStats();
eventIndexSize = stats.size; if (stats) {
eventCount = stats.eventCount; eventIndexSize = stats.size;
eventCount = stats.eventCount;
}
} catch { } catch {
// This call may fail if sporadically, not a huge issue as we // This call may fail if sporadically, not a huge issue as we
// will try later again in the updateCurrentRoom call and // will try later again in the updateCurrentRoom call and
@ -135,7 +139,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
private onDisable = async (): Promise<void> => { private onDisable = async (): Promise<void> => {
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default; const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
Modal.createDialog(DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true); Modal.createDialog(DisableEventIndexDialog, undefined, undefined, /* priority = */ false, /* static = */ true);
}; };
private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => { private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => {
@ -157,11 +161,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
const eventIndexingSettings = ( const eventIndexingSettings = (
<div> <div>
{_t( {_t("%(brand)s is securely caching encrypted messages locally for them to appear in search results:", {
"%(brand)s is securely caching encrypted messages locally for them " + brand,
"to appear in search results:", })}
{ brand },
)}
<div className="mx_SettingsTab_subsectionText"> <div className="mx_SettingsTab_subsectionText">
{crawlerState} {crawlerState}
<br /> <br />

View file

@ -26,7 +26,6 @@ import { accessSecretStorage } from "../../../../SecurityManager";
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import { copyNode } from "../../../../utils/strings"; import { copyNode } from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField"; import PassphraseField from "../../../../components/views/auth/PassphraseField";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field"; import Field from "../../../../components/views/elements/Field";
import Spinner from "../../../../components/views/elements/Spinner"; import Spinner from "../../../../components/views/elements/Spinner";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
@ -45,10 +44,12 @@ enum Phase {
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
interface IProps extends IDialogProps {} interface IProps {
onFinished(done?: boolean): void;
}
interface IState { interface IState {
secureSecretStorage: boolean; secureSecretStorage: boolean | null;
phase: Phase; phase: Phase;
passPhrase: string; passPhrase: string;
passPhraseValid: boolean; passPhraseValid: boolean;
@ -120,7 +121,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
const { secureSecretStorage } = this.state; const { secureSecretStorage } = this.state;
this.setState({ this.setState({
phase: Phase.BackingUp, phase: Phase.BackingUp,
error: null, error: undefined,
}); });
let info; let info;
try { try {
@ -218,7 +219,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
private onPassPhraseValidate = (result: IValidationResult): void => { private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({ this.setState({
passPhraseValid: result.valid, passPhraseValid: !!result.valid,
}); });
}; };
@ -305,7 +306,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
changeText = _t("Go back to set it again."); changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null; let passPhraseMatch: JSX.Element | undefined;
if (matchText) { if (matchText) {
passPhraseMatch = ( passPhraseMatch = (
<div className="mx_CreateKeyBackupDialog_passPhraseMatch"> <div className="mx_CreateKeyBackupDialog_passPhraseMatch">

View file

@ -43,7 +43,6 @@ import {
SecureBackupSetupMethod, SecureBackupSetupMethod,
} from "../../../../utils/WellKnownUtils"; } from "../../../../utils/WellKnownUtils";
import SecurityCustomisations from "../../../../customisations/Security"; import SecurityCustomisations from "../../../../customisations/Security";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field"; import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner"; import Spinner from "../../../../components/views/elements/Spinner";
@ -67,10 +66,11 @@ enum Phase {
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
interface IProps extends IDialogProps { interface IProps {
hasCancel: boolean; hasCancel?: boolean;
accountPassword: string; accountPassword?: string;
forceReset: boolean; forceReset?: boolean;
onFinished(ok?: boolean): void;
} }
interface IState { interface IState {
@ -81,13 +81,13 @@ interface IState {
copied: boolean; copied: boolean;
downloaded: boolean; downloaded: boolean;
setPassphrase: boolean; setPassphrase: boolean;
backupInfo: IKeyBackupInfo; backupInfo: IKeyBackupInfo | null;
backupSigStatus: TrustInfo; backupSigStatus: TrustInfo | null;
// does the server offer a UI auth flow with just m.login.password // does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload? // for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: boolean; canUploadKeysWithPasswordOnly: boolean | null;
accountPassword: string; accountPassword: string;
accountPasswordCorrect: boolean; accountPasswordCorrect: boolean | null;
canSkip: boolean; canSkip: boolean;
passPhraseKeySelected: string; passPhraseKeySelected: string;
error?: string; error?: string;
@ -119,7 +119,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
} }
const accountPassword = props.accountPassword || ""; const accountPassword = props.accountPassword || "";
let canUploadKeysWithPasswordOnly = null; let canUploadKeysWithPasswordOnly: boolean | null = null;
if (accountPassword) { if (accountPassword) {
// If we have an account password in memory, let's simplify and // If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device // assume it means password auth is also supported for device
@ -172,12 +172,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
this.fetchBackupInfo(); this.fetchBackupInfo();
} }
private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo; backupSigStatus: TrustInfo }> { private async fetchBackupInfo(): Promise<{ backupInfo?: IKeyBackupInfo; backupSigStatus?: TrustInfo }> {
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = const backupSigStatus =
// we may not have started crypto yet, in which case we definitely don't trust the backup // we may not have started crypto yet, in which case we definitely don't trust the backup
MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)); backupInfo && MatrixClientPeg.get().isCryptoEnabled()
? await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
: null;
const { forceReset } = this.props; const { forceReset } = this.props;
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase; const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
@ -189,17 +191,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}); });
return { return {
backupInfo, backupInfo: backupInfo ?? undefined,
backupSigStatus, backupSigStatus: backupSigStatus ?? undefined,
}; };
} catch (e) { } catch (e) {
this.setState({ phase: Phase.LoadError }); this.setState({ phase: Phase.LoadError });
return {};
} }
} }
private async queryKeyUploadAuth(): Promise<void> { private async queryKeyUploadAuth(): Promise<void> {
try { try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys); await MatrixClientPeg.get().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
@ -248,7 +251,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private onMigrateFormSubmit = (e: React.FormEvent): void => { private onMigrateFormSubmit = (e: React.FormEvent): void => {
e.preventDefault(); e.preventDefault();
if (this.state.backupSigStatus.usable) { if (this.state.backupSigStatus?.usable) {
this.bootstrapSecretStorage(); this.bootstrapSecretStorage();
} else { } else {
this.restoreBackup(); this.restoreBackup();
@ -265,7 +268,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}; };
private onDownloadClick = (): void => { private onDownloadClick = (): void => {
const blob = new Blob([this.recoveryKey.encodedPrivateKey], { const blob = new Blob([this.recoveryKey.encodedPrivateKey!], {
type: "text/plain;charset=us-ascii", type: "text/plain;charset=us-ascii",
}); });
FileSaver.saveAs(blob, "security-key.txt"); FileSaver.saveAs(blob, "security-key.txt");
@ -323,7 +326,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private bootstrapSecretStorage = async (): Promise<void> => { private bootstrapSecretStorage = async (): Promise<void> => {
this.setState({ this.setState({
phase: Phase.Storing, phase: Phase.Storing,
error: null, error: undefined,
}); });
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -351,7 +354,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}); });
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this.recoveryKey, createSecretStorageKey: async () => this.recoveryKey,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo!,
setupNewKeyBackup: !this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo,
getKeyBackupPassphrase: async (): Promise<Uint8Array> => { getKeyBackupPassphrase: async (): Promise<Uint8Array> => {
// We may already have the backup key if we earlier went // We may already have the backup key if we earlier went
@ -399,14 +402,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
showSummary: false, showSummary: false,
keyCallback, keyCallback,
}, },
null, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ false, /* static = */ false,
); );
await finished; await finished;
const { backupSigStatus } = await this.fetchBackupInfo(); const { backupSigStatus } = await this.fetchBackupInfo();
if (backupSigStatus.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { if (backupSigStatus?.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
this.bootstrapSecretStorage(); this.bootstrapSecretStorage();
} }
}; };
@ -467,7 +470,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private onPassPhraseValidate = (result: IValidationResult): void => { private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({ this.setState({
passPhraseValid: result.valid, passPhraseValid: !!result.valid,
}); });
}; };
@ -581,13 +584,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
label={_t("Password")} label={_t("Password")}
value={this.state.accountPassword} value={this.state.accountPassword}
onChange={this.onAccountPasswordChange} onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : null} forceValidity={this.state.accountPasswordCorrect === false ? false : undefined}
autoFocus={true} autoFocus={true}
/> />
</div> </div>
</div> </div>
); );
} else if (!this.state.backupSigStatus.usable) { } else if (!this.state.backupSigStatus?.usable) {
authPrompt = ( authPrompt = (
<div> <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div> <div>{_t("Restore your key backup to upgrade your encryption")}</div>
@ -612,7 +615,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
primaryButton={nextCaption} primaryButton={nextCaption}
onPrimaryButtonClick={this.onMigrateFormSubmit} onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false} hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} primaryDisabled={!!this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
> >
<button type="button" className="danger" onClick={this.onCancelClick}> <button type="button" className="danger" onClick={this.onCancelClick}>
{_t("Skip")} {_t("Skip")}
@ -680,7 +683,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
changeText = _t("Go back to set it again."); changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null; let passPhraseMatch: JSX.Element | undefined;
if (matchText) { if (matchText) {
passPhraseMatch = ( passPhraseMatch = (
<div> <div>
@ -721,7 +724,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
} }
private renderPhaseShowKey(): JSX.Element { private renderPhaseShowKey(): JSX.Element {
let continueButton; let continueButton: JSX.Element;
if (this.state.phase === Phase.ShowKey) { if (this.state.phase === Phase.ShowKey) {
continueButton = ( continueButton = (
<DialogButtons <DialogButtons
@ -928,7 +931,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
} }
} }
let titleClass = null; let titleClass: string | string[] | undefined;
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Passphrase: case Phase.Passphrase:
case Phase.PassphraseConfirm: case Phase.PassphraseConfirm:

View file

@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption"; import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field"; import Field from "../../../../components/views/elements/Field";
import { KeysStartingWith } from "../../../../@types/common"; import { KeysStartingWith } from "../../../../@types/common";
@ -32,13 +31,14 @@ enum Phase {
Exporting = "exporting", Exporting = "exporting",
} }
interface IProps extends IDialogProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onFinished(doExport?: boolean): void;
} }
interface IState { interface IState {
phase: Phase; phase: Phase;
errStr: string; errStr: string | null;
passphrase1: string; passphrase1: string;
passphrase2: string; passphrase2: string;
} }

View file

@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption"; import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field"; import Field from "../../../../components/views/elements/Field";
@ -29,7 +28,11 @@ function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
resolve(e.target.result as ArrayBuffer); if (e.target?.result) {
resolve(e.target.result as ArrayBuffer);
} else {
reject(new Error("Failed to read file due to unknown error"));
}
}; };
reader.onerror = reject; reader.onerror = reject;
@ -42,14 +45,15 @@ enum Phase {
Importing = "importing", Importing = "importing",
} }
interface IProps extends IDialogProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onFinished(imported?: boolean): void;
} }
interface IState { interface IState {
enableSubmit: boolean; enableSubmit: boolean;
phase: Phase; phase: Phase;
errStr: string; errStr: string | null;
passphrase: string; passphrase: string;
} }
@ -73,7 +77,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
} }
private onFormChange = (): void => { private onFormChange = (): void => {
const files = this.file.current.files; const files = this.file.current?.files;
this.setState({ this.setState({
enableSubmit: this.state.passphrase !== "" && !!files?.length, enableSubmit: this.state.passphrase !== "" && !!files?.length,
}); });
@ -87,7 +91,10 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
private onFormSubmit = (ev: React.FormEvent): boolean => { private onFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
this.startImport(this.file.current.files[0], this.state.passphrase); const file = this.file.current?.files?.[0];
if (file) {
this.startImport(file, this.state.passphrase);
}
return false; return false;
}; };

View file

@ -24,12 +24,12 @@ import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
interface IProps extends IDialogProps { interface IProps {
newVersionInfo: IKeyBackupInfo; newVersionInfo: IKeyBackupInfo;
onFinished(): void;
} }
export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> { export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {

View file

@ -21,11 +21,12 @@ import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
interface IProps extends IDialogProps {} interface IProps {
onFinished(): void;
}
export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> { export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> {
private onGoToSettingsClick = (): void => { private onGoToSettingsClick = (): void => {
@ -38,7 +39,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
Modal.createDialogAsync( Modal.createDialogAsync(
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>, import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>,
undefined, undefined,
null, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ true, /* static = */ true,
); );

View file

@ -27,6 +27,7 @@ import { timeout } from "../utils/promise";
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SpaceProvider from "./SpaceProvider"; import SpaceProvider from "./SpaceProvider";
import { TimelineRenderingType } from "../contexts/RoomContext"; import { TimelineRenderingType } from "../contexts/RoomContext";
import { filterBoolean } from "../utils/arrays";
export interface ISelectionRange { export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not 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 { export interface IProviderCompletions {
completions: ICompletion[]; completions: ICompletion[];
provider: AutocompleteProvider; provider: AutocompleteProvider;
command: ICommand; command: Partial<ICommand>;
} }
export default class Autocompleter { 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 // map then filter to maintain the index for the map-operation, for this.providers to line up
return completionsList return filterBoolean(
.map((completions, i) => { completionsList.map((completions, i) => {
if (!completions || !completions.length) return; if (!completions || !completions.length) return;
return { return {
@ -112,7 +113,7 @@ export default class Autocompleter {
*/ */
command: this.providers[i].getCurrentCommand(query, selection, force), command: this.providers[i].getCurrentCommand(query, selection, force),
}; };
}) }),
.filter(Boolean) as IProviderCompletions[]; );
} }
} }

View file

@ -19,7 +19,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { uniq, sortBy, ListIteratee } from "lodash"; import { uniq, sortBy, uniqBy, ListIteratee } from "lodash";
import EMOTICON_REGEX from "emojibase-regex/emoticon"; import EMOTICON_REGEX from "emojibase-regex/emoticon";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -32,6 +32,7 @@ import SettingsStore from "../settings/SettingsStore";
import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji"; import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji";
import { TimelineRenderingType } from "../contexts/RoomContext"; import { TimelineRenderingType } from "../contexts/RoomContext";
import * as recent from "../emojipicker/recent"; import * as recent from "../emojipicker/recent";
import { filterBoolean } from "../utils/arrays";
const LIMIT = 20; const LIMIT = 20;
@ -94,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider {
shouldMatchWordsOnly: true, 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( public async getCompletions(
@ -117,7 +118,7 @@ export default class EmojiProvider extends AutocompleteProvider {
// Do second match with shouldMatchWordsOnly in order to match against 'name' // Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString)); completions = completions.concat(this.nameMatcher.match(matchedString));
let sorters: ListIteratee<ISortedEmoji>[] = []; const sorters: ListIteratee<ISortedEmoji>[] = [];
// make sure that emoticons come first // make sure that emoticons come first
sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
@ -139,11 +140,27 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = completions.slice(0, LIMIT); completions = completions.slice(0, LIMIT);
// Do a second sort to place emoji matching with frequently used one on top // Do a second sort to place emoji matching with frequently used one on top
sorters = []; const recentlyUsedAutocomplete: ISortedEmoji[] = [];
this.recentlyUsed.forEach((emoji) => { this.recentlyUsed.forEach((emoji) => {
sorters.push((c) => score(emoji.shortcodes[0], c.emoji.shortcodes[0])); if (emoji.shortcodes[0].indexOf(trimmedMatch) === 0) {
recentlyUsedAutocomplete.push({ emoji: emoji, _orderBy: 0 });
}
}); });
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
//if there is an exact shortcode match in the frequently used emojis, it goes before everything
for (let i = 0; i < recentlyUsedAutocomplete.length; i++) {
if (recentlyUsedAutocomplete[i].emoji.shortcodes[0] === trimmedMatch) {
const exactMatchEmoji = recentlyUsedAutocomplete[i];
for (let j = i; j > 0; j--) {
recentlyUsedAutocomplete[j] = recentlyUsedAutocomplete[j - 1];
}
recentlyUsedAutocomplete[0] = exactMatchEmoji;
break;
}
}
completions = recentlyUsedAutocomplete.concat(completions);
completions = uniqBy(completions, "emoji");
return completions.map((c) => ({ return completions.map((c) => ({
completion: c.emoji.unicode, completion: c.emoji.unicode,

View file

@ -46,7 +46,7 @@ interface IState {
export default class EmbeddedPage extends React.PureComponent<IProps, IState> { export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
private unmounted = false; private unmounted = false;
private dispatcherRef: string = null; private dispatcherRef: string | null = null;
public constructor(props: IProps, context: typeof MatrixClientContext) { public constructor(props: IProps, context: typeof MatrixClientContext) {
super(props, context); super(props, context);
@ -64,7 +64,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
let res: Response; let res: Response;
try { try {
res = await fetch(this.props.url, { method: "GET" }); res = await fetch(this.props.url!, { method: "GET" });
} catch (err) { } catch (err) {
if (this.unmounted) return; if (this.unmounted) return;
logger.warn(`Error loading page: ${err}`); logger.warn(`Error loading page: ${err}`);
@ -84,7 +84,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
if (this.props.replaceMap) { if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach((key) => { 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<IProps, IState> {
const client = this.context || MatrixClientPeg.get(); const client = this.context || MatrixClientPeg.get();
const isGuest = client ? client.isGuest() : true; const isGuest = client ? client.isGuest() : true;
const className = this.props.className; const className = this.props.className;
const classes = classnames({ const classes = classnames(className, {
[className]: true,
[`${className}_guest`]: isGuest, [`${className}_guest`]: isGuest,
[`${className}_loggedIn`]: !!client, [`${className}_loggedIn`]: !!client,
}); });

View file

@ -40,6 +40,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
const onDragEnter = (ev: DragEvent): void => { const onDragEnter = (ev: DragEvent): void => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
if (!ev.dataTransfer) return;
setState((state) => ({ setState((state) => ({
// We always increment the counter no matter the types, because dragging is // We always increment the counter no matter the types, because dragging is
@ -49,7 +50,8 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
// https://docs.w3cub.com/dom/datatransfer/types // 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 // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
dragging: 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 ? true
: state.dragging, : state.dragging,
})); }));
@ -68,6 +70,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
const onDragOver = (ev: DragEvent): void => { const onDragOver = (ev: DragEvent): void => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
if (!ev.dataTransfer) return;
ev.dataTransfer.dropEffect = "none"; ev.dataTransfer.dropEffect = "none";
@ -82,6 +85,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
const onDrop = (ev: DragEvent): void => { const onDrop = (ev: DragEvent): void => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
if (!ev.dataTransfer) return;
onFileDrop(ev.dataTransfer); onFileDrop(ev.dataTransfer);
setState((state) => ({ setState((state) => ({

View file

@ -66,7 +66,7 @@ class FilePanel extends React.Component<IProps, IState> {
private onRoomTimeline = ( private onRoomTimeline = (
ev: MatrixEvent, ev: MatrixEvent,
room: Room | null, room: Room | undefined,
toStartOfTimeline: boolean, toStartOfTimeline: boolean,
removed: boolean, removed: boolean,
data: IRoomTimelineData, data: IRoomTimelineData,
@ -78,7 +78,7 @@ class FilePanel extends React.Component<IProps, IState> {
client.decryptEventIfNeeded(ev); client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted()) { if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId()); this.decryptingEvents.add(ev.getId()!);
} else { } else {
this.addEncryptedLiveEvent(ev); this.addEncryptedLiveEvent(ev);
} }
@ -86,7 +86,7 @@ class FilePanel extends React.Component<IProps, IState> {
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => { private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
if (ev.getRoomId() !== this.props.roomId) return; if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId(); const eventId = ev.getId()!;
if (!this.decryptingEvents.delete(eventId)) return; if (!this.decryptingEvents.delete(eventId)) return;
if (err) return; if (err) return;
@ -103,7 +103,7 @@ class FilePanel extends React.Component<IProps, IState> {
return; return;
} }
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) { if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false); this.state.timelineSet.addEventToTimeline(ev, timeline, false);
} }
} }

View file

@ -56,15 +56,15 @@ const getOwnProfile = (
userId: string, userId: string,
): { ): {
displayName: string; displayName: string;
avatarUrl: string; avatarUrl?: string;
} => ({ } => ({
displayName: OwnProfileStore.instance.displayName || userId, displayName: OwnProfileStore.instance.displayName || userId,
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE) ?? undefined,
}); });
const UserWelcomeTop: React.FC = () => { const UserWelcomeTop: React.FC = () => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const userId = cli.getUserId(); const userId = cli.getUserId()!;
const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId)); const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId));
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => { useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
setOwnProfile(getOwnProfile(userId)); setOwnProfile(getOwnProfile(userId));

View file

@ -1,54 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../views/context_menus/IconizedContextMenu";
import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
interface IProps {
onClick?(): void;
}
interface IState {}
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async (): Promise<void> => {
this.props.onClick?.();
await HostSignupStore.instance.setHostSignupActive(true);
};
public render(): React.ReactNode {
const hostSignupConfig = SdkConfig.getObject("host_signup");
if (!hostSignupConfig?.get("brand")) {
return null;
}
return (
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconHosting"
label={_t("Upgrade to %(hostSignupBrand)s", {
hostSignupBrand: hostSignupConfig.get("brand"),
})}
onClick={this.openDialog}
/>
</IconizedContextMenuOptionList>
);
}
}

View file

@ -33,7 +33,7 @@ export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
type InteractiveAuthCallbackSuccess = ( type InteractiveAuthCallbackSuccess = (
success: true, success: true,
response: IAuthData, response?: IAuthData,
extra?: { emailSid?: string; clientSecret?: string }, extra?: { emailSid?: string; clientSecret?: string },
) => void; ) => void;
type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void; type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void;
@ -80,7 +80,7 @@ interface IProps {
// Called when the stage changes, or the stage's phase changes. First // Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have // argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric). // phases and will be counted as 0 (numeric).
onStagePhaseChange?(stage: AuthType, phase: number): void; onStagePhaseChange?(stage: AuthType | null, phase: number): void;
} }
interface IState { interface IState {
@ -94,7 +94,7 @@ interface IState {
export default class InteractiveAuthComponent extends React.Component<IProps, IState> { export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
private readonly authLogic: InteractiveAuth; private readonly authLogic: InteractiveAuth;
private readonly intervalId: number = null; private readonly intervalId: number | null = null;
private readonly stageComponent = createRef<IStageComponent>(); private readonly stageComponent = createRef<IStageComponent>();
private unmounted = false; private unmounted = false;
@ -103,10 +103,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
super(props); super(props);
this.state = { this.state = {
authStage: null,
busy: false, busy: false,
errorText: null,
errorCode: null,
submitButtonEnabled: false, submitButtonEnabled: false,
}; };
@ -173,7 +170,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
busy: true, busy: true,
}); });
try { try {
return await this.props.requestEmailToken(email, secret, attempt, session); // We know this method only gets called on flows where requestEmailToken is passed but types don't
return await this.props.requestEmailToken!(email, secret, attempt, session);
} finally { } finally {
this.setState({ this.setState({
busy: false, busy: false,
@ -213,8 +211,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
if (busy) { if (busy) {
this.setState({ this.setState({
busy: true, busy: true,
errorText: null, errorText: undefined,
errorCode: null, errorCode: undefined,
}); });
} }
// The JS SDK eagerly reports itself as "not busy" right after any // The JS SDK eagerly reports itself as "not busy" right after any
@ -234,7 +232,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
}; };
private onPhaseChange = (newPhase: number): void => { private onPhaseChange = (newPhase: number): void => {
this.props.onStagePhaseChange?.(this.state.authStage, newPhase || 0); this.props.onStagePhaseChange?.(this.state.authStage ?? null, newPhase || 0);
}; };
private onStageCancel = (): void => { private onStageCancel = (): void => {

View file

@ -166,10 +166,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
>(); >();
let lastTopHeader; let lastTopHeader: HTMLDivElement | undefined;
let firstBottomHeader; let firstBottomHeader: HTMLDivElement | undefined;
for (const sublist of sublists) { for (const sublist of sublists) {
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable"); const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable");
if (!header) continue; // this should never occur
header.style.removeProperty("display"); // always clear display:none first header.style.removeProperty("display"); // always clear display:none first
// When an element is <=40% off screen, make it take over // When an element is <=40% off screen, make it take over
@ -196,7 +197,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// cause a no-op update, as adding/removing properties that are/aren't there cause // cause a no-op update, as adding/removing properties that are/aren't there cause
// layout updates. // layout updates.
for (const header of targetStyles.keys()) { for (const header of targetStyles.keys()) {
const style = targetStyles.get(header); const style = targetStyles.get(header)!;
if (style.makeInvisible) { if (style.makeInvisible) {
// we will have already removed the 'display: none', so add it back. // we will have already removed the 'display: none', so add it back.
@ -324,7 +325,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
private renderSearchDialExplore(): React.ReactNode { 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 // If we have dialer support, show a button to bring up the dial pad
// to start a new call // to start a new call
@ -338,7 +339,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
); );
} }
let rightButton: JSX.Element; let rightButton: JSX.Element | undefined;
if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) { if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
rightButton = <RecentlyViewedButton />; rightButton = <RecentlyViewedButton />;
} else if (this.state.activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms)) { } else if (this.state.activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms)) {

View file

@ -35,7 +35,7 @@ const CONNECTING_STATES = [
CallState.CreateAnswer, CallState.CreateAnswer,
]; ];
const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing]; const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing, CallState.Ended];
export enum CustomCallState { export enum CustomCallState {
Missed = "missed", Missed = "missed",
@ -72,7 +72,7 @@ export function buildLegacyCallEventGroupers(
export default class LegacyCallEventGrouper extends EventEmitter { export default class LegacyCallEventGrouper extends EventEmitter {
private events: Set<MatrixEvent> = new Set<MatrixEvent>(); private events: Set<MatrixEvent> = new Set<MatrixEvent>();
private call: MatrixCall; private call: MatrixCall | null = null;
public state: CallState | CustomCallState; public state: CallState | CustomCallState;
public constructor() { public constructor() {
@ -111,7 +111,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
} }
public get hangupReason(): string | null { public get hangupReason(): string | null {
return this.hangup?.getContent()?.reason; return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null;
} }
public get rejectParty(): string { public get rejectParty(): string {

View file

@ -47,7 +47,6 @@ import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from "../views/host_signup/HostSignupContainer";
import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel"; import SpacePanel from "../views/spaces/SpacePanel";
@ -695,7 +694,6 @@ class LoggedInView extends React.Component<IProps, IState> {
</div> </div>
<PipContainer /> <PipContainer />
<NonUrgentToastContainer /> <NonUrgentToastContainer />
<HostSignupContainer />
{audioFeedArraysForCalls} {audioFeedArraysForCalls}
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ComponentType, createRef } from "react"; import React, { createRef } from "react";
import { import {
ClientEvent, ClientEvent,
createClient, createClient,
@ -38,6 +38,8 @@ import "focus-visible";
// what-input helps improve keyboard accessibility // what-input helps improve keyboard accessibility
import "what-input"; import "what-input";
import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog";
import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import PosthogTrackers from "../../PosthogTrackers"; import PosthogTrackers from "../../PosthogTrackers";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
@ -140,6 +142,7 @@ import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/Spotli
import { findDMForUser } from "../../utils/dm/findDMForUser"; import { findDMForUser } from "../../utils/dm/findDMForUser";
import { Linkify } from "../../HtmlUtils"; import { Linkify } from "../../HtmlUtils";
import { NotificationColor } from "../../stores/notifications/NotificationColor"; import { NotificationColor } from "../../stores/notifications/NotificationColor";
import { UserTab } from "../views/dialogs/UserTab";
// legacy export // legacy export
export { default as Views } from "../../Views"; export { default as Views } from "../../Views";
@ -226,8 +229,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private screenAfterLogin?: IScreen; private screenAfterLogin?: IScreen;
private tokenLogin?: boolean; private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: number;
private focusComposer: boolean; private focusComposer: boolean;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
@ -296,9 +297,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.loadSession(); Lifecycle.loadSession();
} }
this.accountPassword = null;
this.accountPasswordTimer = null;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher(); this.themeWatcher = new ThemeWatcher();
@ -439,7 +437,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); this.stores.accountPasswordStore.clearPassword();
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy(); if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
} }
@ -710,7 +708,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const tabPayload = payload as OpenToTabPayload; const tabPayload = payload as OpenToTabPayload;
Modal.createDialog( Modal.createDialog(
UserSettingsDialog, UserSettingsDialog,
{ initialTabId: tabPayload.initialTabId }, { initialTabId: tabPayload.initialTabId as UserTab },
/*className=*/ null, /*className=*/ null,
/*isPriority=*/ false, /*isPriority=*/ false,
/*isStatic=*/ true, /*isStatic=*/ true,
@ -1634,14 +1632,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialogAsync( Modal.createDialogAsync(
import( import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog" "../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<ComponentType<{}>>, ) as unknown as Promise<typeof NewRecoveryMethodDialog>,
{ newVersionInfo }, { newVersionInfo },
); );
} else { } else {
Modal.createDialogAsync( Modal.createDialogAsync(
import( import(
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog" "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
) as unknown as Promise<ComponentType<{}>>, ) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
); );
} }
}); });
@ -1987,13 +1985,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* this, as they instead jump straight into the app after `attemptTokenLogin`. * this, as they instead jump straight into the app after `attemptTokenLogin`.
*/ */
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => { private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => {
this.accountPassword = password; this.stores.accountPasswordStore.setPassword(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);
// Create and start the client // Create and start the client
await Lifecycle.setLoggedIn(credentials); await Lifecycle.setLoggedIn(credentials);
@ -2023,7 +2015,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const fragmentAfterLogin = this.getFragmentAfterLogin(); const fragmentAfterLogin = this.getFragmentAfterLogin();
let view = null; let view: JSX.Element;
if (this.state.view === Views.LOADING) { if (this.state.view === Views.LOADING) {
view = ( view = (
@ -2037,7 +2029,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view = ( view = (
<E2eSetup <E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished} onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.accountPassword} accountPassword={this.stores.accountPasswordStore.getPassword()}
tokenLogin={!!this.tokenLogin} tokenLogin={!!this.tokenLogin}
/> />
); );
@ -2140,6 +2132,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view = <UseCaseSelection onFinished={(useCase): Promise<void> => this.onShowPostLoginScreen(useCase)} />; view = <UseCaseSelection onFinished={(useCase): Promise<void> => this.onShowPostLoginScreen(useCase)} />;
} else { } else {
logger.error(`Unknown view ${this.state.view}`); logger.error(`Unknown view ${this.state.view}`);
return null;
} }
return ( return (

View file

@ -262,7 +262,14 @@ export default class RightPanel extends React.Component<IProps, IState> {
break; break;
case RightPanelPhases.RoomSummary: case RightPanelPhases.RoomSummary:
card = <RoomSummaryCard room={this.props.room} onClose={this.onClose} />; card = (
<RoomSummaryCard
room={this.props.room}
onClose={this.onClose}
// whenever RightPanel is passed a room it is passed a permalinkcreator
permalinkCreator={this.props.permalinkCreator!}
/>
);
break; break;
case RightPanelPhases.Widget: case RightPanelPhases.Widget:

View file

@ -114,8 +114,13 @@ import { RoomSearchView } from "./RoomSearchView";
import eventSearch from "../../Searching"; import eventSearch from "../../Searching";
import VoipUserMapper from "../../VoipUserMapper"; import VoipUserMapper from "../../VoipUserMapper";
import { isCallEvent } from "./LegacyCallEventGrouper"; import { isCallEvent } from "./LegacyCallEventGrouper";
import { WidgetType } from "../../widgets/WidgetType";
import WidgetUtils from "../../utils/WidgetUtils";
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
const DEBUG = false; const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
let debuglog = function (msg: string): void {}; let debuglog = function (msg: string): void {};
const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe"); const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe");
@ -228,6 +233,7 @@ export interface IRoomState {
} }
interface LocalRoomViewProps { interface LocalRoomViewProps {
localRoom: LocalRoom;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
@ -243,7 +249,7 @@ interface LocalRoomViewProps {
function LocalRoomView(props: LocalRoomViewProps): ReactElement { function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext); const context = useContext(RoomContext);
const room = context.room as LocalRoom; const room = context.room as LocalRoom;
const encryptionEvent = context.room.currentState.getStateEvents(EventType.RoomEncryption)[0]; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode; let encryptionTile: ReactNode;
if (encryptionEvent) { if (encryptionEvent) {
@ -258,8 +264,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
}); });
}; };
let statusBar: ReactElement; let statusBar: ReactElement | null = null;
let composer: ReactElement; let composer: ReactElement | null = null;
if (room.isError) { if (room.isError) {
const buttons = ( const buttons = (
@ -278,7 +284,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
} else { } else {
composer = ( composer = (
<MessageComposer <MessageComposer
room={context.room} room={props.localRoom}
resizeNotifier={props.resizeNotifier} resizeNotifier={props.resizeNotifier}
permalinkCreator={props.permalinkCreator} permalinkCreator={props.permalinkCreator}
/> />
@ -290,7 +296,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
<ErrorBoundary> <ErrorBoundary>
<RoomHeader <RoomHeader
room={context.room} room={context.room}
searchInfo={null} searchInfo={undefined}
inRoom={true} inRoom={true}
onSearchClick={null} onSearchClick={null}
onInviteClick={null} onInviteClick={null}
@ -339,7 +345,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
<ErrorBoundary> <ErrorBoundary>
<RoomHeader <RoomHeader
room={context.room} room={context.room}
searchInfo={null} searchInfo={undefined}
inRoom={true} inRoom={true}
onSearchClick={null} onSearchClick={null}
onInviteClick={null} onInviteClick={null}
@ -370,7 +376,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomView = createRef<HTMLElement>(); private roomView = createRef<HTMLElement>();
private searchResultsPanel = createRef<ScrollPanel>(); private searchResultsPanel = createRef<ScrollPanel>();
private messagePanel: TimelinePanel; private messagePanel?: TimelinePanel;
private roomViewBody = createRef<HTMLDivElement>(); private roomViewBody = createRef<HTMLDivElement>();
public static contextType = SDKContext; public static contextType = SDKContext;
@ -379,15 +385,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);
if (!context.client) {
throw new Error("Unable to create RoomView without MatrixClient");
}
const llMembers = context.client.hasLazyLoadMembersEnabled(); const llMembers = context.client.hasLazyLoadMembersEnabled();
this.state = { this.state = {
roomId: null, roomId: undefined,
roomLoading: true, roomLoading: true,
peekLoading: false, peekLoading: false,
shouldPeek: true, shouldPeek: true,
membersLoaded: !llMembers, membersLoaded: !llMembers,
numUnreadMessages: 0, numUnreadMessages: 0,
callState: null, callState: undefined,
activeCall: null, activeCall: null,
canPeek: false, canPeek: false,
canSelfRedact: false, canSelfRedact: false,
@ -483,6 +493,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onWidgetStoreUpdate = (): void => { private onWidgetStoreUpdate = (): void => {
if (!this.state.room) return; if (!this.state.room) return;
this.checkWidgets(this.state.room); this.checkWidgets(this.state.room);
this.doMaybeRemoveOwnJitsiWidget();
}; };
private onWidgetEchoStoreUpdate = (): void => { private onWidgetEchoStoreUpdate = (): void => {
@ -503,6 +514,56 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.checkWidgets(this.state.room); 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 => { private checkWidgets = (room: Room): void => {
this.setState({ this.setState({
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room), hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
@ -1866,10 +1927,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
private renderLocalRoomView(): ReactElement { private renderLocalRoomView(localRoom: LocalRoom): ReactElement {
return ( return (
<RoomContext.Provider value={this.state}> <RoomContext.Provider value={this.state}>
<LocalRoomView <LocalRoomView
localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator} permalinkCreator={this.permalinkCreator}
roomView={this.roomView} roomView={this.roomView}
@ -1879,13 +1941,33 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactElement {
return (
<RoomContext.Provider value={this.state}>
<WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView}
inviteEvent={inviteEvent}
/>
</RoomContext.Provider>
);
}
public render(): React.ReactNode { public render(): React.ReactNode {
if (this.state.room instanceof LocalRoom) { if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) { if (this.state.room.state === LocalRoomState.CREATING) {
return this.renderLocalRoomCreateLoader(); return this.renderLocalRoomCreateLoader();
} }
return this.renderLocalRoomView(); return this.renderLocalRoomView(this.state.room);
}
if (this.state.room) {
const { shouldEncrypt, inviteEvent } = shouldEncryptRoomWithSingle3rdPartyInvite(this.state.room);
if (shouldEncrypt) {
return this.renderWaitingForThirdPartyRoomView(inviteEvent);
}
} }
if (!this.state.room) { if (!this.state.room) {
@ -1903,6 +1985,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
loading={loading} loading={loading}
joining={this.state.joining} joining={this.state.joining}
oobData={this.props.oobData} oobData={this.props.oobData}
roomId={this.state.roomId}
/> />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
@ -1932,7 +2015,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
oobData={this.props.oobData} oobData={this.props.oobData}
signUrl={this.props.threepidInvite?.signUrl} signUrl={this.props.threepidInvite?.signUrl}
room={this.state.room} roomId={this.state.roomId}
/> />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
@ -1969,6 +2052,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
error={this.state.roomLoadError} error={this.state.roomLoadError}
joining={this.state.joining} joining={this.state.joining}
rejecting={this.state.rejecting} rejecting={this.state.rejecting}
roomId={this.state.roomId}
/> />
</ErrorBoundary> </ErrorBoundary>
); );
@ -1998,6 +2082,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canPreview={false} canPreview={false}
joining={this.state.joining} joining={this.state.joining}
room={this.state.room} room={this.state.room}
roomId={this.state.roomId}
/> />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
@ -2090,6 +2175,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
oobData={this.props.oobData} oobData={this.props.oobData}
canPreview={this.state.canPeek} canPreview={this.state.canPeek}
room={this.state.room} room={this.state.room}
roomId={this.state.roomId}
/> />
); );
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {

View file

@ -332,6 +332,7 @@ const Tile: React.FC<ITileProps> = ({
<li <li
className="mx_SpaceHierarchy_roomTileWrapper" className="mx_SpaceHierarchy_roomTileWrapper"
role="treeitem" role="treeitem"
aria-selected={selected}
aria-expanded={children ? showChildren : undefined} aria-expanded={children ? showChildren : undefined}
> >
<AccessibleButton <AccessibleButton

View file

@ -415,7 +415,7 @@ const SpaceAddExistingRooms: React.FC<{
{_t("Skip for now")} {_t("Skip for now")}
</AccessibleButton> </AccessibleButton>
} }
filterPlaceholder={_t("Search for rooms or spaces")} filterPlaceholder={_t("Search for rooms")}
onFinished={onFinished} onFinished={onFinished}
roomsRenderer={defaultRoomsRenderer} roomsRenderer={defaultRoomsRenderer}
dmsRenderer={defaultDmsRenderer} dmsRenderer={defaultDmsRenderer}

View file

@ -1473,9 +1473,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
"do not have permission to view the message in question.", "do not have permission to view the message in question.",
); );
} else { } else {
description = _t( description = _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
"Tried to load a specific point in this room's timeline, but was " + "unable to find it.",
);
} }
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {

View file

@ -43,7 +43,6 @@ import IconizedContextMenu, {
IconizedContextMenuOptionList, IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu"; } from "../views/context_menus/IconizedContextMenu";
import { UIFeature } from "../../settings/UIFeature"; import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import SpaceStore from "../../stores/spaces/SpaceStore"; import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
@ -290,7 +289,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (!this.state.contextMenuPosition) return null; if (!this.state.contextMenuPosition) return null;
let topSection; let topSection;
const hostSignupConfig = SdkConfig.getObject("host_signup");
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
topSection = ( topSection = (
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts"> <div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
@ -318,15 +316,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
)} )}
</div> </div>
); );
} else if (hostSignupConfig?.get("url")) {
// If hostSignup.domains is set to a non-empty array, only show
// dialog if the user is on the domain or a subdomain.
const hostSignupDomains = hostSignupConfig.get("domains") || [];
const mxDomain = MatrixClientPeg.get().getDomain();
const validDomains = hostSignupDomains.filter((d) => d === mxDomain || mxDomain.endsWith(`.${d}`));
if (!hostSignupConfig.get("domains") || validDomains.length > 0) {
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
}
} }
let homeButton = null; let homeButton = null;
@ -432,7 +421,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const avatarSize = 32; // should match border-radius of the avatar const avatarSize = 32; // should match border-radius of the avatar
const userId = MatrixClientPeg.get().getUserId(); const userId = MatrixClientPeg.get().getSafeUserId();
const displayName = OwnProfileStore.instance.displayName || userId; const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);

View file

@ -23,15 +23,15 @@ import { _t } from "../../languageHandler";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { canEditContent } from "../../utils/EventUtils"; import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import BaseDialog from "../views/dialogs/BaseDialog"; import BaseDialog from "../views/dialogs/BaseDialog";
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool"; import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
import { StateEventEditor } from "../views/dialogs/devtools/RoomState"; import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event"; import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
import CopyableText from "../views/elements/CopyableText"; import CopyableText from "../views/elements/CopyableText";
interface IProps extends IDialogProps { interface IProps {
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
onFinished(): void;
} }
interface IState { interface IState {
@ -139,15 +139,15 @@ export default class ViewSource extends React.Component<IProps, IState> {
private canSendStateEvent(mxEvent: MatrixEvent): boolean { private canSendStateEvent(mxEvent: MatrixEvent): boolean {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(mxEvent.getRoomId()); const room = cli.getRoom(mxEvent.getRoomId());
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); return !!room?.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
} }
public render(): React.ReactNode { 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 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 isEditing = this.state.isEditing;
const roomId = mxEvent.getRoomId(); const roomId = mxEvent.getRoomId()!;
const eventId = mxEvent.getId(); const eventId = mxEvent.getId()!;
const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
return ( return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}> <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>

View file

@ -0,0 +1,82 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { RefObject } from "react";
import { useRoomContext } from "../../contexts/RoomContext";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { E2EStatus } from "../../utils/ShieldUtils";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomHeader from "../views/rooms/RoomHeader";
import ScrollPanel from "./ScrollPanel";
import EventTileBubble from "../views/messages/EventTileBubble";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { _t } from "../../languageHandler";
interface Props {
roomView: RefObject<HTMLElement>;
resizeNotifier: ResizeNotifier;
inviteEvent: MatrixEvent;
}
/**
* Component that displays a waiting room for an encrypted DM with a third party invite.
* If encryption by default is enabled, DMs with a third party invite should be encrypted as well.
* To avoid UTDs, users are shown a waiting room until the others have joined.
*/
export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => {
const context = useRoomContext();
return (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
<RoomHeader
room={context.room}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
onForgetClick={null}
e2eStatus={E2EStatus.Normal}
onAppsClick={null}
appsShown={false}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
<main className="mx_RoomView_body" ref={roomView}>
<div className="mx_RoomView_timeline">
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Waiting for users to join Element")}
subtitle={_t(
"Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted",
)}
/>
<NewRoomIntro />
<UnwrappedEventTile mxEvent={inviteEvent} />
</ScrollPanel>
</div>
</main>
</ErrorBoundary>
</div>
);
};

View file

@ -368,7 +368,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
} }
public async renderConfirmLogoutDevicesDialog(): Promise<boolean> { public async renderConfirmLogoutDevicesDialog(): Promise<boolean> {
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
description: ( description: (
<div> <div>

View file

@ -39,6 +39,7 @@ import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { filterBoolean } from "../../../utils/arrays";
// These are used in several places, and come from the js-sdk's autodiscovery // 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. // 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<IProps, IState>
this.state = { this.state = {
busy: false, busy: false,
busyLoggingIn: null,
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
canTryLogin: true, canTryLogin: true,
flows: null,
username: props.defaultUsername ? props.defaultUsername : "", username: props.defaultUsername ? props.defaultUsername : "",
phoneCountry: null,
phoneNumber: "", phoneNumber: "",
serverIsAlive: true, serverIsAlive: true,
@ -167,7 +164,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
} }
} }
public isBusy = (): boolean => this.state.busy || this.props.busy; public isBusy = (): boolean => !!this.state.busy || !!this.props.busy;
public onPasswordLogin: OnPasswordLogin = async ( public onPasswordLogin: OnPasswordLogin = async (
username: string | undefined, username: string | undefined,
@ -349,7 +346,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const ssoKind = ssoFlow.type === "m.login.sso" ? "sso" : "cas"; const ssoKind = ssoFlow.type === "m.login.sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn( PlatformPeg.get()?.startSingleSignOn(
this.loginLogic.createTemporaryClient(), this.loginLogic.createTemporaryClient(),
ssoKind, ssoKind,
this.props.fragmentAfterLogin, this.props.fragmentAfterLogin,
@ -457,7 +454,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
} }
let errorText: ReactNode = let errorText: ReactNode =
_t("There was a problem communicating with the homeserver, " + "please try again later.") + _t("There was a problem communicating with the homeserver, please try again later.") +
(errCode ? " (" + errCode + ")" : ""); (errCode ? " (" + errCode + ")" : "");
if (err instanceof ConnectionError) { if (err instanceof ConnectionError) {
@ -511,13 +508,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
return errorText; return errorText;
} }
public renderLoginComponentForFlows(): JSX.Element { public renderLoginComponentForFlows(): ReactNode {
if (!this.state.flows) return null; if (!this.state.flows) return null;
// this is the ideal order we want to show the flows in // this is the ideal order we want to show the flows in
const order = ["m.login.password", "m.login.sso"]; 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 ( return (
<React.Fragment> <React.Fragment>
{flows.map((flow) => { {flows.map((flow) => {

View file

@ -78,9 +78,9 @@ interface IProps {
} }
interface IState { interface IState {
// true if we're waiting for the user to complete
busy: boolean; busy: boolean;
errorText?: ReactNode; errorText?: ReactNode;
// true if we're waiting for the user to complete
// We remember the values entered by the user because // We remember the values entered by the user because
// the registration form will be unmounted during the // the registration form will be unmounted during the
// course of registration, but if there's an error we // course of registration, but if there's an error we
@ -88,7 +88,7 @@ interface IState {
// values the user entered still in it. We can keep // values the user entered still in it. We can keep
// them in this component's state since this component // them in this component's state since this component
// persist for the duration of the registration process. // persist for the duration of the registration process.
formVals: Record<string, string>; formVals: Record<string, string | undefined>;
// user-interactive auth // user-interactive auth
// If we've been given a session ID, we're resuming // If we've been given a session ID, we're resuming
// straight back into UI auth // straight back into UI auth
@ -96,9 +96,11 @@ interface IState {
// If set, we've registered but are not going to log // If set, we've registered but are not going to log
// the user in to their new account automatically. // the user in to their new account automatically.
completedNoSignin: boolean; completedNoSignin: boolean;
flows: { flows:
stages: string[]; | {
}[]; stages: string[];
}[]
| null;
// We perform liveliness checks later, but for now suppress the errors. // We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so // We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may // that we can render it differently, and override any other error the user may
@ -158,7 +160,7 @@ export default class Registration extends React.Component<IProps, IState> {
window.removeEventListener("beforeunload", this.unloadCallback); window.removeEventListener("beforeunload", this.unloadCallback);
} }
private unloadCallback = (event: BeforeUnloadEvent): string => { private unloadCallback = (event: BeforeUnloadEvent): string | undefined => {
if (this.state.doingUIAuth) { if (this.state.doingUIAuth) {
event.preventDefault(); event.preventDefault();
event.returnValue = ""; event.returnValue = "";
@ -215,7 +217,7 @@ export default class Registration extends React.Component<IProps, IState> {
this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setHomeserverUrl(hsUrl);
this.loginLogic.setIdentityServerUrl(isUrl); this.loginLogic.setIdentityServerUrl(isUrl);
let ssoFlow: ISSOFlow; let ssoFlow: ISSOFlow | undefined;
try { try {
const loginFlows = await this.loginLogic.getFlows(); const loginFlows = await this.loginLogic.getFlows();
if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us
@ -289,6 +291,7 @@ export default class Registration extends React.Component<IProps, IState> {
sendAttempt: number, sendAttempt: number,
sessionId: string, sessionId: string,
): Promise<IRequestTokenResponse> => { ): Promise<IRequestTokenResponse> => {
if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded");
return this.state.matrixClient.requestRegisterEmailToken( return this.state.matrixClient.requestRegisterEmailToken(
emailAddress, emailAddress,
clientSecret, clientSecret,
@ -303,6 +306,8 @@ export default class Registration extends React.Component<IProps, IState> {
}; };
private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise<void> => { private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise<void> => {
if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded");
debuglog("Registration: ui authentication finished: ", { success, response }); debuglog("Registration: ui authentication finished: ", { success, response });
if (!success) { if (!success) {
let errorText: ReactNode = (response as Error).message || (response as Error).toString(); let errorText: ReactNode = (response as Error).message || (response as Error).toString();
@ -327,10 +332,8 @@ export default class Registration extends React.Component<IProps, IState> {
</div> </div>
); );
} else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) { } else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) {
let msisdnAvailable = false; const flows = (response as IAuthData).available_flows ?? [];
for (const flow of (response as IAuthData).available_flows) { const msisdnAvailable = flows.some((flow) => flow.stages.includes(AuthType.Msisdn));
msisdnAvailable = msisdnAvailable || flow.stages.includes(AuthType.Msisdn);
}
if (!msisdnAvailable) { if (!msisdnAvailable) {
errorText = _t("This server does not support authentication with a phone number."); errorText = _t("This server does not support authentication with a phone number.");
} }
@ -348,12 +351,16 @@ export default class Registration extends React.Component<IProps, IState> {
return; return;
} }
MatrixClientPeg.setJustRegisteredUserId((response as IAuthData).user_id); const userId = (response as IAuthData).user_id;
const accessToken = (response as IAuthData).access_token;
if (!userId || !accessToken) throw new Error("Registration failed");
MatrixClientPeg.setJustRegisteredUserId(userId);
const newState: Partial<IState> = { const newState: Partial<IState> = {
doingUIAuth: false, doingUIAuth: false,
registeredUsername: (response as IAuthData).user_id, registeredUsername: (response as IAuthData).user_id,
differentLoggedInUserId: null, differentLoggedInUserId: undefined,
completedNoSignin: false, completedNoSignin: false,
// we're still busy until we get unmounted: don't show the registration form again // we're still busy until we get unmounted: don't show the registration form again
busy: true, busy: true,
@ -393,13 +400,13 @@ export default class Registration extends React.Component<IProps, IState> {
// the email, not the client that started the registration flow // the email, not the client that started the registration flow
await this.props.onLoggedIn( await this.props.onLoggedIn(
{ {
userId: (response as IAuthData).user_id, userId,
deviceId: (response as IAuthData).device_id, deviceId: (response as IAuthData).device_id,
homeserverUrl: this.state.matrixClient.getHomeserverUrl(), homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
accessToken: (response as IAuthData).access_token, accessToken,
}, },
this.state.formVals.password, this.state.formVals.password!,
); );
this.setupPushers(); this.setupPushers();
@ -457,6 +464,8 @@ export default class Registration extends React.Component<IProps, IState> {
}; };
private makeRegisterRequest = (auth: IAuthData | null): Promise<IAuthData> => { private makeRegisterRequest = (auth: IAuthData | null): Promise<IAuthData> => {
if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded");
const registerParams: IRegisterRequestParams = { const registerParams: IRegisterRequestParams = {
username: this.state.formVals.username, username: this.state.formVals.username,
password: this.state.formVals.password, password: this.state.formVals.password,
@ -494,7 +503,7 @@ export default class Registration extends React.Component<IProps, IState> {
return sessionLoaded; return sessionLoaded;
}; };
private renderRegisterComponent(): JSX.Element { private renderRegisterComponent(): ReactNode {
if (this.state.matrixClient && this.state.doingUIAuth) { if (this.state.matrixClient && this.state.doingUIAuth) {
return ( return (
<InteractiveAuth <InteractiveAuth
@ -517,8 +526,8 @@ export default class Registration extends React.Component<IProps, IState> {
<Spinner /> <Spinner />
</div> </div>
); );
} else if (this.state.flows.length) { } else if (this.state.matrixClient && this.state.flows.length) {
let ssoSection; let ssoSection: JSX.Element | undefined;
if (this.state.ssoFlow) { if (this.state.ssoFlow) {
let continueWithSection; let continueWithSection;
const providers = this.state.ssoFlow.identity_providers || []; const providers = this.state.ssoFlow.identity_providers || [];
@ -571,6 +580,8 @@ export default class Registration extends React.Component<IProps, IState> {
</React.Fragment> </React.Fragment>
); );
} }
return null;
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton"; import AccessibleButton from "../../../views/elements/AccessibleButton";
@ -26,7 +26,8 @@ import { ErrorMessage } from "../../ErrorMessage";
interface Props { interface Props {
email: string; email: string;
errorText: string | null; errorText: ReactNode | null;
onFinished(): void; // This modal is weird in that the way you close it signals intent
onCloseClick: () => void; onCloseClick: () => void;
onReEnterEmailClick: () => void; onReEnterEmailClick: () => void;
onResendClick: () => Promise<boolean>; onResendClick: () => Promise<boolean>;

View file

@ -60,7 +60,7 @@ export default class PlayPauseButton extends React.PureComponent<IProps> {
return ( return (
<AccessibleTooltipButton <AccessibleTooltipButton
data-test-id="play-pause-button" data-testid="play-pause-button"
className={classes} className={classes}
title={isPlaying ? _t("Pause") : _t("Play")} title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick} onClick={this.onClick}

View file

@ -65,7 +65,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
"src", "src",
`https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`, `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`,
); );
this.recaptchaContainer.current.appendChild(scriptTag); this.recaptchaContainer.current?.appendChild(scriptTag);
} }
} }
@ -91,11 +91,11 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
const publicKey = this.props.sitePublicKey; const publicKey = this.props.sitePublicKey;
if (!publicKey) { if (!publicKey) {
logger.error("No public key for recaptcha!"); logger.error("No public key for recaptcha!");
throw new Error("This server has not supplied enough information for Recaptcha " + "authentication"); throw new Error("This server has not supplied enough information for Recaptcha authentication");
} }
logger.info("Rendering to %s", divId); logger.info("Rendering to %s", divId);
this.captchaWidgetId = global.grecaptcha.render(divId, { this.captchaWidgetId = global.grecaptcha?.render(divId, {
sitekey: publicKey, sitekey: publicKey,
callback: this.props.onCaptchaResponse, callback: this.props.onCaptchaResponse,
}); });
@ -113,7 +113,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
this.renderRecaptcha(DIV_ID); this.renderRecaptcha(DIV_ID);
// clear error if re-rendered // clear error if re-rendered
this.setState({ this.setState({
errorText: null, errorText: undefined,
}); });
} catch (e) { } catch (e) {
this.setState({ this.setState({
@ -123,7 +123,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
} }
public render(): React.ReactNode { public render(): React.ReactNode {
let error = null; let error: JSX.Element | undefined;
if (this.state.errorText) { if (this.state.errorText) {
error = <div className="error">{this.state.errorText}</div>; error = <div className="error">{this.state.errorText}</div>;
} }

View file

@ -50,12 +50,12 @@ class EmailField extends PureComponent<IProps> {
{ {
key: "required", key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value, test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t(this.props.labelRequired), invalid: () => _t(this.props.labelRequired!),
}, },
{ {
key: "email", key: "email",
test: ({ value }) => !value || Email.looksValid(value), test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t(this.props.labelInvalid), invalid: () => _t(this.props.labelInvalid!),
}, },
], ],
}); });
@ -80,7 +80,7 @@ class EmailField extends PureComponent<IProps> {
id={this.props.id} id={this.props.id}
ref={this.props.fieldRef} ref={this.props.fieldRef}
type="text" type="text"
label={_t(this.props.label)} label={_t(this.props.label!)}
value={this.props.value} value={this.props.value}
autoFocus={this.props.autoFocus} autoFocus={this.props.autoFocus}
onChange={this.props.onChange} onChange={this.props.onChange}

View file

@ -83,7 +83,7 @@ export const DEFAULT_PHASE = 0;
interface IAuthEntryProps { interface IAuthEntryProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
loginType: string; loginType: string;
authSessionId: string; authSessionId?: string;
errorText?: string; errorText?: string;
errorCode?: string; errorCode?: string;
// Is the auth logic currently waiting for something to happen? // Is the auth logic currently waiting for something to happen?
@ -120,7 +120,7 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
type: AuthType.Password, type: AuthType.Password,
// TODO: Remove `user` once servers support proper UIA // TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/vector-im/element-web/issues/10312
user: this.props.matrixClient.credentials.userId, user: this.props.matrixClient.credentials.userId ?? undefined,
identifier: { identifier: {
type: "m.id.user", type: "m.id.user",
user: this.props.matrixClient.credentials.userId, user: this.props.matrixClient.credentials.userId,
@ -286,7 +286,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
// } // }
// } // }
const allPolicies = this.props.stageParams.policies || {}; const allPolicies = this.props.stageParams?.policies || {};
const prefLang = SettingsStore.getValue("language"); const prefLang = SettingsStore.getValue("language");
const initToggles: Record<string, boolean> = {}; const initToggles: Record<string, boolean> = {};
const pickedPolicies: { const pickedPolicies: {
@ -300,12 +300,12 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
// Pick a language based on the user's language, falling back to english, // Pick a language based on the user's language, falling back to english,
// and finally to the first language available. If there's still no policy // and finally to the first language available. If there's still no policy
// available then the homeserver isn't respecting the spec. // available then the homeserver isn't respecting the spec.
let langPolicy = policy[prefLang]; let langPolicy: LocalisedPolicy | undefined = policy[prefLang];
if (!langPolicy) langPolicy = policy["en"]; if (!langPolicy) langPolicy = policy["en"];
if (!langPolicy) { if (!langPolicy) {
// last resort // last resort
const firstLang = Object.keys(policy).find((e) => e !== "version"); const firstLang = Object.keys(policy).find((e) => e !== "version");
langPolicy = policy[firstLang]; langPolicy = firstLang ? policy[firstLang] : undefined;
} }
if (!langPolicy) throw new Error("Failed to find a policy to show the user"); if (!langPolicy) throw new Error("Failed to find a policy to show the user");
@ -358,7 +358,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
return <Spinner />; return <Spinner />;
} }
const checkboxes = []; const checkboxes: JSX.Element[] = [];
let allChecked = true; let allChecked = true;
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id]; const checked = this.state.toggledPolicies[policy.id];
@ -384,7 +384,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
); );
} }
let submitButton; let submitButton: JSX.Element | undefined;
if (this.props.showContinue !== false) { if (this.props.showContinue !== false) {
// XXX: button classes // XXX: button classes
submitButton = ( submitButton = (
@ -462,7 +462,7 @@ export class EmailIdentityAuthEntry extends React.Component<
// We only have a session ID if the user has clicked the link in their email, // We only have a session ID if the user has clicked the link in their email,
// so show a loading state instead of "an email has been sent to..." because // so show a loading state instead of "an email has been sent to..." because
// that's confusing when you've already read that email. // that's confusing when you've already read that email.
if (this.props.inputs.emailAddress === undefined || this.props.stageState?.emailSid) { if (this.props.inputs?.emailAddress === undefined || this.props.stageState?.emailSid) {
if (errorSection) { if (errorSection) {
return errorSection; return errorSection;
} }
@ -549,13 +549,13 @@ interface IMsisdnAuthEntryProps extends IAuthEntryProps {
interface IMsisdnAuthEntryState { interface IMsisdnAuthEntryState {
token: string; token: string;
requestingToken: boolean; requestingToken: boolean;
errorText: string; errorText: string | null;
} }
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> { export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
public static LOGIN_TYPE = AuthType.Msisdn; public static LOGIN_TYPE = AuthType.Msisdn;
private submitUrl: string; private submitUrl?: string;
private sid: string; private sid: string;
private msisdn: string; private msisdn: string;
@ -798,11 +798,13 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
public static PHASE_POSTAUTH = 2; // button to confirm SSO completed public static PHASE_POSTAUTH = 2; // button to confirm SSO completed
private ssoUrl: string; private ssoUrl: string;
private popupWindow: Window; private popupWindow: Window | null;
public constructor(props: ISSOAuthEntryProps) { public constructor(props: ISSOAuthEntryProps) {
super(props); super(props);
if (!this.props.authSessionId) throw new Error("This UIA flow requires an authSessionId");
// We actually send the user through fallback auth so we don't have to // We actually send the user through fallback auth so we don't have to
// deal with a redirect back to us, losing application context. // deal with a redirect back to us, losing application context.
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(this.props.loginType, this.props.authSessionId); this.ssoUrl = props.matrixClient.getFallbackAuthUrl(this.props.loginType, this.props.authSessionId);
@ -858,10 +860,10 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
let continueButton = null; let continueButton: JSX.Element;
const cancelButton = ( const cancelButton = (
<AccessibleButton <AccessibleButton
onClick={this.props.onCancel} onClick={this.props.onCancel ?? null}
kind={this.props.continueKind ? this.props.continueKind + "_outline" : "primary_outline"} kind={this.props.continueKind ? this.props.continueKind + "_outline" : "primary_outline"}
> >
{_t("Cancel")} {_t("Cancel")}
@ -909,7 +911,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
} }
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> { export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
private popupWindow: Window; private popupWindow: Window | null;
private fallbackButton = createRef<HTMLButtonElement>(); private fallbackButton = createRef<HTMLButtonElement>();
public constructor(props: IAuthEntryProps) { public constructor(props: IAuthEntryProps) {
@ -927,18 +929,16 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
public componentWillUnmount(): void { public componentWillUnmount(): void {
window.removeEventListener("message", this.onReceiveMessage); window.removeEventListener("message", this.onReceiveMessage);
if (this.popupWindow) { this.popupWindow?.close();
this.popupWindow.close();
}
} }
public focus = (): void => { public focus = (): void => {
if (this.fallbackButton.current) { this.fallbackButton.current?.focus();
this.fallbackButton.current.focus();
}
}; };
private onShowFallbackClick = (e: MouseEvent): void => { private onShowFallbackClick = (e: MouseEvent): void => {
if (!this.props.authSessionId) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -26,7 +26,7 @@ import LanguageDropdown from "../elements/LanguageDropdown";
function onChange(newLang: string): void { function onChange(newLang: string): void {
if (getCurrentLanguage() !== newLang) { if (getCurrentLanguage() !== newLang) {
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload(); PlatformPeg.get()?.reload();
} }
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports"; import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports";
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels"; import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
@ -158,7 +158,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
client: this.props.client, client: this.props.client,
}); });
const channel = new MSC3903ECDHv1RendezvousChannel<MSC3906RendezvousPayload>( const channel = new MSC3903ECDHv2RendezvousChannel<MSC3906RendezvousPayload>(
transport, transport,
undefined, undefined,
this.onFailure, this.onFailure,

View file

@ -20,15 +20,16 @@ import Field, { IInputProps } from "../elements/Field";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
interface IProps extends Omit<IInputProps, "onValidate"> { interface IProps extends Omit<IInputProps, "onValidate" | "label"> {
id?: string; id?: string;
fieldRef?: RefCallback<Field> | RefObject<Field>; fieldRef?: RefCallback<Field> | RefObject<Field>;
autoComplete?: string; autoComplete?: string;
value: string; value: string;
password: string; // The password we're confirming password: string; // The password we're confirming
labelRequired?: string; label: string;
labelInvalid?: string; labelRequired: string;
labelInvalid: string;
onChange(ev: React.FormEvent<HTMLElement>): void; onChange(ev: React.FormEvent<HTMLElement>): void;
onValidate?(result: IValidationResult): void; onValidate?(result: IValidationResult): void;

View file

@ -31,10 +31,10 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
value: string; value: string;
fieldRef?: RefCallback<Field> | RefObject<Field>; fieldRef?: RefCallback<Field> | RefObject<Field>;
label?: string; label: string;
labelEnterPassword?: string; labelEnterPassword: string;
labelStrongPassword?: string; labelStrongPassword: string;
labelAllowedButUnsafe?: string; labelAllowedButUnsafe: string;
onChange(ev: React.FormEvent<HTMLElement>): void; onChange(ev: React.FormEvent<HTMLElement>): void;
onValidate?(result: IValidationResult): void; onValidate?(result: IValidationResult): void;
@ -48,12 +48,12 @@ class PassphraseField extends PureComponent<IProps> {
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
}; };
public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({ public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult | null>({
description: function (complexity) { description: function (complexity) {
const score = complexity ? complexity.score : 0; const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />; return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
}, },
deriveData: async ({ value }): Promise<zxcvbn.ZXCVBNResult> => { deriveData: async ({ value }): Promise<zxcvbn.ZXCVBNResult | null> => {
if (!value) return null; if (!value) return null;
const { scorePassword } = await import("../../../utils/PasswordScorer"); const { scorePassword } = await import("../../../utils/PasswordScorer");
return scorePassword(value); return scorePassword(value);
@ -67,7 +67,7 @@ class PassphraseField extends PureComponent<IProps> {
{ {
key: "complexity", key: "complexity",
test: async function ({ value }, complexity): Promise<boolean> { test: async function ({ value }, complexity): Promise<boolean> {
if (!value) { if (!value || !complexity) {
return false; return false;
} }
const safe = complexity.score >= this.props.minScore; const safe = complexity.score >= this.props.minScore;
@ -78,7 +78,7 @@ class PassphraseField extends PureComponent<IProps> {
// Unsafe passwords that are valid are only possible through a // Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal // configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe. // to the user that their password is allowed, but unsafe.
if (complexity.score >= this.props.minScore) { if (complexity && complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword); return _t(this.props.labelStrongPassword);
} }
return _t(this.props.labelAllowedButUnsafe); return _t(this.props.labelAllowedButUnsafe);

View file

@ -36,7 +36,7 @@ interface IProps {
phoneNumber: string; phoneNumber: string;
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
loginIncorrect?: boolean; loginIncorrect: boolean;
disableSubmit?: boolean; disableSubmit?: boolean;
busy?: boolean; busy?: boolean;
@ -67,9 +67,9 @@ const enum LoginField {
* The email/username/phone fields are fully-controlled, the password field is not. * The email/username/phone fields are fully-controlled, the password field is not.
*/ */
export default class PasswordLogin extends React.PureComponent<IProps, IState> { export default class PasswordLogin extends React.PureComponent<IProps, IState> {
private [LoginField.Email]: Field; private [LoginField.Email]: Field | null;
private [LoginField.Phone]: Field; private [LoginField.Phone]: Field | null;
private [LoginField.MatrixId]: Field; private [LoginField.MatrixId]: Field | null;
public static defaultProps = { public static defaultProps = {
onUsernameChanged: function () {}, onUsernameChanged: function () {},
@ -93,7 +93,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
private onForgotPasswordClick = (ev: ButtonEvent): void => { private onForgotPasswordClick = (ev: ButtonEvent): void => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onForgotPasswordClick(); this.props.onForgotPasswordClick?.();
}; };
private onSubmitForm = async (ev: SyntheticEvent): Promise<void> => { private onSubmitForm = async (ev: SyntheticEvent): Promise<void> => {
@ -116,25 +116,25 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
}; };
private onUsernameChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => { private onUsernameChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.props.onUsernameChanged(ev.target.value); this.props.onUsernameChanged?.(ev.target.value);
}; };
private onUsernameBlur = (ev: React.FocusEvent<HTMLInputElement>): void => { private onUsernameBlur = (ev: React.FocusEvent<HTMLInputElement>): void => {
this.props.onUsernameBlur(ev.target.value); this.props.onUsernameBlur?.(ev.target.value);
}; };
private onLoginTypeChange = (ev: React.ChangeEvent<HTMLSelectElement>): void => { private onLoginTypeChange = (ev: React.ChangeEvent<HTMLSelectElement>): void => {
const loginType = ev.target.value as IState["loginType"]; const loginType = ev.target.value as IState["loginType"];
this.setState({ 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 => { private onPhoneCountryChanged = (country: PhoneNumberCountryDefinition): void => {
this.props.onPhoneCountryChanged(country.iso2); this.props.onPhoneCountryChanged?.(country.iso2);
}; };
private onPhoneNumberChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => { private onPhoneNumberChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.props.onPhoneNumberChanged(ev.target.value); this.props.onPhoneNumberChanged?.(ev.target.value);
}; };
private onPasswordChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => { private onPasswordChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
@ -199,7 +199,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
return null; return null;
} }
private markFieldValid(fieldID: LoginField, valid: boolean): void { private markFieldValid(fieldID: LoginField, valid?: boolean): void {
const { fieldValid } = this.state; const { fieldValid } = this.state;
fieldValid[fieldID] = valid; fieldValid[fieldID] = valid;
this.setState({ this.setState({
@ -368,7 +368,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
} }
public render(): React.ReactNode { public render(): React.ReactNode {
let forgotPasswordJsx; let forgotPasswordJsx: JSX.Element | undefined;
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = ( forgotPasswordJsx = (

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { BaseSyntheticEvent } from "react"; import React, { BaseSyntheticEvent, ReactNode } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixError } from "matrix-js-sdk/src/matrix"; import { MatrixError } from "matrix-js-sdk/src/matrix";
@ -82,7 +82,7 @@ interface IState {
// Field error codes by field ID // Field error codes by field ID
fieldValid: Partial<Record<RegistrationField, boolean>>; fieldValid: Partial<Record<RegistrationField, boolean>>;
// The ISO2 country code selected in the phone number entry // The ISO2 country code selected in the phone number entry
phoneCountry: string; phoneCountry?: string;
username: string; username: string;
email: string; email: string;
phoneNumber: string; phoneNumber: string;
@ -95,11 +95,11 @@ interface IState {
* A pure UI component which displays a registration form. * A pure UI component which displays a registration form.
*/ */
export default class RegistrationForm extends React.PureComponent<IProps, IState> { export default class RegistrationForm extends React.PureComponent<IProps, IState> {
private [RegistrationField.Email]: Field; private [RegistrationField.Email]: Field | null;
private [RegistrationField.Password]: Field; private [RegistrationField.Password]: Field | null;
private [RegistrationField.PasswordConfirm]: Field; private [RegistrationField.PasswordConfirm]: Field | null;
private [RegistrationField.Username]: Field; private [RegistrationField.Username]: Field | null;
private [RegistrationField.PhoneNumber]: Field; private [RegistrationField.PhoneNumber]: Field | null;
public static defaultProps = { public static defaultProps = {
onValidationChange: logger.error, onValidationChange: logger.error,
@ -117,7 +117,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
phoneNumber: this.props.defaultPhoneNumber || "", phoneNumber: this.props.defaultPhoneNumber || "",
password: this.props.defaultPassword || "", password: this.props.defaultPassword || "",
passwordConfirm: this.props.defaultPassword || "", passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
}; };
} }
@ -138,7 +137,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (this.showEmail()) { if (this.showEmail()) {
Modal.createDialog(RegistrationEmailPromptDialog, { Modal.createDialog(RegistrationEmailPromptDialog, {
onFinished: async (confirmed: boolean, email?: string): Promise<void> => { onFinished: async (confirmed: boolean, email?: string): Promise<void> => {
if (confirmed) { if (confirmed && email !== undefined) {
this.setState( this.setState(
{ {
email, email,
@ -265,7 +264,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
}; };
private onEmailValidate = (result: IValidationResult): void => { private onEmailValidate = (result: IValidationResult): void => {
this.markFieldValid(RegistrationField.Email, result.valid); this.markFieldValid(RegistrationField.Email, !!result.valid);
}; };
private validateEmailRules = withValidation({ private validateEmailRules = withValidation({
@ -294,7 +293,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
}; };
private onPasswordValidate = (result: IValidationResult): void => { private onPasswordValidate = (result: IValidationResult): void => {
this.markFieldValid(RegistrationField.Password, result.valid); this.markFieldValid(RegistrationField.Password, !!result.valid);
}; };
private onPasswordConfirmChange = (ev: React.ChangeEvent<HTMLInputElement>): void => { private onPasswordConfirmChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
@ -304,7 +303,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
}; };
private onPasswordConfirmValidate = (result: IValidationResult): void => { private onPasswordConfirmValidate = (result: IValidationResult): void => {
this.markFieldValid(RegistrationField.PasswordConfirm, result.valid); this.markFieldValid(RegistrationField.PasswordConfirm, !!result.valid);
}; };
private onPhoneCountryChange = (newVal: PhoneNumberCountryDefinition): void => { private onPhoneCountryChange = (newVal: PhoneNumberCountryDefinition): void => {
@ -321,7 +320,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
private onPhoneNumberValidate = async (fieldState: IFieldState): Promise<IValidationResult> => { private onPhoneNumberValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validatePhoneNumberRules(fieldState); const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(RegistrationField.PhoneNumber, result.valid); this.markFieldValid(RegistrationField.PhoneNumber, !!result.valid);
return result; return result;
}; };
@ -352,14 +351,14 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
private onUsernameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => { private onUsernameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validateUsernameRules(fieldState); const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(RegistrationField.Username, result.valid); this.markFieldValid(RegistrationField.Username, !!result.valid);
return result; return result;
}; };
private validateUsernameRules = withValidation<this, UsernameAvailableStatus>({ private validateUsernameRules = withValidation<this, UsernameAvailableStatus>({
description: (_, results) => { description: (_, results) => {
// omit the description if the only failing result is the `available` one as it makes no sense for it. // omit the description if the only failing result is the `available` one as it makes no sense for it.
if (results.every(({ key, valid }) => key === "available" || valid)) return; if (results.every(({ key, valid }) => key === "available" || valid)) return null;
return _t("Use lowercase letters, numbers, dashes and underscores only"); return _t("Use lowercase letters, numbers, dashes and underscores only");
}, },
hideDescriptionIfValid: true, hideDescriptionIfValid: true,
@ -448,7 +447,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
return true; return true;
} }
private renderEmail(): JSX.Element { private renderEmail(): ReactNode {
if (!this.showEmail()) { if (!this.showEmail()) {
return null; return null;
} }
@ -492,7 +491,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
); );
} }
public renderPhoneNumber(): JSX.Element { public renderPhoneNumber(): ReactNode {
if (!this.showPhoneNumber()) { if (!this.showPhoneNumber()) {
return null; return null;
} }
@ -518,7 +517,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
); );
} }
public renderUsername(): JSX.Element { public renderUsername(): ReactNode {
return ( return (
<Field <Field
id="mx_RegistrationForm_username" id="mx_RegistrationForm_username"
@ -534,12 +533,12 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
); );
} }
public render(): React.ReactNode { public render(): ReactNode {
const registerButton = ( const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
); );
let emailHelperText = null; let emailHelperText: JSX.Element | undefined;
if (this.showEmail()) { if (this.showEmail()) {
if (this.showPhoneNumber()) { if (this.showPhoneNumber()) {
emailHelperText = ( emailHelperText = (

Some files were not shown because too many files have changed in this diff Show more