Merge branch 'develop' into staging
This commit is contained in:
commit
47b5ff55cc
542 changed files with 10290 additions and 10153 deletions
|
@ -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
|
||||||
|
|
26
.eslintrc.js
26
.eslintrc.js
|
@ -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: {
|
||||||
|
|
32
.github/workflows/cypress.yaml
vendored
32
.github/workflows/cypress.yaml
vendored
|
@ -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:
|
||||||
|
|
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
|
@ -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'
|
||||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -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.
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 can’t test finding rooms on other homeservers/other protocols
|
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||||
// We obviously don’t have federation or bridges in cypress tests
|
// We obviously don’t 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(() => {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
43
package.json
43
package.json
|
@ -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)"
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 }[];
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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[]> = {
|
||||||
|
|
111
src/Modal.tsx
111
src/Modal.tsx
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>>;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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[];
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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")}>
|
||||||
|
|
82
src/components/structures/WaitingForThirdPartyRoomView.tsx
Normal file
82
src/components/structures/WaitingForThirdPartyRoomView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue