Merge branch 'develop' into PlaybackContainer

This commit is contained in:
Suguru Hirahara 2023-02-25 07:10:38 +00:00 committed by GitHub
commit b51ea6546e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
317 changed files with 5781 additions and 5175 deletions

View file

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

View file

@ -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
@ -162,8 +165,9 @@ jobs:
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }} PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }} PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }} PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
# We manually finalize the build in the report stage
PERCY_PARALLEL_TOTAL: -1
- name: Upload Artifact - name: Upload Artifact
if: failure() if: failure()
@ -181,14 +185,35 @@ jobs:
with: with:
name: cypress-junit name: cypress-junit
path: cypress/results path: cypress/results
report: report:
name: Report results name: Report results
needs: tests needs:
- prepare
- tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
permissions: permissions:
statuses: write statuses: write
steps: steps:
- name: Finalize Percy
if: needs.prepare.outputs.percy_enable == '1'
run: npx -p @percy/cli percy build:finalize
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
- name: Skip Percy required check
if: needs.prepare.outputs.percy_enable != '1'
uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
description: Percy skipped
context: percy/matrix-react-sdk
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- uses: Sibz/github-status-action@v1 - uses: Sibz/github-status-action@v1
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
@ -196,6 +221,7 @@ jobs:
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
testrail: testrail:
name: Report results to testrail name: Report results to testrail
needs: needs:

View file

@ -6,7 +6,5 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs: jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with:
labels: "T-Defect,T-Enhancement,T-Task"
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -7,6 +7,10 @@ on:
types: [upstream-sdk-notify] types: [upstream-sdk-notify]
workflow_call: workflow_call:
inputs: inputs:
disable_coverage:
type: boolean
required: false
description: "Specify true to skip generating and uploading coverage for tests"
matrix-js-sdk-sha: matrix-js-sdk-sha:
type: string type: string
required: false required: false
@ -39,16 +43,21 @@ jobs:
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage and metrics - name: Load metrics reporter
id: metrics
if: github.ref == 'refs/heads/develop' if: github.ref == 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}" run: |
echo "extra-reporter='--reporters=<rootDir>/test/slowReporter.js'" >> $GITHUB_OUTPUT
- name: Run tests with coverage - name: Run tests
if: github.ref != 'refs/heads/develop' run: |
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}" yarn ${{ inputs.disable_coverage != 'true' && 'coverage' || 'test' }} \
--ci \
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
--max-workers ${{ steps.cpu-cores.outputs.count }}
- name: Upload Artifact - name: Upload Artifact
if: inputs.matrix-js-sdk-sha == '' if: inputs.disable_coverage != 'true'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: coverage name: coverage

View file

@ -118,7 +118,12 @@ describe("Decryption Failure Bar", () => {
"Verify this device to access all messages", "Verify this device to access all messages",
); );
cy.percySnapshot("DecryptionFailureBar prompts user to verify"); cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
"DecryptionFailureBar prompts user to verify",
{
widths: [320, 640],
},
);
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
cy.contains(".mx_DecryptionFailureBar_button", "Verify").click(); cy.contains(".mx_DecryptionFailureBar_button", "Verify").click();
@ -146,8 +151,11 @@ describe("Decryption Failure Bar", () => {
"Open another device to load encrypted messages", "Open another device to load encrypted messages",
); );
cy.percySnapshot( cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
"DecryptionFailureBar prompts user to open another device, with Resend Key Requests button", "DecryptionFailureBar prompts user to open another device, with Resend Key Requests button",
{
widths: [320, 640],
},
); );
cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest"); cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest");
@ -155,8 +163,11 @@ describe("Decryption Failure Bar", () => {
cy.wait("@keyRequest"); cy.wait("@keyRequest");
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
cy.percySnapshot( cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
"DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button", "DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button",
{
widths: [320, 640],
},
); );
}, },
); );
@ -177,7 +188,9 @@ describe("Decryption Failure Bar", () => {
"Reset your keys to prevent future decryption errors", "Reset your keys to prevent future decryption errors",
); );
cy.percySnapshot("DecryptionFailureBar prompts user to reset keys"); cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar prompts user to reset keys", {
widths: [320, 640],
});
cy.contains(".mx_DecryptionFailureBar_button", "Reset").click(); cy.contains(".mx_DecryptionFailureBar_button", "Reset").click();
@ -196,7 +209,12 @@ describe("Decryption Failure Bar", () => {
"Some messages could not be decrypted", "Some messages could not be decrypted",
); );
cy.percySnapshot("DecryptionFailureBar displays general message with no call to action"); cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
"DecryptionFailureBar displays general message with no call to action",
{
widths: [320, 640],
},
);
}, },
); );
@ -210,7 +228,10 @@ describe("Decryption Failure Bar", () => {
cy.get(".mx_DecryptionFailureBar").should("exist"); cy.get(".mx_DecryptionFailureBar").should("exist");
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist"); cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist");
cy.percySnapshot("DecryptionFailureBar displays loading spinner"); cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar displays loading spinner", {
allowSpinners: true,
widths: [320, 640],
});
cy.wait(5000); cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist"); cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist");

View file

@ -27,7 +27,7 @@ describe("Location sharing", () => {
}; };
const submitShareLocation = (): void => { const submitShareLocation = (): void => {
cy.get('[data-test-id="location-picker-submit-button"]').click(); cy.get('[data-testid="location-picker-submit-button"]').click();
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -159,8 +159,8 @@ describe("Timeline", () => {
".mx_GenericEventListSummary_summary", ".mx_GenericEventListSummary_summary",
"created and configured the room.", "created and configured the room.",
).should("exist"); ).should("exist");
cy.get(".mx_Spinner").should("not.exist");
cy.percySnapshot("Configured room on IRC layout"); cy.get(".mx_MainSplit").percySnapshotElement("Configured room on IRC layout");
}); });
it("should add inline start margin to an event line on IRC layout", () => { it("should add inline start margin to an event line on IRC layout", () => {
@ -185,11 +185,12 @@ describe("Timeline", () => {
.should("have.css", "margin-inline-start", "104px") .should("have.css", "margin-inline-start", "104px")
.should("have.css", "inset-inline-start", "0px"); .should("have.css", "inset-inline-start", "0px");
cy.get(".mx_Spinner").should("not.exist");
// Exclude timestamp from snapshot // Exclude timestamp from snapshot
const percyCSS = const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }"; ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS }); cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
percyCSS,
});
cy.checkA11y(); cy.checkA11y();
}); });
@ -213,8 +214,7 @@ describe("Timeline", () => {
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 from snapshot
const percyCSS = const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp { 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,14 +223,20 @@ 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 top left of view source event toggle", () => {
@ -329,7 +335,12 @@ describe("Timeline", () => {
cy.wait("@mxc"); cy.wait("@mxc");
cy.checkA11y(); cy.checkA11y();
// Exclude timestamp from snapshot
const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", { cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
percyCSS,
widths: [800, 400], widths: [800, 400],
}); });
}); });

View file

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

View file

@ -22,6 +22,7 @@ declare global {
namespace Cypress { namespace Cypress {
interface SnapshotOptions extends PercySnapshotOptions { interface SnapshotOptions extends PercySnapshotOptions {
domTransformation?: (documentClone: Document) => void; domTransformation?: (documentClone: Document) => void;
allowSpinners?: boolean;
} }
interface Chainable { interface Chainable {
@ -38,6 +39,10 @@ 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").should("not.exist");
}
cy.percySnapshot(name, { cy.percySnapshot(name, {
domTransformation: (documentClone) => scope(documentClone, subject.selector), domTransformation: (documentClone) => scope(documentClone, subject.selector),
...options, ...options,

View file

@ -58,7 +58,7 @@
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.4.0", "@matrix-org/analytics-events": "^0.4.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",
@ -92,7 +92,7 @@
"lodash": "^4.17.20", "lodash": "^4.17.20",
"maplibre-gl": "^2.0.0", "maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "2.0.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.1.1", "matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5", "minimist": "^1.2.5",
@ -111,7 +111,7 @@
"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.3.2", "sanitize-html": "2.8.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",
@ -167,9 +167,8 @@
"@types/react": "17.0.49", "@types/react": "17.0.49",
"@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.17",
"@types/react-test-renderer": "^17.0.1",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1", "@types/sanitize-html": "2.8.0",
"@types/tar-js": "^0.3.2", "@types/tar-js": "^0.3.2",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
@ -212,7 +211,6 @@
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"prettier": "2.8.0", "prettier": "2.8.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stylelint": "^14.9.1", "stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.4", "stylelint-config-prettier": "^9.0.4",

View file

@ -18,6 +18,7 @@
@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/_PollListItem.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/elements/_FilterDropdown.pcss"; @import "./components/views/elements/_FilterDropdown.pcss";
@import "./components/views/elements/_FilterTabGroup.pcss"; @import "./components/views/elements/_FilterTabGroup.pcss";
@import "./components/views/elements/_LearnMore.pcss"; @import "./components/views/elements/_LearnMore.pcss";

View file

@ -0,0 +1,60 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_PollListItemEnded {
width: 100%;
display: flex;
flex-direction: column;
color: $primary-content;
}
.mx_PollListItemEnded_title {
display: grid;
justify-content: left;
align-items: center;
grid-gap: $spacing-8;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: auto;
}
.mx_PollListItemEnded_icon {
height: 14px;
width: 14px;
color: $quaternary-content;
padding-left: $spacing-8;
}
.mx_PollListItemEnded_date {
font-size: $font-12px;
color: $secondary-content;
}
.mx_PollListItemEnded_question {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_PollListItemEnded_answers {
display: grid;
grid-gap: $spacing-8;
margin-top: $spacing-12;
}
.mx_PollListItemEnded_voteCount {
// 6px to match PollOption padding
margin: $spacing-8 0 0 6px;
}

View file

@ -18,7 +18,6 @@ limitations under the License.
border: 1px solid $quinary-content; border: 1px solid $quinary-content;
border-radius: 8px; border-radius: 8px;
padding: 6px 12px; padding: 6px 12px;
max-width: 550px;
background-color: $background; background-color: $background;
.mx_StyledRadioButton_content, .mx_StyledRadioButton_content,

View file

@ -32,6 +32,10 @@ limitations under the License.
grid-gap: $spacing-20; grid-gap: $spacing-20;
padding-right: $spacing-64; padding-right: $spacing-64;
margin: $spacing-32 0; margin: $spacing-32 0;
&.mx_PollHistoryList_list_ENDED {
grid-gap: $spacing-32;
}
} }
.mx_PollHistoryList_noResults { .mx_PollHistoryList_noResults {
@ -42,3 +46,14 @@ limitations under the License.
justify-content: center; justify-content: center;
color: $secondary-content; color: $secondary-content;
} }
.mx_PollHistoryList_loading {
color: $secondary-content;
text-align: center;
// center in all free space
// when there are no results
&.mx_PollHistoryList_noResultsYet {
margin: auto auto;
}
}

View file

@ -23,11 +23,12 @@ limitations under the License.
max-width: 100%; max-width: 100%;
&.mx_CopyableText_border { &.mx_CopyableText_border {
overflow: auto;
border-radius: 5px; border-radius: 5px;
border: solid 1px $light-fg-color; border: solid 1px $light-fg-color;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 10px; margin-top: 10px;
padding: 10px; padding: 10px 0 10px 10px;
} }
.mx_CopyableText_copyButton { .mx_CopyableText_copyButton {
@ -36,11 +37,15 @@ 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 */
position: sticky;
right: 0;
/* center to first line */ /* center to first line */
position: relative;
top: 0.15em; top: 0.15em;
background-color: $background;
&::before { &::before {
content: ""; content: "";

View file

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

View file

@ -70,4 +70,5 @@ limitations under the License.
display: grid; display: grid;
grid-gap: $spacing-16; grid-gap: $spacing-16;
margin-bottom: $spacing-8; margin-bottom: $spacing-8;
max-width: 550px;
} }

View file

@ -49,7 +49,7 @@ export type Binding = {
*/ */
export default class AddThreepid { export default class AddThreepid {
private sessionId: string; private sessionId: string;
private submitUrl: string; private submitUrl?: string;
private clientSecret: string; private clientSecret: string;
private bind: boolean; private bind: boolean;
@ -93,7 +93,7 @@ export default class AddThreepid {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
return MatrixClientPeg.get() return MatrixClientPeg.get()
.requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken) .requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken)
.then( .then(
@ -155,7 +155,7 @@ export default class AddThreepid {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
return MatrixClientPeg.get() return MatrixClientPeg.get()
.requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken) .requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken)
.then( .then(
@ -184,7 +184,7 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> { public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null] | undefined> {
try { try {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
if (this.bind) { if (this.bind) {
@ -282,7 +282,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();

View file

@ -546,7 +546,7 @@ export default class ContentMessages {
if (upload.cancelled) throw new UploadCanceledError(); if (upload.cancelled) throw new UploadCanceledError();
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const response = await matrixClient.sendMessage(roomId, threadId, content); const response = await matrixClient.sendMessage(roomId, threadId ?? null, content);
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
sendRoundTripMetric(matrixClient, roomId, response.event_id); sendRoundTripMetric(matrixClient, roomId, response.event_id);

View file

@ -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"),

View file

@ -287,7 +287,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
return MatrixClientPeg.get().store.deleteAllData(); return MatrixClientPeg.get().store.deleteAllData();
}) })
.then(() => { .then(() => {
PlatformPeg.get().reload(); PlatformPeg.get()?.reload();
}); });
} }
} }
@ -519,7 +519,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
stopMatrixClient(); stopMatrixClient();
const pickleKey = const pickleKey =
credentials.userId && credentials.deviceId credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) ? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
: null; : null;
if (pickleKey) { if (pickleKey) {

View file

@ -166,7 +166,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 +176,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;
@ -292,7 +292,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
} }
public getCredentials(): IMatrixClientCreds { public getCredentials(): IMatrixClientCreds {
let copiedCredentials = this.currentClientCreds; let copiedCredentials: IMatrixClientCreds | null = this.currentClientCreds;
if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) { if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) {
// cached credentials belong to a different user - don't use them // cached credentials belong to a different user - don't use them
copiedCredentials = null; copiedCredentials = null;

View file

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

View file

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

View file

@ -26,7 +26,6 @@ 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: {

View file

@ -198,7 +198,7 @@ function reject(error?: any): RunResult {
return { error }; return { error };
} }
function success(promise?: Promise<any>): RunResult { function success(promise: Promise<any> = Promise.resolve()): RunResult {
return { promise }; return { promise };
} }
@ -221,7 +221,7 @@ export const Commands = [
command: "spoiler", command: "spoiler",
args: "<message>", args: "<message>",
description: _td("Sends the given message as a spoiler"), description: _td("Sends the given message as a spoiler"),
runFn: function (roomId, message) { runFn: function (roomId, message = "") {
return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`)); return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -282,7 +282,7 @@ export const Commands = [
command: "plain", command: "plain",
args: "<message>", args: "<message>",
description: _td("Sends a message as plain text, without interpreting it as markdown"), description: _td("Sends a message as plain text, without interpreting it as markdown"),
runFn: function (roomId, messages) { runFn: function (roomId, messages = "") {
return successSync(ContentHelpers.makeTextMessage(messages)); return successSync(ContentHelpers.makeTextMessage(messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -291,7 +291,7 @@ export const Commands = [
command: "html", command: "html",
args: "<message>", args: "<message>",
description: _td("Sends a message as html, without interpreting it as markdown"), description: _td("Sends a message as html, without interpreting it as markdown"),
runFn: function (roomId, messages) { runFn: function (roomId, messages = "") {
return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,

View file

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

View file

@ -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(

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
@ -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,
}; };
@ -213,8 +210,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

View file

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

View file

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

View file

@ -226,8 +226,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 +294,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 +434,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();
} }
@ -1987,13 +1982,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);
@ -2037,7 +2026,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}
/> />
); );

View file

@ -114,8 +114,11 @@ 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";
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");
@ -483,6 +486,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 +507,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),
@ -1903,6 +1957,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 +1987,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 +2024,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 +2054,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 +2147,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
oobData={this.props.oobData} oobData={this.props.oobData}
canPreview={this.state.canPeek} canPreview={this.state.canPeek}
room={this.state.room} room={this.state.room}
roomId={this.state.roomId}
/> />
); );
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {

View file

@ -139,15 +139,15 @@ export default class ViewSource extends React.Component<IProps, IState> {
private canSendStateEvent(mxEvent: MatrixEvent): boolean { private canSendStateEvent(mxEvent: MatrixEvent): boolean {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(mxEvent.getRoomId()); const room = cli.getRoom(mxEvent.getRoomId());
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); return !!room?.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEditing = this.state.isEditing; const isEditing = this.state.isEditing;
const roomId = mxEvent.getRoomId(); const roomId = mxEvent.getRoomId()!;
const eventId = mxEvent.getId(); const eventId = mxEvent.getId()!;
const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
return ( return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}> <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>

View file

@ -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,
@ -511,13 +508,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
return errorText; return errorText;
} }
public renderLoginComponentForFlows(): JSX.Element { public renderLoginComponentForFlows(): ReactNode {
if (!this.state.flows) return null; if (!this.state.flows) return null;
// this is the ideal order we want to show the flows in // this is the ideal order we want to show the flows in
const order = ["m.login.password", "m.login.sso"]; const order = ["m.login.password", "m.login.sso"];
const flows = order.map((type) => this.state.flows.find((flow) => flow.type === type)).filter(Boolean); const flows = filterBoolean(order.map((type) => this.state.flows.find((flow) => flow.type === type)));
return ( return (
<React.Fragment> <React.Fragment>
{flows.map((flow) => { {flows.map((flow) => {

View file

@ -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);
} }
} }
@ -95,7 +95,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
} }
logger.info("Rendering to %s", divId); logger.info("Rendering to %s", divId);
this.captchaWidgetId = global.grecaptcha.render(divId, { this.captchaWidgetId = global.grecaptcha?.render(divId, {
sitekey: publicKey, sitekey: publicKey,
callback: this.props.onCaptchaResponse, callback: this.props.onCaptchaResponse,
}); });
@ -113,7 +113,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
this.renderRecaptcha(DIV_ID); this.renderRecaptcha(DIV_ID);
// clear error if re-rendered // clear error if re-rendered
this.setState({ this.setState({
errorText: null, errorText: undefined,
}); });
} catch (e) { } catch (e) {
this.setState({ this.setState({
@ -123,7 +123,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
} }
public render(): React.ReactNode { public render(): React.ReactNode {
let error = null; let error: JSX.Element | undefined;
if (this.state.errorText) { if (this.state.errorText) {
error = <div className="error">{this.state.errorText}</div>; error = <div className="error">{this.state.errorText}</div>;
} }

View file

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

View file

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

View file

@ -34,7 +34,7 @@ interface IProps {}
export default class Welcome extends React.PureComponent<IProps> { export default class Welcome extends React.PureComponent<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const pagesConfig = SdkConfig.getObject("embedded_pages"); const pagesConfig = SdkConfig.getObject("embedded_pages");
let pageUrl = null; let pageUrl!: string;
if (pagesConfig) { if (pagesConfig) {
pageUrl = pagesConfig.get("welcome_url"); pageUrl = pagesConfig.get("welcome_url");
} }

View file

@ -158,12 +158,12 @@ const BeaconViewDialog: React.FC<IProps> = ({ initialFocusedBeacon, roomId, matr
)} )}
{mapDisplayError && <MapError error={mapDisplayError.message as LocationShareError} isMinimised />} {mapDisplayError && <MapError error={mapDisplayError.message as LocationShareError} isMinimised />}
{!centerGeoUri && !mapDisplayError && ( {!centerGeoUri && !mapDisplayError && (
<MapFallback data-test-id="beacon-view-dialog-map-fallback" className="mx_BeaconViewDialog_map"> <MapFallback data-testid="beacon-view-dialog-map-fallback" className="mx_BeaconViewDialog_map">
<span className="mx_BeaconViewDialog_mapFallbackMessage">{_t("No live locations")}</span> <span className="mx_BeaconViewDialog_mapFallbackMessage">{_t("No live locations")}</span>
<AccessibleButton <AccessibleButton
kind="primary" kind="primary"
onClick={onFinished} onClick={onFinished}
data-test-id="beacon-view-dialog-fallback-close" data-testid="beacon-view-dialog-fallback-close"
> >
{_t("Close")} {_t("Close")}
</AccessibleButton> </AccessibleButton>
@ -179,7 +179,7 @@ const BeaconViewDialog: React.FC<IProps> = ({ initialFocusedBeacon, roomId, matr
<AccessibleButton <AccessibleButton
kind="primary" kind="primary"
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
data-test-id="beacon-view-dialog-open-sidebar" data-testid="beacon-view-dialog-open-sidebar"
className="mx_BeaconViewDialog_viewListButton" className="mx_BeaconViewDialog_viewListButton"
> >
<LiveLocationIcon height={12} /> <LiveLocationIcon height={12} />

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { useContext } from "react"; import React, { useContext } from "react";
import { MatrixCapabilities } from "matrix-widget-api"; import { MatrixCapabilities } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu"; import { ChevronFace } from "../../structures/ContextMenu";
@ -34,6 +35,8 @@ import { WidgetType } from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> { interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
app: IApp; app: IApp;
@ -45,7 +48,7 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
onEditClick?(): void; onEditClick?(): void;
} }
const WidgetContextMenu: React.FC<IProps> = ({ export const WidgetContextMenu: React.FC<IProps> = ({
onFinished, onFinished,
app, app,
userWidget, userWidget,
@ -158,24 +161,31 @@ const WidgetContextMenu: React.FC<IProps> = ({
const isLocalWidget = WidgetType.JITSI.matches(app.type); const isLocalWidget = WidgetType.JITSI.matches(app.type);
let revokeButton; let revokeButton;
if (!userWidget && !isLocalWidget && isAllowedWidget) { if (!userWidget && !isLocalWidget && isAllowedWidget) {
const onRevokeClick = (): void => { const opts: ApprovalOpts = { approved: undefined };
logger.info("Revoking permission for widget to load: " + app.eventId); ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (app.eventId !== undefined) current[app.eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch((err) => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
onFinished();
};
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />; if (!opts.approved) {
const onRevokeClick = (): void => {
logger.info("Revoking permission for widget to load: " + app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (app.eventId !== undefined) current[app.eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
if (!level) throw new Error("level must be defined");
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
onFinished();
};
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
}
} }
let moveLeftButton; let moveLeftButton;
if (showUnpin && widgetIndex > 0) { if (showUnpin && widgetIndex > 0) {
const onClick = (): void => { const onClick = (): void => {
if (!room) throw new Error("room must be defined");
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1); WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
onFinished(); onFinished();
}; };
@ -207,5 +217,3 @@ const WidgetContextMenu: React.FC<IProps> = ({
</IconizedContextMenu> </IconizedContextMenu>
); );
}; };
export default WidgetContextMenu;

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
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,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { useCallback } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
interface IProps { export interface AskInviteAnywayDialogProps {
unknownProfileUsers: Array<{ unknownProfileUsers: Array<{
userId: string; userId: string;
errorText: string; errorText: string;
@ -31,57 +32,58 @@ interface IProps {
onFinished: (success: boolean) => void; onFinished: (success: boolean) => void;
} }
export default class AskInviteAnywayDialog extends React.Component<IProps> { export default function AskInviteAnywayDialog({
private onInviteClicked = (): void => { onFinished,
this.props.onInviteAnyways(); onGiveUp,
this.props.onFinished(true); onInviteAnyways,
}; unknownProfileUsers,
}: AskInviteAnywayDialogProps): JSX.Element {
const onInviteClicked = useCallback((): void => {
onInviteAnyways();
onFinished(true);
}, [onInviteAnyways, onFinished]);
private onInviteNeverWarnClicked = (): void => { const onInviteNeverWarnClicked = useCallback((): void => {
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
this.props.onInviteAnyways(); onInviteAnyways();
this.props.onFinished(true); onFinished(true);
}; }, [onInviteAnyways, onFinished]);
private onGiveUpClicked = (): void => { const onGiveUpClicked = useCallback((): void => {
this.props.onGiveUp(); onGiveUp();
this.props.onFinished(false); onFinished(false);
}; }, [onGiveUp, onFinished]);
public render(): React.ReactNode { const errorList = unknownProfileUsers.map((address) => (
const errorList = this.props.unknownProfileUsers.map((address) => ( <li key={address.userId}>
<li key={address.userId}> {address.userId}: {address.errorText}
{address.userId}: {address.errorText} </li>
</li> ));
));
return ( return (
<BaseDialog <BaseDialog
className="mx_RetryInvitesDialog" className="mx_RetryInvitesDialog"
onFinished={this.onGiveUpClicked} onFinished={onGiveUpClicked}
title={_t("The following users may not exist")} title={_t("The following users may not exist")}
contentId="mx_Dialog_content" contentId="mx_Dialog_content"
> >
<div id="mx_Dialog_content"> <div id="mx_Dialog_content">
<p> <p>
{_t( {_t(
"Unable to find profiles for the Matrix IDs listed below - " + "Unable to find profiles for the Matrix IDs listed below - " +
"would you like to invite them anyway?", "would you like to invite them anyway?",
)} )}
</p> </p>
<ul>{errorList}</ul> <ul>{errorList}</ul>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onGiveUpClicked}>{_t("Close")}</button> <button onClick={onGiveUpClicked}>{_t("Close")}</button>
<button onClick={this.onInviteNeverWarnClicked}> <button onClick={onInviteNeverWarnClicked}>{_t("Invite anyway and never warn me again")}</button>
{_t("Invite anyway and never warn me again")} <button onClick={onInviteClicked} autoFocus={true}>
</button> {_t("Invite anyway")}
<button onClick={this.onInviteClicked} autoFocus={true}> </button>
{_t("Invite anyway")} </div>
</button> </BaseDialog>
</div> );
</BaseDialog>
);
}
} }

View file

@ -36,7 +36,7 @@ interface IProps {
interface IState { interface IState {
shouldLoadBackupStatus: boolean; shouldLoadBackupStatus: boolean;
loading: boolean; loading: boolean;
backupInfo: IKeyBackupInfo; backupInfo: IKeyBackupInfo | null;
error?: string; error?: string;
} }
@ -55,7 +55,6 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
shouldLoadBackupStatus: shouldLoadBackupStatus, shouldLoadBackupStatus: shouldLoadBackupStatus,
loading: shouldLoadBackupStatus, loading: shouldLoadBackupStatus,
backupInfo: null, backupInfo: null,
error: null,
}; };
if (shouldLoadBackupStatus) { if (shouldLoadBackupStatus) {
@ -103,14 +102,20 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
// A key backup exists for this account, but the creating device is not // A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and // verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it) // allow us to trust it (ie. upload keys to it)
Modal.createDialog(RestoreKeyBackupDialog, null, null, /* priority = */ false, /* static = */ true); Modal.createDialog(
RestoreKeyBackupDialog,
undefined,
undefined,
/* priority = */ false,
/* static = */ true,
);
} else { } else {
Modal.createDialogAsync( Modal.createDialogAsync(
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise< import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
ComponentType<{}> ComponentType<{}>
>, >,
null, undefined,
null, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ true, /* static = */ true,
); );

View file

@ -20,7 +20,7 @@ import React, { useContext } from "react";
import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { useNotificationState } from "../../../../hooks/useRoomNotificationState"; import { useNotificationState } from "../../../../hooks/useRoomNotificationState";
import { _t } from "../../../../languageHandler"; import { _t, _td } from "../../../../languageHandler";
import { determineUnreadState } from "../../../../RoomNotifs"; import { determineUnreadState } from "../../../../RoomNotifs";
import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor"; import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor";
import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread";
@ -39,22 +39,38 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme
<h2>{_t("Room status")}</h2> <h2>{_t("Room status")}</h2>
<ul> <ul>
<li> <li>
{_t("Room unread status: ")} {_t(
<strong>{humanReadableNotificationColor(color)}</strong> "Room unread status: <strong>%(status)s</strong>, count: <strong>%(count)s</strong>",
{count > 0 && ( {
<> status: humanReadableNotificationColor(color),
{_t(", count:")} <strong>{count}</strong> count,
</> },
{
strong: (sub) => <strong>{sub}</strong>,
},
)} )}
</li> </li>
<li> <li>
{_t("Notification state is")} <strong>{notificationState}</strong> {_t(
"Notification state is <strong>%(notificationState)s</strong>",
{
notificationState,
},
{
strong: (sub) => <strong>{sub}</strong>,
},
)}
</li> </li>
<li> <li>
{_t("Room is ")} {_t(
<strong> cli.isRoomEncrypted(room.roomId!)
{cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")} ? _td("Room is <strong>encrypted ✅</strong>")
</strong> : _td("Room is <strong>not encrypted 🚨</strong>"),
{},
{
strong: (sub) => <strong>{sub}</strong>,
},
)}
</li> </li>
</ul> </ul>
</section> </section>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
import { IDialogProps } from "../IDialogProps"; import { IDialogProps } from "../IDialogProps";
import { PollHistoryList } from "./PollHistoryList"; import { PollHistoryList } from "./PollHistoryList";
import { PollHistoryFilter } from "./types"; import { PollHistoryFilter } from "./types";
import { usePolls } from "./usePollHistory"; import { usePollsWithRelations } from "./usePollHistory";
import { useFetchPastPolls } from "./fetchPastPolls";
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & { type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
roomId: string; roomId: string;
@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
const filterPolls = const filterPolls =
(filter: PollHistoryFilter) => (filter: PollHistoryFilter) =>
(poll: Poll): boolean => (poll: Poll): boolean =>
(filter === "ACTIVE") !== poll.isEnded; // exclude polls while they are still loading
// to avoid jitter in list
!poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded;
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => { const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
return [...polls.values()] return [...polls.values()]
.filter(filterPolls(filter)) .filter(filterPolls(filter))
@ -43,18 +47,24 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
}; };
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => { export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
const { polls } = usePolls(roomId, matrixClient); const room = matrixClient.getRoom(roomId)!;
const { isLoading } = useFetchPastPolls(room, matrixClient);
const { polls } = usePollsWithRelations(roomId, matrixClient);
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE"); const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
useEffect(() => { const pollStartEvents = filterAndSortPolls(polls, filter);
setPollStartEvents(filterAndSortPolls(polls, filter)); const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
}, [filter, polls]);
return ( return (
<BaseDialog title={_t("Polls history")} onFinished={onFinished}> <BaseDialog title={_t("Polls history")} onFinished={onFinished}>
<div className="mx_PollHistoryDialog_content"> <div className="mx_PollHistoryDialog_content">
<PollHistoryList pollStartEvents={pollStartEvents} filter={filter} onFilterChange={setFilter} /> <PollHistoryList
pollStartEvents={pollStartEvents}
isLoading={isLoading || isLoadingPollResponses}
polls={polls}
filter={filter}
onFilterChange={setFilter}
/>
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -15,19 +15,41 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import classNames from "classnames";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
import PollListItem from "./PollListItem";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { FilterTabGroup } from "../../elements/FilterTabGroup"; import { FilterTabGroup } from "../../elements/FilterTabGroup";
import InlineSpinner from "../../elements/InlineSpinner";
import { PollHistoryFilter } from "./types"; import { PollHistoryFilter } from "./types";
import { PollListItem } from "./PollListItem";
import { PollListItemEnded } from "./PollListItemEnded";
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
<div
className={classNames("mx_PollHistoryList_loading", {
mx_PollHistoryList_noResultsYet: noResultsYet,
})}
>
<InlineSpinner />
{_t("Loading polls")}
</div>
);
type PollHistoryListProps = { type PollHistoryListProps = {
pollStartEvents: MatrixEvent[]; pollStartEvents: MatrixEvent[];
polls: Map<string, Poll>;
filter: PollHistoryFilter; filter: PollHistoryFilter;
onFilterChange: (filter: PollHistoryFilter) => void; onFilterChange: (filter: PollHistoryFilter) => void;
isLoading?: boolean;
}; };
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, filter, onFilterChange }) => { export const PollHistoryList: React.FC<PollHistoryListProps> = ({
pollStartEvents,
polls,
filter,
isLoading,
onFilterChange,
}) => {
return ( return (
<div className="mx_PollHistoryList"> <div className="mx_PollHistoryList">
<FilterTabGroup<PollHistoryFilter> <FilterTabGroup<PollHistoryFilter>
@ -39,19 +61,30 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
{ id: "ENDED", label: "Past polls" }, { id: "ENDED", label: "Past polls" },
]} ]}
/> />
{!!pollStartEvents.length ? ( {!!pollStartEvents.length && (
<ol className="mx_PollHistoryList_list"> <ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
{pollStartEvents.map((pollStartEvent) => ( {pollStartEvents.map((pollStartEvent) =>
<PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} /> filter === "ACTIVE" ? (
))} <PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} />
) : (
<PollListItemEnded
key={pollStartEvent.getId()!}
event={pollStartEvent}
poll={polls.get(pollStartEvent.getId()!)!}
/>
),
)}
{isLoading && <LoadingPolls />}
</ol> </ol>
) : ( )}
{!pollStartEvents.length && !isLoading && (
<span className="mx_PollHistoryList_noResults"> <span className="mx_PollHistoryList_noResults">
{filter === "ACTIVE" {filter === "ACTIVE"
? _t("There are no active polls in this room") ? _t("There are no active polls in this room")
: _t("There are no past polls in this room")} : _t("There are no past polls in this room")}
</span> </span>
)} )}
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
</div> </div>
); );
}; };

View file

@ -25,7 +25,7 @@ interface Props {
event: MatrixEvent; event: MatrixEvent;
} }
const PollListItem: React.FC<Props> = ({ event }) => { export const PollListItem: React.FC<Props> = ({ event }) => {
const pollEvent = event.unstableExtensibleEvent as unknown as PollStartEvent; const pollEvent = event.unstableExtensibleEvent as unknown as PollStartEvent;
if (!pollEvent) { if (!pollEvent) {
return null; return null;
@ -39,5 +39,3 @@ const PollListItem: React.FC<Props> = ({ event }) => {
</li> </li>
); );
}; };
export default PollListItem;

View file

@ -0,0 +1,127 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useState } from "react";
import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { MatrixEvent, Poll, PollEvent } from "matrix-js-sdk/src/matrix";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { Icon as PollIcon } from "../../../../../res/img/element-icons/room/composer/poll.svg";
import { _t } from "../../../../languageHandler";
import { formatLocalDateShort } from "../../../../DateUtils";
import { allVotes, collectUserVotes, countVotes } from "../../messages/MPollBody";
import { PollOption } from "../../polls/PollOption";
import { Caption } from "../../typography/Caption";
interface Props {
event: MatrixEvent;
poll: Poll;
}
type EndedPollState = {
winningAnswers: {
answer: PollAnswerSubevent;
voteCount: number;
}[];
totalVoteCount: number;
};
const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollState => {
const userVotes = collectUserVotes(allVotes(responseRelations));
const votes = countVotes(userVotes, poll.pollEvent);
const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote, 0);
const winCount = Math.max(...votes.values());
return {
totalVoteCount,
winningAnswers: poll.pollEvent.answers
.filter((answer) => votes.get(answer.id) === winCount)
.map((answer) => ({
answer,
voteCount: votes.get(answer.id) || 0,
})),
};
};
/**
* Get deduplicated and validated poll responses
* Will use cached responses from Poll instance when existing
* Updates on changes to Poll responses (paging relations or from sync)
* Returns winning answers and total vote count
*/
const usePollVotes = (poll: Poll): Partial<EndedPollState> => {
const [results, setResults] = useState({ totalVoteCount: 0 });
useEffect(() => {
const getResponses = async (): Promise<void> => {
const responseRelations = await poll.getResponses();
setResults(getWinningAnswers(poll, responseRelations));
};
const onPollResponses = (responseRelations: Relations): void =>
setResults(getWinningAnswers(poll, responseRelations));
poll.on(PollEvent.Responses, onPollResponses);
getResponses();
return () => {
poll.off(PollEvent.Responses, onPollResponses);
};
}, [poll]);
return results;
};
/**
* Render an ended poll with the winning answer and vote count
* @param event - the poll start MatrixEvent
* @param poll - Poll instance
*/
export const PollListItemEnded: React.FC<Props> = ({ event, poll }) => {
const pollEvent = poll.pollEvent;
const { winningAnswers, totalVoteCount } = usePollVotes(poll);
if (!pollEvent) {
return null;
}
const formattedDate = formatLocalDateShort(event.getTs());
return (
<li data-testid={`pollListItem-${event.getId()!}`} className="mx_PollListItemEnded">
<div className="mx_PollListItemEnded_title">
<PollIcon className="mx_PollListItemEnded_icon" />
<span className="mx_PollListItemEnded_question">{pollEvent.question.text}</span>
<Caption>{formattedDate}</Caption>
</div>
{!!winningAnswers?.length && (
<div className="mx_PollListItemEnded_answers">
{winningAnswers?.map(({ answer, voteCount }) => (
<PollOption
key={answer.id}
answer={answer}
voteCount={voteCount}
totalVoteCount={totalVoteCount!}
pollId={poll.pollId}
displayVoteCount
isChecked
isEnded
/>
))}
</div>
)}
<div className="mx_PollListItemEnded_voteCount">
<Caption>{_t("Final result based on %(count)s votes", { count: totalVoteCount })}</Caption>
</div>
</li>
);
};

View file

@ -0,0 +1,129 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useState } from "react";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix";
import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter";
import { logger } from "matrix-js-sdk/src/logger";
/**
* Page timeline backwards until either:
* - event older than endOfHistoryPeriodTimestamp is encountered
* - end of timeline is reached
* @param timelineSet - timelineset to page
* @param matrixClient - client
* @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until
* @returns void
*/
const pagePolls = async (
timelineSet: EventTimelineSet,
matrixClient: MatrixClient,
endOfHistoryPeriodTimestamp: number,
): Promise<void> => {
const liveTimeline = timelineSet.getLiveTimeline();
const events = liveTimeline.getEvents();
const oldestEventTimestamp = events[0]?.getTs() || Date.now();
const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS);
if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) {
return;
}
await matrixClient.paginateEventTimeline(liveTimeline, {
backwards: true,
});
return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
};
const ONE_DAY_MS = 60000 * 60 * 24;
/**
* Fetches timeline history for given number of days in past
* @param timelineSet - timelineset to page
* @param matrixClient - client
* @param historyPeriodDays - number of days of history to fetch, from current day
* @returns isLoading - true while fetching history
*/
const useTimelineHistory = (
timelineSet: EventTimelineSet | null,
matrixClient: MatrixClient,
historyPeriodDays: number,
): { isLoading: boolean } => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!timelineSet) {
return;
}
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
const doFetchHistory = async (): Promise<void> => {
setIsLoading(true);
try {
await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
} catch (error) {
logger.error("Failed to fetch room polls history", error);
} finally {
setIsLoading(false);
}
};
doFetchHistory();
}, [timelineSet, historyPeriodDays, matrixClient]);
return { isLoading };
};
const filterDefinition: IFilterDefinition = {
room: {
timeline: {
types: [M_POLL_START.name, M_POLL_START.altName],
},
},
};
/**
* Fetch poll start events in the last N days of room history
* @param room - room to fetch history for
* @param matrixClient - client
* @param historyPeriodDays - number of days of history to fetch, from current day
* @returns isLoading - true while fetching history
*/
export const useFetchPastPolls = (
room: Room,
matrixClient: MatrixClient,
historyPeriodDays = 30,
): { isLoading: boolean } => {
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
useEffect(() => {
const filter = new Filter(matrixClient.getSafeUserId());
filter.setDefinition(filterDefinition);
const getFilteredTimelineSet = async (): Promise<void> => {
const filterId = await matrixClient.getOrCreateFilter(`POLL_HISTORY_FILTER_${room.roomId}}`, filter);
filter.filterId = filterId;
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
setTimelineSet(timelineSet);
};
getFilteredTimelineSet();
}, [room, matrixClient]);
const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays);
return { isLoading };
};

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useEffect, useState } from "react";
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix"; import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
/** /**
* Get poll instances from a room * Get poll instances from a room
* Updates to include new polls
* @param roomId - id of room to retrieve polls for * @param roomId - id of room to retrieve polls for
* @param matrixClient - client * @param matrixClient - client
* @returns {Map<string, Poll>} - Map of Poll instances * @returns {Map<string, Poll>} - Map of Poll instances
@ -37,9 +39,58 @@ export const usePolls = (
throw new Error("Cannot find room"); throw new Error("Cannot find room");
} }
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls); // copy room.polls map so changes can be detected
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.polls));
// @TODO(kerrya) watch polls for end events, trigger refiltering
return { polls }; return { polls };
}; };
/**
* Get all poll instances from a room
* Fetch their responses (using cached poll responses)
* Updates on:
* - new polls added to room
* - new responses added to polls
* - changes to poll ended state
* @param roomId - id of room to retrieve polls for
* @param matrixClient - client
* @returns {Map<string, Poll>} - Map of Poll instances
*/
export const usePollsWithRelations = (
roomId: string,
matrixClient: MatrixClient,
): {
polls: Map<string, Poll>;
} => {
const { polls } = usePolls(roomId, matrixClient);
const [pollsWithRelations, setPollsWithRelations] = useState<Map<string, Poll>>(polls);
useEffect(() => {
const onPollUpdate = async (): Promise<void> => {
// trigger rerender by creating a new poll map
setPollsWithRelations(new Map(polls));
};
if (polls) {
for (const poll of polls.values()) {
// listen to changes in responses and end state
poll.on(PollEvent.End, onPollUpdate);
poll.on(PollEvent.Responses, onPollUpdate);
// trigger request to get all responses
// if they are not already in cache
poll.getResponses();
}
setPollsWithRelations(polls);
}
// unsubscribe
return () => {
if (polls) {
for (const poll of polls.values()) {
poll.off(PollEvent.End, onPollUpdate);
poll.off(PollEvent.Responses, onPollUpdate);
}
}
};
}, [polls, setPollsWithRelations]);
return { polls: pollsWithRelations };
};

View file

@ -93,6 +93,7 @@ import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
import { shouldShowFeedback } from "../../../../utils/Feedback"; import { shouldShowFeedback } from "../../../../utils/Feedback";
import RoomAvatar from "../../avatars/RoomAvatar"; import RoomAvatar from "../../avatars/RoomAvatar";
import { useFeatureEnabled } from "../../../../hooks/useSettings"; import { useFeatureEnabled } from "../../../../hooks/useSettings";
import { filterBoolean } from "../../../../utils/arrays";
const MAX_RECENT_SEARCHES = 10; const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@ -173,13 +174,13 @@ const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResul
publicRoom, publicRoom,
section: Section.PublicRooms, section: Section.PublicRooms,
filter: [Filter.PublicRooms], filter: [Filter.PublicRooms],
query: [ query: filterBoolean([
publicRoom.room_id.toLowerCase(), publicRoom.room_id.toLowerCase(),
publicRoom.canonical_alias?.toLowerCase(), publicRoom.canonical_alias?.toLowerCase(),
publicRoom.name?.toLowerCase(), publicRoom.name?.toLowerCase(),
sanitizeHtml(publicRoom.topic?.toLowerCase() ?? "", { allowedTags: [] }), sanitizeHtml(publicRoom.topic?.toLowerCase() ?? "", { allowedTags: [] }),
...(publicRoom.aliases?.map((it) => it.toLowerCase()) || []), ...(publicRoom.aliases?.map((it) => it.toLowerCase()) || []),
].filter(Boolean) as string[], ]),
}); });
const toRoomResult = (room: Room): IRoomResult => { const toRoomResult = (room: Room): IRoomResult => {

View file

@ -23,6 +23,7 @@ import classNames from "classnames";
import { MatrixCapabilities } from "matrix-widget-api"; import { MatrixCapabilities } from "matrix-widget-api";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -36,7 +37,7 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
import PersistedElement, { getPersistKey } from "./PersistedElement"; import PersistedElement, { getPersistKey } from "./PersistedElement";
import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetType } from "../../../widgets/WidgetType";
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetAvatar from "../avatars/WidgetAvatar";
import LegacyCallHandler from "../../../LegacyCallHandler"; import LegacyCallHandler from "../../../LegacyCallHandler";
import { IApp } from "../../../stores/WidgetStore"; import { IApp } from "../../../stores/WidgetStore";
@ -50,6 +51,7 @@ import { Action } from "../../../dispatcher/actions";
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities"; import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import { SdkContextClass } from "../../../contexts/SDKContext"; import { SdkContextClass } from "../../../contexts/SDKContext";
import { ModuleRunner } from "../../../modules/ModuleRunner";
interface IProps { interface IProps {
app: IApp; app: IApp;
@ -162,6 +164,9 @@ export default class AppTile extends React.Component<IProps, IState> {
private hasPermissionToLoad = (props: IProps): boolean => { private hasPermissionToLoad = (props: IProps): boolean => {
if (this.usingLocalWidget()) return true; if (this.usingLocalWidget()) return true;
if (!props.room) return true; // user widgets always have permissions if (!props.room) return true; // user widgets always have permissions
const opts: ApprovalOpts = { approved: undefined };
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(this.props.app));
if (opts.approved) return true;
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false); const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);

View file

@ -32,7 +32,7 @@ interface IProps {
/* callback to update the value. Called with a single argument: the new /* callback to update the value. Called with a single argument: the new
* value. */ * value. */
onSubmit?: (value: string) => Promise<{} | void>; onSubmit: (value: string) => Promise<{} | void>;
/* should the input submit when focus is lost? */ /* should the input submit when focus is lost? */
blurToSubmit?: boolean; blurToSubmit?: boolean;
@ -40,7 +40,7 @@ interface IProps {
interface IState { interface IState {
busy: boolean; busy: boolean;
errorString: string; errorString: string | null;
value: string; value: string;
} }
@ -72,7 +72,7 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
this.state = { this.state = {
busy: false, busy: false,
errorString: null, errorString: null,
value: props.initialValue, value: props.initialValue ?? "",
}; };
} }

View file

@ -113,7 +113,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
brandClass = `mx_SSOButton_brand_${brandName}`; brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />; icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) { } else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24); const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24) ?? undefined;
icon = <img src={src} height="24" width="24" alt={idp.name} />; icon = <img src={src} height="24" width="24" alt={idp.name} />;
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import EventIndexPeg from "../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../indexing/EventIndexPeg";
@ -31,13 +31,13 @@ export enum WarningKind {
} }
interface IProps { interface IProps {
isRoomEncrypted: boolean; isRoomEncrypted?: boolean;
kind: WarningKind; kind: WarningKind;
} }
export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element { export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element {
if (!isRoomEncrypted) return null; if (!isRoomEncrypted) return <></>;
if (EventIndexPeg.get()) return null; if (EventIndexPeg.get()) return <></>;
if (EventIndexPeg.error) { if (EventIndexPeg.error) {
return ( return (
@ -69,8 +69,8 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
const brand = SdkConfig.get("brand"); const brand = SdkConfig.get("brand");
const desktopBuilds = SdkConfig.getObject("desktop_builds"); const desktopBuilds = SdkConfig.getObject("desktop_builds");
let text = null; let text: ReactNode | undefined;
let logo = null; let logo: JSX.Element | undefined;
if (desktopBuilds.get("available")) { if (desktopBuilds.get("available")) {
logo = <img src={desktopBuilds.get("logo")} />; logo = <img src={desktopBuilds.get("logo")} />;
const buildUrl = desktopBuilds.get("url"); const buildUrl = desktopBuilds.get("url");
@ -116,7 +116,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
// for safety // for safety
if (!text) { if (!text) {
logger.warn("Unknown desktop builds warning kind: ", kind); logger.warn("Unknown desktop builds warning kind: ", kind);
return null; return <></>;
} }
return ( return (

View file

@ -26,7 +26,7 @@ interface IResult {
text: string; text: string;
} }
interface IRule<T, D = void> { interface IRule<T, D = undefined> {
key: string; key: string;
final?: boolean; final?: boolean;
skip?(this: T, data: Data, derivedData: D): boolean; skip?(this: T, data: Data, derivedData: D): boolean;
@ -90,14 +90,12 @@ export default function withValidation<T = void, D = void>({
{ value, focused, allowEmpty = true }: IFieldState, { value, focused, allowEmpty = true }: IFieldState,
): Promise<IValidationResult> { ): Promise<IValidationResult> {
if (!value && allowEmpty) { if (!value && allowEmpty) {
return { return {};
valid: null,
feedback: null,
};
} }
const data = { value, allowEmpty }; const data = { value, allowEmpty };
const derivedData: D | undefined = deriveData ? await deriveData.call(this, data) : undefined; // We know that if deriveData is set then D will not be undefined
const derivedData: D = (await deriveData?.call(this, data)) as D;
const results: IResult[] = []; const results: IResult[] = [];
let valid = true; let valid = true;
@ -149,10 +147,7 @@ export default function withValidation<T = void, D = void>({
// Hide feedback when not focused // Hide feedback when not focused
if (!focused) { if (!focused) {
return { return { valid };
valid,
feedback: null,
};
} }
let details; let details;

View file

@ -38,7 +38,7 @@ interface IProps {
id: string; id: string;
name: string; name: string;
emojis: IEmoji[]; emojis: IEmoji[];
selectedEmojis: Set<string>; selectedEmojis?: Set<string>;
heightBefore: number; heightBefore: number;
viewportHeight: number; viewportHeight: number;
scrollTop: number; scrollTop: number;

View file

@ -26,6 +26,7 @@ import Search from "./Search";
import Preview from "./Preview"; import Preview from "./Preview";
import QuickReactions from "./QuickReactions"; import QuickReactions from "./QuickReactions";
import Category, { ICategory, CategoryKey } from "./Category"; import Category, { ICategory, CategoryKey } from "./Category";
import { filterBoolean } from "../../../utils/arrays";
export const CATEGORY_HEADER_HEIGHT = 20; export const CATEGORY_HEADER_HEIGHT = 20;
export const EMOJI_HEIGHT = 35; export const EMOJI_HEIGHT = 35;
@ -62,13 +63,12 @@ class EmojiPicker extends React.Component<IProps, IState> {
this.state = { this.state = {
filter: "", filter: "",
previewEmoji: null,
scrollTop: 0, scrollTop: 0,
viewportHeight: 280, viewportHeight: 280,
}; };
// Convert recent emoji characters to emoji data, removing unknowns and duplicates // Convert recent emoji characters to emoji data, removing unknowns and duplicates
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode))));
this.memoizedDataByCategory = { this.memoizedDataByCategory = {
recent: this.recentlyUsed, recent: this.recentlyUsed,
...DATA_BY_CATEGORY, ...DATA_BY_CATEGORY,
@ -230,9 +230,9 @@ class EmojiPicker extends React.Component<IProps, IState> {
}); });
}; };
private onHoverEmojiEnd = (emoji: IEmoji): void => { private onHoverEmojiEnd = (): void => {
this.setState({ this.setState({
previewEmoji: null, previewEmoji: undefined,
}); });
}; };

View file

@ -42,9 +42,7 @@ interface IState {
class QuickReactions extends React.Component<IProps, IState> { class QuickReactions extends React.Component<IProps, IState> {
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {};
hover: null,
};
} }
private onMouseEnter = (emoji: IEmoji): void => { private onMouseEnter = (emoji: IEmoji): void => {
@ -55,7 +53,7 @@ class QuickReactions extends React.Component<IProps, IState> {
private onMouseLeave = (): void => { private onMouseLeave = (): void => {
this.setState({ this.setState({
hover: null, hover: undefined,
}); });
}; };

View file

@ -77,8 +77,8 @@ class ReactionPicker extends React.Component<IProps, IState> {
if (!this.props.reactions) { if (!this.props.reactions) {
return {}; return {};
} }
const userId = MatrixClientPeg.get().getUserId(); const userId = MatrixClientPeg.get().getUserId()!;
const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || new Set<MatrixEvent>(); const myAnnotations = this.props.reactions.getAnnotationsBySender()?.[userId] ?? new Set<MatrixEvent>();
return Object.fromEntries( return Object.fromEntries(
[...myAnnotations] [...myAnnotations]
.filter((event) => !event.isRedacted()) .filter((event) => !event.isRedacted())
@ -97,9 +97,9 @@ class ReactionPicker extends React.Component<IProps, IState> {
this.props.onFinished(); this.props.onFinished();
const myReactions = this.getReactions(); const myReactions = this.getReactions();
if (myReactions.hasOwnProperty(reaction)) { if (myReactions.hasOwnProperty(reaction)) {
if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return; if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return false;
MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId(), myReactions[reaction]); MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]);
dis.dispatch<FocusComposerPayload>({ dis.dispatch<FocusComposerPayload>({
action: Action.FocusAComposer, action: Action.FocusAComposer,
context: this.context.timelineRenderingType, context: this.context.timelineRenderingType,
@ -107,7 +107,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
// Tell the emoji picker not to bump this in the more frequently used list. // Tell the emoji picker not to bump this in the more frequently used list.
return false; return false;
} else { } else {
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), EventType.Reaction, { MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, {
"m.relates_to": { "m.relates_to": {
rel_type: RelationType.Annotation, rel_type: RelationType.Annotation,
event_id: this.props.mxEvent.getId(), event_id: this.props.mxEvent.getId(),

View file

@ -32,7 +32,7 @@ class Search extends React.PureComponent<IProps> {
public componentDidMount(): void { public componentDidMount(): void {
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout
window.setTimeout(() => this.inputRef.current.focus(), 0); window.setTimeout(() => this.inputRef.current?.focus(), 0);
} }
private onKeyDown = (ev: React.KeyboardEvent): void => { private onKeyDown = (ev: React.KeyboardEvent): void => {

View file

@ -29,7 +29,7 @@ interface Props {
export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => { export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
const [isEnabled, setEnabled] = useState(false); const [isEnabled, setEnabled] = useState(false);
return ( return (
<div data-test-id="location-picker-enable-live-share" className="mx_EnableLiveShare"> <div data-testid="location-picker-enable-live-share" className="mx_EnableLiveShare">
<StyledLiveBeaconIcon className="mx_EnableLiveShare_icon" /> <StyledLiveBeaconIcon className="mx_EnableLiveShare_icon" />
<Heading className="mx_EnableLiveShare_heading" size="h3"> <Heading className="mx_EnableLiveShare_heading" size="h3">
{_t("Live location sharing")} {_t("Live location sharing")}
@ -43,13 +43,13 @@ export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
)} )}
</p> </p>
<LabelledToggleSwitch <LabelledToggleSwitch
data-test-id="enable-live-share-toggle" data-testid="enable-live-share-toggle"
value={isEnabled} value={isEnabled}
onChange={setEnabled} onChange={setEnabled}
label={_t("Enable live location sharing")} label={_t("Enable live location sharing")}
/> />
<AccessibleButton <AccessibleButton
data-test-id="enable-live-share-submit" data-testid="enable-live-share-submit"
className="mx_EnableLiveShare_button" className="mx_EnableLiveShare_button"
element="button" element="button"
kind="primary" kind="primary"

View file

@ -62,7 +62,7 @@ const LiveDurationDropdown: React.FC<Props> = ({ timeout, onChange }) => {
return ( return (
<Dropdown <Dropdown
id="live-duration" id="live-duration"
data-test-id="live-duration-dropdown" data-testid="live-duration-dropdown"
label={getLabel(timeout)} label={getLabel(timeout)}
value={timeout.toString()} value={timeout.toString()}
onOptionChange={onOptionChange} onOptionChange={onOptionChange}

View file

@ -231,7 +231,7 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
<LiveDurationDropdown onChange={this.onTimeoutChange} timeout={this.state.timeout} /> <LiveDurationDropdown onChange={this.onTimeoutChange} timeout={this.state.timeout} />
)} )}
<AccessibleButton <AccessibleButton
data-test-id="location-picker-submit-button" data-testid="location-picker-submit-button"
type="submit" type="submit"
element="button" element="button"
kind="primary" kind="primary"

View file

@ -33,7 +33,7 @@ export interface MapErrorProps {
export const MapError: React.FC<MapErrorProps> = ({ error, isMinimised, className, onFinished, onClick }) => ( export const MapError: React.FC<MapErrorProps> = ({ error, isMinimised, className, onFinished, onClick }) => (
<div <div
data-test-id="map-rendering-error" data-testid="map-rendering-error"
className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })} className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })}
onClick={onClick} onClick={onClick}
> >

View file

@ -19,6 +19,7 @@ import React from "react";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { Icon as BackIcon } from "../../../../res/img/element-icons/caret-left.svg"; import { Icon as BackIcon } from "../../../../res/img/element-icons/caret-left.svg";
import { Icon as CloseIcon } from "../../../../res/img/element-icons/cancel-rounded.svg"; import { Icon as CloseIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
import { _t } from "../../../languageHandler";
interface Props { interface Props {
onCancel: () => void; onCancel: () => void;
@ -32,7 +33,8 @@ const ShareDialogButtons: React.FC<Props> = ({ onBack, onCancel, displayBack })
{displayBack && ( {displayBack && (
<AccessibleButton <AccessibleButton
className="mx_ShareDialogButtons_button left" className="mx_ShareDialogButtons_button left"
data-test-id="share-dialog-buttons-back" data-testid="share-dialog-buttons-back"
aria-label={_t("Back")}
onClick={onBack} onClick={onBack}
element="button" element="button"
> >
@ -41,7 +43,8 @@ const ShareDialogButtons: React.FC<Props> = ({ onBack, onCancel, displayBack })
)} )}
<AccessibleButton <AccessibleButton
className="mx_ShareDialogButtons_button right" className="mx_ShareDialogButtons_button right"
data-test-id="share-dialog-buttons-cancel" data-testid="share-dialog-buttons-cancel"
aria-label={_t("Close")}
onClick={onCancel} onClick={onCancel}
element="button" element="button"
> >

View file

@ -73,7 +73,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.settingWatcherRef); if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef);
} }
private onContextMenuOpenClick = (e: React.MouseEvent): void => { private onContextMenuOpenClick = (e: React.MouseEvent): void => {
@ -89,7 +89,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
private closeMenu = (): void => { private closeMenu = (): void => {
this.setState({ this.setState({
contextMenuPosition: null, contextMenuPosition: undefined,
}); });
}; };
@ -181,7 +181,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
}; };
private renderJumpToDateMenu(): React.ReactElement { private renderJumpToDateMenu(): React.ReactElement {
let contextMenu: JSX.Element; let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuPosition) { if (this.state.contextMenuPosition) {
contextMenu = ( contextMenu = (
<IconizedContextMenu <IconizedContextMenu

View file

@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactNode } from "react"; import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
// A placeholder element for messages that could not be decrypted function getErrorMessage(mxEvent?: MatrixEvent): string {
export default class DecryptionFailureBody extends React.Component<Partial<IBodyProps>> { return mxEvent?.isEncryptedDisabledForUnverifiedDevices
public render(): ReactNode { ? _t("The sender has blocked you from receiving this message")
return <div className="mx_DecryptionFailureBody mx_EventTile_content">{_t("Unable to decrypt message")}</div>; : _t("Unable to decrypt message");
} }
// A placeholder element for messages that could not be decrypted
export function DecryptionFailureBody({ mxEvent }: Partial<IBodyProps>): JSX.Element {
return <div className="mx_DecryptionFailureBody mx_EventTile_content">{getErrorMessage(mxEvent)}</div>;
} }

View file

@ -191,6 +191,13 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
{this.props.timestamp} {this.props.timestamp}
</div> </div>
); );
} else if (hangupReason === CallErrorCode.AnsweredElsewhere) {
return (
<div className="mx_LegacyCallEvent_content">
{_t("Answered elsewhere")}
{this.props.timestamp}
</div>
);
} }
let reason; let reason;

View file

@ -182,12 +182,14 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
private addListeners(): void { private addListeners(): void {
this.state.poll?.on(PollEvent.Responses, this.onResponsesChange); this.state.poll?.on(PollEvent.Responses, this.onResponsesChange);
this.state.poll?.on(PollEvent.End, this.onRelationsChange); this.state.poll?.on(PollEvent.End, this.onRelationsChange);
this.state.poll?.on(PollEvent.UndecryptableRelations, this.render.bind(this));
} }
private removeListeners(): void { private removeListeners(): void {
if (this.state.poll) { if (this.state.poll) {
this.state.poll.off(PollEvent.Responses, this.onResponsesChange); this.state.poll.off(PollEvent.Responses, this.onResponsesChange);
this.state.poll.off(PollEvent.End, this.onRelationsChange); this.state.poll.off(PollEvent.End, this.onRelationsChange);
this.state.poll.off(PollEvent.UndecryptableRelations, this.render.bind(this));
} }
} }
@ -297,7 +299,9 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
const showResults = poll.isEnded || (disclosed && myVote !== undefined); const showResults = poll.isEnded || (disclosed && myVote !== undefined);
let totalText: string; let totalText: string;
if (poll.isEnded) { if (showResults && poll.undecryptableRelationsCount) {
totalText = _t("Due to decryption errors, some votes may not be counted");
} else if (poll.isEnded) {
totalText = _t("Final result based on %(count)s votes", { count: totalVotes }); totalText = _t("Final result based on %(count)s votes", { count: totalVotes });
} else if (!disclosed) { } else if (!disclosed) {
totalText = _t("Results will be visible when the poll is ended"); totalText = _t("Results will be visible when the poll is ended");
@ -384,7 +388,7 @@ export function allVotes(voteRelations: Relations): Array<UserVote> {
* @param {string?} selected Local echo selected option for the userId * @param {string?} selected Local echo selected option for the userId
* @returns a Map of user ID to their vote info * @returns a Map of user ID to their vote info
*/ */
function collectUserVotes( export function collectUserVotes(
userResponses: Array<UserVote>, userResponses: Array<UserVote>,
userId?: string | null | undefined, userId?: string | null | undefined,
selected?: string | null | undefined, selected?: string | null | undefined,
@ -405,7 +409,7 @@ function collectUserVotes(
return userVotes; return userVotes;
} }
function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, number> { export function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, number> {
const collected = new Map<string, number>(); const collected = new Map<string, number>();
for (const response of userVotes.values()) { for (const response of userVotes.values()) {

View file

@ -21,7 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger";
import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg"; import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { _t } from "../../../languageHandler";
import { textForEvent } from "../../../TextForEvent"; import { textForEvent } from "../../../TextForEvent";
import { Caption } from "../typography/Caption";
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
import MPollBody from "./MPollBody"; import MPollBody from "./MPollBody";
@ -105,5 +107,10 @@ export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...pro
); );
} }
return <MPollBody mxEvent={pollStartEvent} {...props} />; return (
<div>
<Caption>{_t("Ended a poll")}</Caption>
<MPollBody mxEvent={pollStartEvent} {...props} />
</div>
);
}); });

View file

@ -41,7 +41,7 @@ import { MPollEndBody } from "./MPollEndBody";
import MLocationBody from "./MLocationBody"; import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody"; import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody"; import MBeaconBody from "./MBeaconBody";
import DecryptionFailureBody from "./DecryptionFailureBody"; import { DecryptionFailureBody } from "./DecryptionFailureBody";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast"; import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";

View file

@ -40,7 +40,7 @@ import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import { UIComponent, UIFeature } from "../../../settings/UIFeature"; import { UIComponent, UIFeature } from "../../../settings/UIFeature";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import { useRoomMemberCount } from "../../../hooks/useRoomMembers"; import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
import { useFeatureEnabled } from "../../../hooks/useSettings"; import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "./PinnedMessagesCard"; import { usePinnedEvents } from "./PinnedMessagesCard";

View file

@ -24,7 +24,7 @@ import AppTile from "../elements/AppTile";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useWidgets } from "./RoomSummaryCard"; import { useWidgets } from "./RoomSummaryCard";
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore";

View file

@ -399,7 +399,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
return ( return (
<div className="mx_AliasSettings"> <div className="mx_AliasSettings">
<SettingsFieldset <SettingsFieldset
data-test-id="published-address-fieldset" data-testid="published-address-fieldset"
legend={_t("Published Addresses")} legend={_t("Published Addresses")}
description={ description={
<> <>
@ -450,7 +450,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
/> />
</SettingsFieldset> </SettingsFieldset>
<SettingsFieldset <SettingsFieldset
data-test-id="local-address-fieldset" data-testid="local-address-fieldset"
legend={_t("Local Addresses")} legend={_t("Local Addresses")}
description={ description={
isSpaceRoom isSpaceRoom

View file

@ -35,7 +35,7 @@ import { Layout } from "../../../settings/enums/Layout";
import { formatTime } from "../../../DateUtils"; import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DecryptionFailureBody from "../messages/DecryptionFailureBody"; import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
import { E2EState } from "./E2EIcon"; import { E2EState } from "./E2EIcon";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import MessageContextMenu from "../context_menus/MessageContextMenu"; import MessageContextMenu from "../context_menus/MessageContextMenu";
@ -1270,7 +1270,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{this.props.mxEvent.isRedacted() ? ( {this.props.mxEvent.isRedacted() ? (
<RedactedBody mxEvent={this.props.mxEvent} /> <RedactedBody mxEvent={this.props.mxEvent} />
) : this.props.mxEvent.isDecryptionFailure() ? ( ) : this.props.mxEvent.isDecryptionFailure() ? (
<DecryptionFailureBody /> <DecryptionFailureBody mxEvent={this.props.mxEvent} />
) : ( ) : (
MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent) MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent)
)} )}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Copyright 2020 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); 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.
@ -21,8 +21,9 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../
import NotificationBadge from "./NotificationBadge"; import NotificationBadge from "./NotificationBadge";
import { NotificationState } from "../../../stores/notifications/NotificationState"; import { NotificationState } from "../../../stores/notifications/NotificationState";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import useHover from "../../../hooks/useHover";
interface IProps { interface ExtraTileProps {
isMinimized: boolean; isMinimized: boolean;
isSelected: boolean; isSelected: boolean;
displayName: string; displayName: string;
@ -31,83 +32,68 @@ interface IProps {
onClick: (ev: ButtonEvent) => void; onClick: (ev: ButtonEvent) => void;
} }
interface IState { export default function ExtraTile({
hover: boolean; isSelected,
} isMinimized,
notificationState,
displayName,
onClick,
avatar,
}: ExtraTileProps): JSX.Element {
const [, { onMouseOver, onMouseLeave }] = useHover(() => false);
export default class ExtraTile extends React.Component<IProps, IState> { // XXX: We copy classes because it's easier
public constructor(props: IProps) { const classes = classNames({
super(props); mx_ExtraTile: true,
mx_RoomTile: true,
mx_RoomTile_selected: isSelected,
mx_RoomTile_minimized: isMinimized,
});
this.state = { let badge: JSX.Element | null = null;
hover: false, if (notificationState) {
}; badge = <NotificationBadge notification={notificationState} forceCount={false} />;
} }
private onTileMouseEnter = (): void => { let name = displayName;
this.setState({ hover: true }); if (typeof name !== "string") name = "";
}; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
private onTileMouseLeave = (): void => { const nameClasses = classNames({
this.setState({ hover: false }); mx_RoomTile_title: true,
}; mx_RoomTile_titleHasUnreadEvents: notificationState?.isUnread,
});
public render(): React.ReactElement { let nameContainer: JSX.Element | null = (
// XXX: We copy classes because it's easier <div className="mx_RoomTile_titleContainer">
const classes = classNames({ <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
mx_ExtraTile: true, {name}
mx_RoomTile: true, </div>
mx_RoomTile_selected: this.props.isSelected, </div>
mx_RoomTile_minimized: this.props.isMinimized, );
}); if (isMinimized) nameContainer = null;
let badge; let Button = RovingAccessibleButton;
if (this.props.notificationState) { if (isMinimized) {
badge = <NotificationBadge notification={this.props.notificationState} forceCount={false} />; Button = RovingAccessibleTooltipButton;
} }
let name = this.props.displayName; return (
if (typeof name !== "string") name = ""; <Button
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon className={classes}
onMouseEnter={onMouseOver}
const nameClasses = classNames({ onMouseLeave={onMouseLeave}
mx_RoomTile_title: true, onClick={onClick}
mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread, role="treeitem"
}); title={isMinimized ? name : undefined}
>
let nameContainer = ( <div className="mx_RoomTile_avatarContainer">{avatar}</div>
<div className="mx_RoomTile_titleContainer"> <div className="mx_RoomTile_details">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto"> <div className="mx_RoomTile_primaryDetails">
{name} {nameContainer}
<div className="mx_RoomTile_badgeContainer">{badge}</div>
</div> </div>
</div> </div>
); </Button>
if (this.props.isMinimized) nameContainer = null; );
let Button = RovingAccessibleButton;
if (this.props.isMinimized) {
Button = RovingAccessibleTooltipButton;
}
return (
<React.Fragment>
<Button
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.props.onClick}
role="treeitem"
title={this.props.isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">{this.props.avatar}</div>
<div className="mx_RoomTile_details">
<div className="mx_RoomTile_primaryDetails">
{nameContainer}
<div className="mx_RoomTile_badgeContainer">{badge}</div>
</div>
</div>
</Button>
</React.Fragment>
);
}
} }

View file

@ -241,7 +241,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
private waitForOwnMember(): void { private waitForOwnMember(): void {
// If we have the member already, do that // If we have the member already, do that
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!);
if (me) { if (me) {
this.setState({ me }); this.setState({ me });
return; return;
@ -250,14 +250,14 @@ export class MessageComposer extends React.Component<IProps, IState> {
// The members should already be loading, and loadMembersIfNeeded // The members should already be loading, and loadMembersIfNeeded
// will return the promise for the existing operation // will return the promise for the existing operation
this.props.room.loadMembersIfNeeded().then(() => { this.props.room.loadMembersIfNeeded().then(() => {
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!);
this.setState({ me }); this.setState({ me });
}); });
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
@ -268,12 +268,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
private onTombstoneClick = (ev: ButtonEvent): void => { private onTombstoneClick = (ev: ButtonEvent): void => {
ev.preventDefault(); ev.preventDefault();
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"]; const replacementRoomId = this.context.tombstone?.getContent()["replacement_room"];
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId); const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
let createEventId = null; let createEventId: string | undefined;
if (replacementRoom) { if (replacementRoom) {
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, ""); const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, "");
if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); if (createEvent?.getId()) createEventId = createEvent.getId();
} }
const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")]; const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")];
@ -408,7 +408,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
private onRecordingEndingSoon = ({ secondsLeft }: { secondsLeft: number }): void => { private onRecordingEndingSoon = ({ secondsLeft }: { secondsLeft: number }): void => {
this.setState({ recordingTimeLeftSeconds: secondsLeft }); this.setState({ recordingTimeLeftSeconds: secondsLeft });
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000); window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: undefined }), 3000);
}; };
private setStickerPickerOpen = (isStickerPickerOpen: boolean): void => { private setStickerPickerOpen = (isStickerPickerOpen: boolean): void => {

View file

@ -38,7 +38,7 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P
// Don't show a badge if we don't need to // Don't show a badge if we don't need to
if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) { if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) {
return null; return <></>;
} }
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
@ -54,8 +54,8 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount, mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
mx_NotificationBadge_highlighted: color >= NotificationColor.Red, mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
mx_NotificationBadge_dot: isEmptyBadge, mx_NotificationBadge_dot: isEmptyBadge,
mx_NotificationBadge_2char: symbol?.length > 0 && symbol?.length < 3, mx_NotificationBadge_2char: symbol && symbol.length > 0 && symbol.length < 3,
mx_NotificationBadge_3char: symbol?.length > 2, mx_NotificationBadge_3char: symbol && symbol.length > 2,
}); });
if (props.onClick) { if (props.onClick) {

View file

@ -62,7 +62,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
eventId: string, eventId: string,
relationType: RelationType | string, relationType: RelationType | string,
eventType: EventType | string, eventType: EventType | string,
): Relations => { ): Relations | undefined => {
if (eventId === this.props.event.getId()) { if (eventId === this.props.event.getId()) {
return this.relations.get(relationType)?.get(eventType); return this.relations.get(relationType)?.get(eventType);
} }
@ -71,7 +71,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const sender = this.props.event.getSender(); const sender = this.props.event.getSender();
let unpinButton = null; let unpinButton: JSX.Element | undefined;
if (this.props.onUnpinClicked) { if (this.props.onUnpinClicked) {
unpinButton = ( unpinButton = (
<AccessibleTooltipButton <AccessibleTooltipButton

View file

@ -66,7 +66,7 @@ export function determineAvatarPosition(index: number, max: number): IAvatarPosi
} }
} }
export function readReceiptTooltip(members: string[], hasMore: boolean): string | null { export function readReceiptTooltip(members: string[], hasMore: boolean): string | undefined {
if (hasMore) { if (hasMore) {
return _t("%(members)s and more", { return _t("%(members)s and more", {
members: members.join(", "), members: members.join(", "),
@ -78,8 +78,6 @@ export function readReceiptTooltip(members: string[], hasMore: boolean): string
}); });
} else if (members.length) { } else if (members.length) {
return members[0]; return members[0];
} else {
return null;
} }
} }
@ -134,7 +132,7 @@ export function ReadReceiptGroup({
const { hidden, position } = determineAvatarPosition(index, maxAvatars); const { hidden, position } = determineAvatarPosition(index, maxAvatars);
const userId = receipt.userId; const userId = receipt.userId;
let readReceiptInfo: IReadReceiptInfo; let readReceiptInfo: IReadReceiptInfo | undefined;
if (readReceiptMap) { if (readReceiptMap) {
readReceiptInfo = readReceiptMap[userId]; readReceiptInfo = readReceiptMap[userId];
@ -161,7 +159,7 @@ export function ReadReceiptGroup({
}) })
.reverse(); .reverse();
let remText: JSX.Element; let remText: JSX.Element | undefined;
const remainder = readReceipts.length - maxAvatars; const remainder = readReceipts.length - maxAvatars;
if (remainder > 0) { if (remainder > 0) {
remText = ( remText = (

View file

@ -133,7 +133,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
target.top = 0; target.top = 0;
target.right = 0; target.right = 0;
target.parent = null; target.parent = undefined;
return target; return target;
} }
// this is the mx_ReadReceiptsGroup // this is the mx_ReadReceiptsGroup
@ -146,7 +146,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
target.top = 0; target.top = 0;
target.right = 0; target.right = 0;
target.parent = null; target.parent = undefined;
return target; return target;
} }
@ -179,7 +179,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
: // treat new RRs as though they were off the top of the screen : // treat new RRs as though they were off the top of the screen
-READ_AVATAR_SIZE; -READ_AVATAR_SIZE;
const startStyles = []; const startStyles: IReadReceiptMarkerStyle[] = [];
if (oldInfo?.right) { if (oldInfo?.right) {
startStyles.push({ startStyles.push({
top: oldPosition - newPosition, top: oldPosition - newPosition,
@ -210,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
return ( return (
<NodeAnimator startStyles={this.state.startStyles}> <NodeAnimator startStyles={this.state.startStyles}>
<MemberAvatar <MemberAvatar
member={this.props.member} member={this.props.member ?? null}
fallbackUserId={this.props.fallbackUserId} fallbackUserId={this.props.fallbackUserId}
aria-hidden="true" aria-hidden="true"
aria-live="off" aria-live="off"

View file

@ -168,7 +168,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
...this.props, ...this.props,
// overrides // overrides
ref: null, ref: undefined,
showUrlPreview: false, showUrlPreview: false,
overrideBodyTypes: msgtypeOverrides, overrideBodyTypes: msgtypeOverrides,
overrideEventTypes: evOverrides, overrideEventTypes: evOverrides,

View file

@ -37,5 +37,5 @@ export function RoomContextDetails<T extends keyof ReactHTML>({ room, component,
); );
} }
return null; return <></>;
} }

View file

@ -62,7 +62,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room"); roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
} }
let members: JSX.Element; let members: JSX.Element | undefined;
if (membership === "invite" && summary) { if (membership === "invite" && summary) {
// Don't trust local state and instead use the summary API // Don't trust local state and instead use the summary API
members = ( members = (

View file

@ -117,7 +117,7 @@ const auxButtonContextMenuPosition = (handle: RefObject<HTMLDivElement>): MenuPr
const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = defaultDispatcher }) => { const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = defaultDispatcher }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace: Room = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom; return SpaceStore.instance.activeSpaceRoom;
}); });
@ -125,7 +125,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers); const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
if (activeSpace && (showCreateRooms || showInviteUsers)) { if (activeSpace && (showCreateRooms || showInviteUsers)) {
let contextMenu: JSX.Element; let contextMenu: JSX.Element | undefined;
if (menuDisplayed) { if (menuDisplayed) {
const canInvite = shouldShowSpaceInvite(activeSpace); const canInvite = shouldShowSpaceInvite(activeSpace);
@ -208,7 +208,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => { const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { const activeSpace = useEventEmitterState<Room | null>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom; return SpaceStore.instance.activeSpaceRoom;
}); });
@ -216,11 +216,11 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
let contextMenuContent: JSX.Element | null = null; let contextMenuContent: JSX.Element | undefined;
if (menuDisplayed && activeSpace) { if (menuDisplayed && activeSpace) {
const canAddRooms = activeSpace.currentState.maySendStateEvent( const canAddRooms = activeSpace.currentState.maySendStateEvent(
EventType.SpaceChild, EventType.SpaceChild,
MatrixClientPeg.get().getUserId(), MatrixClientPeg.get().getUserId()!,
); );
contextMenuContent = ( contextMenuContent = (
@ -469,13 +469,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
SettingsStore.unwatchSetting(this.favouriteMessageWatcher); SettingsStore.unwatchSetting(this.favouriteMessageWatcher);
defaultDispatcher.unregister(this.dispatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
} }
private onRoomViewStoreUpdate = (): void => { private onRoomViewStoreUpdate = (): void => {
this.setState({ this.setState({
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId(), currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined,
}); });
}; };
@ -629,7 +629,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
Object.values(RoomListStore.instance.orderedLists).every((list) => !list?.length); Object.values(RoomListStore.instance.orderedLists).every((list) => !list?.length);
return TAG_ORDER.map((orderedTagId) => { return TAG_ORDER.map((orderedTagId) => {
let extraTiles = null; let extraTiles: ReactComponentElement<typeof ExtraTile>[] | undefined;
if (orderedTagId === DefaultTagID.Suggested) { if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms(); extraTiles = this.renderSuggestedRooms();
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) { } else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {

View file

@ -137,12 +137,10 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
} }
}, [closeMainMenu, canShowMainMenu, mainMenuDisplayed]); }, [closeMainMenu, canShowMainMenu, mainMenuDisplayed]);
const spaceName = useTypedEventEmitterState(activeSpace, RoomEvent.Name, () => activeSpace?.name); const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name);
useEffect(() => { useEffect(() => {
if (onVisibilityChange) { onVisibilityChange?.();
onVisibilityChange();
}
}, [onVisibilityChange]); }, [onVisibilityChange]);
const canExploreRooms = shouldShowComponent(UIComponent.ExploreRooms); const canExploreRooms = shouldShowComponent(UIComponent.ExploreRooms);
@ -151,7 +149,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent( const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent(
EventType.SpaceChild, EventType.SpaceChild,
cli.getUserId(), cli.getUserId()!,
); );
const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms; const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms;
const canAddSubSpaces = hasPermissionToAddSpaceChild && canCreateSpaces; const canAddSubSpaces = hasPermissionToAddSpaceChild && canCreateSpaces;
@ -161,7 +159,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
// communities and spaces, but is at risk of no options on the Home tab. // communities and spaces, but is at risk of no options on the Home tab.
const canShowPlusMenu = canCreateRooms || canExploreRooms || canCreateSpaces || activeSpace; const canShowPlusMenu = canCreateRooms || canExploreRooms || canCreateSpaces || activeSpace;
let contextMenu: JSX.Element; let contextMenu: JSX.Element | undefined;
if (mainMenuDisplayed && mainMenuHandle.current) { if (mainMenuDisplayed && mainMenuHandle.current) {
let ContextMenuComponent; let ContextMenuComponent;
if (activeSpace) { if (activeSpace) {
@ -179,7 +177,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
/> />
); );
} else if (plusMenuDisplayed && activeSpace) { } else if (plusMenuDisplayed && activeSpace) {
let inviteOption: JSX.Element; let inviteOption: JSX.Element | undefined;
if (shouldShowSpaceInvite(activeSpace)) { if (shouldShowSpaceInvite(activeSpace)) {
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -195,8 +193,8 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
); );
} }
let newRoomOptions: JSX.Element; let newRoomOptions: JSX.Element | undefined;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) { if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()!)) {
newRoomOptions = ( newRoomOptions = (
<> <>
<IconizedContextMenuOption <IconizedContextMenuOption
@ -265,7 +263,9 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
closePlusMenu(); closePlusMenu();
}} }}
disabled={!canAddSubRooms} disabled={!canAddSubRooms}
tooltip={!canAddSubRooms && _t("You do not have permissions to add rooms to this space")} tooltip={
!canAddSubRooms ? _t("You do not have permissions to add rooms to this space") : undefined
}
/> />
{canCreateSpaces && ( {canCreateSpaces && (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -278,7 +278,11 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
closePlusMenu(); closePlusMenu();
}} }}
disabled={!canAddSubSpaces} disabled={!canAddSubSpaces}
tooltip={!canAddSubSpaces && _t("You do not have permissions to add spaces to this space")} tooltip={
!canAddSubSpaces
? _t("You do not have permissions to add spaces to this space")
: undefined
}
> >
<BetaPill /> <BetaPill />
</IconizedContextMenuOption> </IconizedContextMenuOption>
@ -287,8 +291,8 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
</IconizedContextMenu> </IconizedContextMenu>
); );
} else if (plusMenuDisplayed) { } else if (plusMenuDisplayed) {
let newRoomOpts: JSX.Element; let newRoomOpts: JSX.Element | undefined;
let joinRoomOpt: JSX.Element; let joinRoomOpt: JSX.Element | undefined;
if (canCreateRooms) { if (canCreateRooms) {
newRoomOpts = ( newRoomOpts = (
@ -366,7 +370,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
} }
let title: string; let title: string;
if (activeSpace) { if (activeSpace && spaceName) {
title = spaceName; title = spaceName;
} else { } else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome); title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixError } from "matrix-js-sdk/src/http-api"; import { MatrixError } from "matrix-js-sdk/src/http-api";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
@ -76,6 +76,12 @@ interface IProps {
canPreview?: boolean; canPreview?: boolean;
previewLoading?: boolean; previewLoading?: boolean;
// The id of the room to be previewed, if it is known.
// (It may be unknown if we are waiting for an alias to be resolved.)
roomId?: string;
// A `Room` object for the room to be previewed, if we have one.
room?: Room; room?: Room;
loading?: boolean; loading?: boolean;
@ -215,25 +221,27 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
return { memberName, reason }; return { memberName, reason };
} }
private joinRule(): JoinRule { private joinRule(): JoinRule | null {
return this.props.room?.currentState return (
.getStateEvents(EventType.RoomJoinRules, "") this.props.room?.currentState
?.getContent<IJoinRuleEventContent>().join_rule; .getStateEvents(EventType.RoomJoinRules, "")
?.getContent<IJoinRuleEventContent>().join_rule ?? null
);
} }
private getMyMember(): RoomMember { private getMyMember(): RoomMember | null {
return this.props.room?.getMember(MatrixClientPeg.get().getUserId()); return this.props.room?.getMember(MatrixClientPeg.get().getUserId()!) ?? null;
} }
private getInviteMember(): RoomMember { private getInviteMember(): RoomMember | null {
const { room } = this.props; const { room } = this.props;
if (!room) { if (!room) {
return; return null;
} }
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId()!;
const inviteEvent = room.currentState.getMember(myUserId); const inviteEvent = room.currentState.getMember(myUserId);
if (!inviteEvent) { if (!inviteEvent) {
return; return null;
} }
const inviterUserId = inviteEvent.events.member.getSender(); const inviterUserId = inviteEvent.events.member.getSender();
return room.currentState.getMember(inviterUserId); return room.currentState.getMember(inviterUserId);
@ -276,15 +284,15 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const isSpace = this.props.room?.isSpaceRoom() ?? this.props.oobData?.roomType === RoomType.Space; const isSpace = this.props.room?.isSpaceRoom() ?? this.props.oobData?.roomType === RoomType.Space;
let showSpinner = false; let showSpinner = false;
let title; let title: string | undefined;
let subTitle; let subTitle: string | ReactNode[] | undefined;
let reasonElement; let reasonElement: JSX.Element | undefined;
let primaryActionHandler; let primaryActionHandler: (() => void) | undefined;
let primaryActionLabel; let primaryActionLabel: string | undefined;
let secondaryActionHandler; let secondaryActionHandler: (() => void) | undefined;
let secondaryActionLabel; let secondaryActionLabel: string | undefined;
let footer; let footer: JSX.Element | undefined;
const extraComponents = []; const extraComponents: JSX.Element[] = [];
const messageCase = this.getMessageCase(); const messageCase = this.getMessageCase();
switch (messageCase) { switch (messageCase) {
@ -310,18 +318,14 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
} }
case MessageCase.NotLoggedIn: { case MessageCase.NotLoggedIn: {
const opts: RoomPreviewOpts = { canJoin: false }; const opts: RoomPreviewOpts = { canJoin: false };
if (this.props.room?.roomId) { if (this.props.roomId) {
ModuleRunner.instance.invoke( ModuleRunner.instance.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.roomId);
RoomViewLifecycle.PreviewRoomNotLoggedIn,
opts,
this.props.room.roomId,
);
} }
if (opts.canJoin) { if (opts.canJoin) {
title = _t("Join the room to participate"); title = _t("Join the room to participate");
primaryActionLabel = _t("Join"); primaryActionLabel = _t("Join");
primaryActionHandler = () => { primaryActionHandler = () => {
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.room.roomId); ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.roomId);
}; };
} else { } else {
title = _t("Join the conversation with an account"); title = _t("Join the conversation with an account");
@ -349,7 +353,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
} else { } else {
title = _t("You were removed by %(memberName)s", { memberName }); title = _t("You were removed by %(memberName)s", { memberName });
} }
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null; subTitle = reason ? _t("Reason: %(reason)s", { reason }) : undefined;
if (isSpace) { if (isSpace) {
primaryActionLabel = _t("Forget this space"); primaryActionLabel = _t("Forget this space");
@ -374,7 +378,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
} else { } else {
title = _t("You were banned by %(memberName)s", { memberName }); title = _t("You were banned by %(memberName)s", { memberName });
} }
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null; subTitle = reason ? _t("Reason: %(reason)s", { reason }) : undefined;
if (isSpace) { if (isSpace) {
primaryActionLabel = _t("Forget this space"); primaryActionLabel = _t("Forget this space");
} else { } else {
@ -497,7 +501,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
primaryActionLabel = _t("Accept"); primaryActionLabel = _t("Accept");
} }
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId()!;
const member = this.props.room?.currentState.getMember(myUserId); const member = this.props.room?.currentState.getMember(myUserId);
const memberEventContent = member?.events.member?.getContent(); const memberEventContent = member?.events.member?.getContent();

View file

@ -22,7 +22,7 @@ import { Dispatcher } from "flux";
import { Enable, Resizable } from "re-resizable"; import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer"; import { Direction } from "re-resizable/lib/resizer";
import * as React from "react"; import * as React from "react";
import { ComponentType, createRef, ReactComponentElement } from "react"; import { ComponentType, createRef, ReactComponentElement, ReactNode } from "react";
import { polyfillTouchEvent } from "../../../@types/polyfill"; import { polyfillTouchEvent } from "../../../@types/polyfill";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@ -82,7 +82,7 @@ interface IProps {
alwaysVisible?: boolean; alwaysVisible?: boolean;
forceExpanded?: boolean; forceExpanded?: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[]; extraTiles?: ReactComponentElement<typeof ExtraTile>[] | null;
onListCollapse?: (isExpanded: boolean) => void; onListCollapse?: (isExpanded: boolean) => void;
} }
@ -95,7 +95,7 @@ interface ResizeDelta {
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">; type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState { interface IState {
contextMenuPosition: PartialDOMRect; contextMenuPosition?: PartialDOMRect;
isResizing: boolean; isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number; height: number;
@ -123,7 +123,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
this.heightAtStart = 0; this.heightAtStart = 0;
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId); this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
this.state = { this.state = {
contextMenuPosition: null,
isResizing: false, isResizing: false,
isExpanded: !this.layout.isCollapsed, isExpanded: !this.layout.isCollapsed,
height: 0, // to be fixed in a moment, we need `rooms` to calculate this. height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
@ -160,17 +159,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
} }
private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null { private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null {
if (this.props.extraTiles) { return this.props.extraTiles ?? null;
return this.props.extraTiles;
}
return null;
} }
private get numTiles(): number { private get numTiles(): number {
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles); return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
} }
private static calcNumTiles(rooms: Room[], extraTiles: any[]): number { private static calcNumTiles(rooms: Room[], extraTiles?: any[] | null): number {
return (rooms || []).length + (extraTiles || []).length; return (rooms || []).length + (extraTiles || []).length;
} }
@ -390,7 +386,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}; };
private onCloseMenu = (): void => { private onCloseMenu = (): void => {
this.setState({ contextMenuPosition: null }); this.setState({ contextMenuPosition: undefined });
}; };
private onUnreadFirstChanged = (): void => { private onUnreadFirstChanged = (): void => {
@ -506,7 +502,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// On ArrowLeft go to the sublist header // On ArrowLeft go to the sublist header
case KeyBindingAction.ArrowLeft: case KeyBindingAction.ArrowLeft:
ev.stopPropagation(); ev.stopPropagation();
this.headerButton.current.focus(); this.headerButton.current?.focus();
break; break;
// Consume ArrowRight so it doesn't cause focus to get sent to composer // Consume ArrowRight so it doesn't cause focus to get sent to composer
case KeyBindingAction.ArrowRight: case KeyBindingAction.ArrowRight:
@ -557,10 +553,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return tiles; return tiles;
} }
private renderMenu(): React.ReactElement { private renderMenu(): ReactNode {
if (this.props.tagId === DefaultTagID.Suggested || this.props.tagId === DefaultTagID.SavedItems) return null; // not sortable if (this.props.tagId === DefaultTagID.Suggested || this.props.tagId === DefaultTagID.SavedItems) return null; // not sortable
let contextMenu = null; let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuPosition) { if (this.state.contextMenuPosition) {
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
@ -571,7 +567,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
} }
// Invites don't get some nonsense options, so only add them if we have to. // Invites don't get some nonsense options, so only add them if we have to.
let otherSections = null; let otherSections: JSX.Element | undefined;
if (this.props.tagId !== DefaultTagID.Invite) { if (this.props.tagId !== DefaultTagID.Invite) {
otherSections = ( otherSections = (
<React.Fragment> <React.Fragment>
@ -665,7 +661,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
/> />
); );
let addRoomButton = null; let addRoomButton: JSX.Element | undefined;
if (this.props.AuxButtonComponent) { if (this.props.AuxButtonComponent) {
const AuxButtonComponent = this.props.AuxButtonComponent; const AuxButtonComponent = this.props.AuxButtonComponent;
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />; addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
@ -747,7 +743,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
mx_RoomSublist_hidden: hidden, mx_RoomSublist_hidden: hidden,
}); });
let content = null; let content: JSX.Element | undefined;
if (this.state.roomsLoading) { if (this.state.roomsLoading) {
content = <div className="mx_RoomSublist_skeletonUI" />; content = <div className="mx_RoomSublist_skeletonUI" />;
} else if (visibleTiles.length > 0 && this.props.forceExpanded) { } else if (visibleTiles.length > 0 && this.props.forceExpanded) {
@ -773,7 +769,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// If we're hiding rooms, show a 'show more' button to the user. This button // If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present. If the user has all // floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'. // tiles visible, it becomes 'show less'.
let showNButton = null; let showNButton: JSX.Element | undefined;
const hasMoreSlidingSync = const hasMoreSlidingSync =
this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length; this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length;
@ -786,7 +782,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown; numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
} }
const label = _t("Show %(count)s more", { count: numMissing }); const label = _t("Show %(count)s more", { count: numMissing });
let showMoreText = <span className="mx_RoomSublist_showNButtonText">{label}</span>; let showMoreText: ReactNode = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
if (this.props.isMinimized) showMoreText = null; if (this.props.isMinimized) showMoreText = null;
showNButton = ( showNButton = (
<RovingAccessibleButton <RovingAccessibleButton
@ -804,7 +800,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
} else if (this.numTiles > this.layout.defaultVisibleTiles) { } else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less // we have all tiles visible - add a button to show less
const label = _t("Show less"); const label = _t("Show less");
let showLessText = <span className="mx_RoomSublist_showNButtonText">{label}</span>; let showLessText: ReactNode = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
if (this.props.isMinimized) showLessText = null; if (this.props.isMinimized) showLessText = null;
showNButton = ( showNButton = (
<RovingAccessibleButton <RovingAccessibleButton

View file

@ -72,13 +72,13 @@ export default class SearchBar extends React.Component<IProps, IState> {
}; };
private searchIfQuery(): void { private searchIfQuery(): void {
if (this.searchTerm.current.value) { if (this.searchTerm.current?.value) {
this.onSearch(); this.onSearch();
} }
} }
private onSearch = (): void => { private onSearch = (): void => {
if (!this.searchTerm.current.value.trim()) return; if (!this.searchTerm.current?.value.trim()) return;
this.props.onSearch(this.searchTerm.current.value, this.state.scope); this.props.onSearch(this.searchTerm.current.value, this.state.scope);
}; };

View file

@ -368,7 +368,12 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null; this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null;
let commandSuccessful: boolean; let commandSuccessful: boolean;
[content, commandSuccessful] = await runSlashCommand(cmd, args, this.props.room.roomId, threadId); [content, commandSuccessful] = await runSlashCommand(
cmd,
args,
this.props.room.roomId,
threadId ?? null,
);
if (!commandSuccessful) { if (!commandSuccessful) {
return; // errored return; // errored
} }
@ -425,7 +430,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
const prom = doMaybeLocalRoomAction( const prom = doMaybeLocalRoomAction(
roomId, roomId,
(actualRoomId: string) => this.props.mxClient.sendMessage(actualRoomId, threadId, content), (actualRoomId: string) => this.props.mxClient.sendMessage(actualRoomId, threadId ?? null, content!),
this.props.mxClient, this.props.mxClient,
); );
if (replyToEvent) { if (replyToEvent) {
@ -439,7 +444,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
} }
dis.dispatch({ action: "message_sent" }); dis.dispatch({ action: "message_sent" });
CHAT_EFFECTS.forEach((effect) => { CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(content, effect.emojis)) { if (containsEmoji(content!, effect.emojis)) {
// For initial threads launch, chat effects are disabled // For initial threads launch, chat effects are disabled
// see #19731 // see #19731
const isNotThread = this.props.relation?.rel_type !== THREAD_RELATION_TYPE.name; const isNotThread = this.props.relation?.rel_type !== THREAD_RELATION_TYPE.name;

View file

@ -52,9 +52,9 @@ interface IProps {
} }
interface IState { interface IState {
imError: string; imError: string | null;
stickerpickerWidget: IWidgetEvent; stickerpickerWidget: IWidgetEvent | null;
widgetId: string; widgetId: string | null;
} }
export default class Stickerpicker extends React.PureComponent<IProps, IState> { export default class Stickerpicker extends React.PureComponent<IProps, IState> {
@ -71,7 +71,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private popoverWidth = 300; private popoverWidth = 300;
private popoverHeight = 300; private popoverHeight = 300;
// This is loaded by _acquireScalarClient on an as-needed basis. // This is loaded by _acquireScalarClient on an as-needed basis.
private scalarClient: ScalarAuthClient = null; private scalarClient: ScalarAuthClient | null = null;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -82,13 +82,13 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
}; };
} }
private acquireScalarClient(): Promise<void | ScalarAuthClient> { private async acquireScalarClient(): Promise<void | undefined | null | ScalarAuthClient> {
if (this.scalarClient) return Promise.resolve(this.scalarClient); if (this.scalarClient) return Promise.resolve(this.scalarClient);
// TODO: Pick the right manager for the widget // TODO: Pick the right manager for the widget
if (IntegrationManagers.sharedInstance().hasManager()) { if (IntegrationManagers.sharedInstance().hasManager()) {
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient(); this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager()?.getScalarClient() ?? null;
return this.scalarClient return this.scalarClient
.connect() ?.connect()
.then(() => { .then(() => {
this.forceUpdate(); this.forceUpdate();
return this.scalarClient; return this.scalarClient;
@ -170,21 +170,14 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private updateWidget = (): void => { private updateWidget = (): void => {
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0]; const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
if (!stickerpickerWidget) { if (!stickerpickerWidget) {
Stickerpicker.currentWidget = null; Stickerpicker.currentWidget = undefined;
this.setState({ stickerpickerWidget: null, widgetId: null }); this.setState({ stickerpickerWidget: null, widgetId: null });
return; return;
} }
const currentWidget = Stickerpicker.currentWidget; const currentWidget = Stickerpicker.currentWidget;
let currentUrl = null; const currentUrl = currentWidget?.content?.url ?? null;
if (currentWidget && currentWidget.content && currentWidget.content.url) { const newUrl = stickerpickerWidget?.content?.url ?? null;
currentUrl = currentWidget.content.url;
}
let newUrl = null;
if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
newUrl = stickerpickerWidget.content.url;
}
if (newUrl !== currentUrl) { if (newUrl !== currentUrl) {
// Destroy the existing frame so a new one can be created // Destroy the existing frame so a new one can be created
@ -238,7 +231,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private sendVisibilityToWidget(visible: boolean): void { private sendVisibilityToWidget(visible: boolean): void {
if (!this.state.stickerpickerWidget) return; if (!this.state.stickerpickerWidget) return;
const messaging = WidgetMessagingStore.instance.getMessagingForUid( const messaging = WidgetMessagingStore.instance.getMessagingForUid(
WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id, null), WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id),
); );
if (messaging && visible !== this.prevSentVisibility) { if (messaging && visible !== this.prevSentVisibility) {
messaging.updateVisibility(visible).catch((err) => { messaging.updateVisibility(visible).catch((err) => {
@ -300,8 +293,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
room={this.props.room} room={this.props.room}
threadId={this.props.threadId} threadId={this.props.threadId}
fullWidth={true} fullWidth={true}
userId={MatrixClientPeg.get().credentials.userId} userId={MatrixClientPeg.get().credentials.userId!}
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId!}
waitForIframeLoad={true} waitForIframeLoad={true}
showMenubar={true} showMenubar={true}
onEditClick={this.launchManageIntegrations} onEditClick={this.launchManageIntegrations}
@ -347,8 +340,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private launchManageIntegrations = (): void => { private launchManageIntegrations = (): void => {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
IntegrationManagers.sharedInstance() IntegrationManagers.sharedInstance()
.getPrimaryManager() ?.getPrimaryManager()
.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId); ?.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId ?? undefined);
}; };
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -45,19 +45,19 @@ interface IState {
} }
export default class ThirdPartyMemberInfo extends React.Component<IProps, IState> { export default class ThirdPartyMemberInfo extends React.Component<IProps, IState> {
private room: Room; private readonly room: Room | null;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId()); this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
const me = this.room.getMember(MatrixClientPeg.get().getUserId()); const me = this.room?.getMember(MatrixClientPeg.get().getUserId()!);
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevels = this.room?.currentState.getStateEvents("m.room.power_levels", "");
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50; let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
if (typeof kickLevel !== "number") kickLevel = 50; if (typeof kickLevel !== "number") kickLevel = 50;
const sender = this.room.getMember(this.props.event.getSender()); const sender = this.room?.getMember(this.props.event.getSender());
this.state = { this.state = {
stateKey: this.props.event.getStateKey(), stateKey: this.props.event.getStateKey(),
@ -121,7 +121,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
let adminTools = null; let adminTools: JSX.Element | undefined;
if (this.state.canKick && this.state.invited) { if (this.state.canKick && this.state.invited) {
adminTools = ( adminTools = (
<div className="mx_MemberInfo_container"> <div className="mx_MemberInfo_container">
@ -135,8 +135,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
); );
} }
let scopeHeader; let scopeHeader: JSX.Element | undefined;
if (this.room.isSpaceRoom()) { if (this.room?.isSpaceRoom()) {
scopeHeader = ( scopeHeader = (
<div className="mx_RightPanel_scopeHeader"> <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} /> <RoomAvatar room={this.room} height={32} width={32} />

View file

@ -77,18 +77,18 @@ interface IPreviewProps {
export const ThreadMessagePreview: React.FC<IPreviewProps> = ({ thread, showDisplayname = false }) => { export const ThreadMessagePreview: React.FC<IPreviewProps> = ({ thread, showDisplayname = false }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.replyToEvent); const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.replyToEvent) ?? undefined;
// track the content as a means to regenerate the thread message preview upon edits & decryption // track the content as a means to regenerate the thread message preview upon edits & decryption
const [content, setContent] = useState<IContent>(lastReply?.getContent()); const [content, setContent] = useState<IContent | undefined>(lastReply?.getContent());
useTypedEventEmitter(lastReply, MatrixEventEvent.Replaced, () => { useTypedEventEmitter(lastReply, MatrixEventEvent.Replaced, () => {
setContent(lastReply.getContent()); setContent(lastReply!.getContent());
}); });
const awaitDecryption = lastReply?.shouldAttemptDecryption() || lastReply?.isBeingDecrypted(); const awaitDecryption = lastReply?.shouldAttemptDecryption() || lastReply?.isBeingDecrypted();
useTypedEventEmitter(awaitDecryption ? lastReply : null, MatrixEventEvent.Decrypted, () => { useTypedEventEmitter(awaitDecryption ? lastReply : undefined, MatrixEventEvent.Decrypted, () => {
setContent(lastReply.getContent()); setContent(lastReply!.getContent());
}); });
const preview = useAsyncMemo(async (): Promise<string> => { const preview = useAsyncMemo(async (): Promise<string | undefined> => {
if (!lastReply) return; if (!lastReply) return;
await cli.decryptEventIfNeeded(lastReply); await cli.decryptEventIfNeeded(lastReply);
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply); return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);

View file

@ -20,8 +20,8 @@ import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
interface IProps { interface IProps {
onScrollUpClick?: (e: React.MouseEvent) => void; onScrollUpClick: (e: React.MouseEvent) => void;
onCloseClick?: (e: React.MouseEvent) => void; onCloseClick: (e: React.MouseEvent) => void;
} }
export default class TopUnreadMessagesBar extends React.PureComponent<IProps> { export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {

View file

@ -48,7 +48,7 @@ import { createVoiceMessageContent } from "../../../utils/createVoiceMessageCont
interface IProps { interface IProps {
room: Room; room: Room;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
relation?: IEventRelation; relation?: IEventRelation;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
} }
@ -70,9 +70,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {};
recorder: null, // no recording started by default
};
this.voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation); this.voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
} }
@ -163,7 +161,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
await VoiceRecordingStore.instance.disposeRecording(this.voiceRecordingId); await VoiceRecordingStore.instance.disposeRecording(this.voiceRecordingId);
// Reset back to no recording, which means no phase (ie: restart component entirely) // Reset back to no recording, which means no phase (ie: restart component entirely)
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false }); this.setState({ recorder: undefined, recordingPhase: undefined, didUploadFail: false });
} }
private onCancel = async (): Promise<void> => { private onCancel = async (): Promise<void> => {
@ -220,7 +218,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
try { try {
// stop any noises which might be happening // stop any noises which might be happening
PlaybackManager.instance.pauseAllExcept(null); PlaybackManager.instance.pauseAllExcept();
const recorder = VoiceRecordingStore.instance.startRecording(this.voiceRecordingId); const recorder = VoiceRecordingStore.instance.startRecording(this.voiceRecordingId);
await recorder.start(); await recorder.start();

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