mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge branch 'develop' into PlaybackContainer
This commit is contained in:
commit
b51ea6546e
@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
30
.github/workflows/cypress.yaml
vendored
30
.github/workflows/cypress.yaml
vendored
@ -5,6 +5,9 @@ on:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
@ -162,8 +165,9 @@ jobs:
|
||||
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
|
||||
PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
# We manually finalize the build in the report stage
|
||||
PERCY_PARALLEL_TOTAL: -1
|
||||
|
||||
- name: Upload Artifact
|
||||
if: failure()
|
||||
@ -181,14 +185,35 @@ jobs:
|
||||
with:
|
||||
name: cypress-junit
|
||||
path: cypress/results
|
||||
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
needs:
|
||||
- prepare
|
||||
- tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Finalize Percy
|
||||
if: needs.prepare.outputs.percy_enable == '1'
|
||||
run: npx -p @percy/cli percy build:finalize
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
- name: Skip Percy required check
|
||||
if: needs.prepare.outputs.percy_enable != '1'
|
||||
uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: Percy skipped
|
||||
context: percy/matrix-react-sdk
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
@ -196,6 +221,7 @@ jobs:
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
testrail:
|
||||
name: Report results to testrail
|
||||
needs:
|
||||
|
2
.github/workflows/pull_request.yaml
vendored
2
.github/workflows/pull_request.yaml
vendored
@ -6,7 +6,5 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
with:
|
||||
labels: "T-Defect,T-Enhancement,T-Task"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
21
.github/workflows/tests.yml
vendored
21
.github/workflows/tests.yml
vendored
@ -7,6 +7,10 @@ on:
|
||||
types: [upstream-sdk-notify]
|
||||
workflow_call:
|
||||
inputs:
|
||||
disable_coverage:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Specify true to skip generating and uploading coverage for tests"
|
||||
matrix-js-sdk-sha:
|
||||
type: string
|
||||
required: false
|
||||
@ -39,16 +43,21 @@ jobs:
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
- name: Load metrics reporter
|
||||
id: metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions '--reporters=<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
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn ${{ inputs.disable_coverage != 'true' && 'coverage' || 'test' }} \
|
||||
--ci \
|
||||
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: inputs.matrix-js-sdk-sha == ''
|
||||
if: inputs.disable_coverage != 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
|
@ -118,7 +118,12 @@ describe("Decryption Failure Bar", () => {
|
||||
"Verify this device to access all messages",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar prompts user to verify");
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
|
||||
"DecryptionFailureBar prompts user to verify",
|
||||
{
|
||||
widths: [320, 640],
|
||||
},
|
||||
);
|
||||
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Verify").click();
|
||||
@ -146,8 +151,11 @@ describe("Decryption Failure Bar", () => {
|
||||
"Open another device to load encrypted messages",
|
||||
);
|
||||
|
||||
cy.percySnapshot(
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
|
||||
"DecryptionFailureBar prompts user to open another device, with Resend Key Requests button",
|
||||
{
|
||||
widths: [320, 640],
|
||||
},
|
||||
);
|
||||
|
||||
cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest");
|
||||
@ -155,8 +163,11 @@ describe("Decryption Failure Bar", () => {
|
||||
cy.wait("@keyRequest");
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
|
||||
|
||||
cy.percySnapshot(
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
|
||||
"DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button",
|
||||
{
|
||||
widths: [320, 640],
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -177,7 +188,9 @@ describe("Decryption Failure Bar", () => {
|
||||
"Reset your keys to prevent future decryption errors",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar prompts user to reset keys");
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar prompts user to reset keys", {
|
||||
widths: [320, 640],
|
||||
});
|
||||
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Reset").click();
|
||||
|
||||
@ -196,7 +209,12 @@ describe("Decryption Failure Bar", () => {
|
||||
"Some messages could not be decrypted",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar displays general message with no call to action");
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
|
||||
"DecryptionFailureBar displays general message with no call to action",
|
||||
{
|
||||
widths: [320, 640],
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -210,7 +228,10 @@ describe("Decryption Failure Bar", () => {
|
||||
cy.get(".mx_DecryptionFailureBar").should("exist");
|
||||
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist");
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar displays loading spinner");
|
||||
cy.get(".mx_DecryptionFailureBar").percySnapshotElement("DecryptionFailureBar displays loading spinner", {
|
||||
allowSpinners: true,
|
||||
widths: [320, 640],
|
||||
});
|
||||
|
||||
cy.wait(5000);
|
||||
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist");
|
||||
|
@ -27,7 +27,7 @@ describe("Location sharing", () => {
|
||||
};
|
||||
|
||||
const submitShareLocation = (): void => {
|
||||
cy.get('[data-test-id="location-picker-submit-button"]').click();
|
||||
cy.get('[data-testid="location-picker-submit-button"]').click();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -159,8 +159,8 @@ describe("Timeline", () => {
|
||||
".mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.percySnapshot("Configured room on IRC layout");
|
||||
|
||||
cy.get(".mx_MainSplit").percySnapshotElement("Configured room on IRC layout");
|
||||
});
|
||||
|
||||
it("should add inline start margin to an event line on IRC layout", () => {
|
||||
@ -185,11 +185,12 @@ describe("Timeline", () => {
|
||||
.should("have.css", "margin-inline-start", "104px")
|
||||
.should("have.css", "inset-inline-start", "0px");
|
||||
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS =
|
||||
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
|
||||
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
|
||||
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
|
||||
cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
|
||||
percyCSS,
|
||||
});
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
||||
@ -213,8 +214,7 @@ describe("Timeline", () => {
|
||||
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
|
||||
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS =
|
||||
".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
|
||||
const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp { visibility: hidden !important; }";
|
||||
|
||||
// should not add inline start padding to a hidden event line on IRC layout
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
@ -223,14 +223,20 @@ describe("Timeline", () => {
|
||||
"padding-inline-start",
|
||||
"0px",
|
||||
);
|
||||
cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS });
|
||||
|
||||
cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with zero padding on IRC layout", {
|
||||
percyCSS,
|
||||
});
|
||||
|
||||
// should add inline start padding to a hidden event line on modern layout
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line")
|
||||
// calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
|
||||
.should("have.css", "padding-inline-start", "84px");
|
||||
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
|
||||
|
||||
cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", {
|
||||
percyCSS,
|
||||
});
|
||||
});
|
||||
|
||||
it("should click top left of view source event toggle", () => {
|
||||
@ -329,7 +335,12 @@ describe("Timeline", () => {
|
||||
cy.wait("@mxc");
|
||||
|
||||
cy.checkA11y();
|
||||
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS =
|
||||
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
|
||||
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
|
||||
percyCSS,
|
||||
widths: [800, 400],
|
||||
});
|
||||
});
|
||||
|
@ -133,6 +133,7 @@ describe("Stickers", () => {
|
||||
type: "m.stickerpicker",
|
||||
name: STICKER_PICKER_WIDGET_NAME,
|
||||
url: stickerPickerUrl,
|
||||
creatorUserId: "@userId",
|
||||
},
|
||||
id: STICKER_PICKER_WIDGET_ID,
|
||||
},
|
||||
|
@ -22,6 +22,7 @@ declare global {
|
||||
namespace Cypress {
|
||||
interface SnapshotOptions extends PercySnapshotOptions {
|
||||
domTransformation?: (documentClone: Document) => void;
|
||||
allowSpinners?: boolean;
|
||||
}
|
||||
|
||||
interface Chainable {
|
||||
@ -38,6 +39,10 @@ declare global {
|
||||
}
|
||||
|
||||
Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => {
|
||||
if (!options?.allowSpinners) {
|
||||
// Await spinners to vanish
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
}
|
||||
cy.percySnapshot(name, {
|
||||
domTransformation: (documentClone) => scope(documentClone, subject.selector),
|
||||
...options,
|
||||
|
10
package.json
10
package.json
@ -58,7 +58,7 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.4.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^1.1.1",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.4",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
@ -92,7 +92,7 @@
|
||||
"lodash": "^4.17.20",
|
||||
"maplibre-gl": "^2.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-events-sdk": "2.0.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.1.1",
|
||||
"minimist": "^1.2.5",
|
||||
@ -111,7 +111,7 @@
|
||||
"react-transition-group": "^4.4.1",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.3.2",
|
||||
"sanitize-html": "2.8.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
@ -167,9 +167,8 @@
|
||||
"@types/react": "17.0.49",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "^2.3.1",
|
||||
"@types/sanitize-html": "2.8.0",
|
||||
"@types/tar-js": "^0.3.2",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
@ -212,7 +211,6 @@
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "2.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
|
@ -18,6 +18,7 @@
|
||||
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
|
||||
@import "./components/views/context_menus/_KebabContextMenu.pcss";
|
||||
@import "./components/views/dialogs/polls/_PollListItem.pcss";
|
||||
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
|
||||
@import "./components/views/elements/_FilterDropdown.pcss";
|
||||
@import "./components/views/elements/_FilterTabGroup.pcss";
|
||||
@import "./components/views/elements/_LearnMore.pcss";
|
||||
|
@ -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;
|
||||
}
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||
border: 1px solid $quinary-content;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
max-width: 550px;
|
||||
background-color: $background;
|
||||
|
||||
.mx_StyledRadioButton_content,
|
||||
|
@ -32,6 +32,10 @@ limitations under the License.
|
||||
grid-gap: $spacing-20;
|
||||
padding-right: $spacing-64;
|
||||
margin: $spacing-32 0;
|
||||
|
||||
&.mx_PollHistoryList_list_ENDED {
|
||||
grid-gap: $spacing-32;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_PollHistoryList_noResults {
|
||||
@ -42,3 +46,14 @@ limitations under the License.
|
||||
justify-content: center;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_PollHistoryList_loading {
|
||||
color: $secondary-content;
|
||||
text-align: center;
|
||||
|
||||
// center in all free space
|
||||
// when there are no results
|
||||
&.mx_PollHistoryList_noResultsYet {
|
||||
margin: auto auto;
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,12 @@ limitations under the License.
|
||||
max-width: 100%;
|
||||
|
||||
&.mx_CopyableText_border {
|
||||
overflow: auto;
|
||||
border-radius: 5px;
|
||||
border: solid 1px $light-fg-color;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
padding: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
.mx_CopyableText_copyButton {
|
||||
@ -36,11 +37,15 @@ limitations under the License.
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
padding-left: 12px;
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
/* If the copy button is used within a scrollable div, make it stick to the right while scrolling */
|
||||
position: sticky;
|
||||
right: 0;
|
||||
/* center to first line */
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
background-color: $background;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
@ -160,6 +160,7 @@ limitations under the License.
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: $secondary-content;
|
||||
font-size: $font-12px;
|
||||
gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */
|
||||
margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */
|
||||
word-break: break-word;
|
||||
@ -168,6 +169,7 @@ limitations under the License.
|
||||
.mx_LegacyCallEvent_content_button {
|
||||
@mixin LegacyCallButton;
|
||||
padding: 0 $spacing-12;
|
||||
font-size: inherit;
|
||||
|
||||
span::before {
|
||||
mask-size: 16px;
|
||||
|
@ -70,4 +70,5 @@ limitations under the License.
|
||||
display: grid;
|
||||
grid-gap: $spacing-16;
|
||||
margin-bottom: $spacing-8;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export type Binding = {
|
||||
*/
|
||||
export default class AddThreepid {
|
||||
private sessionId: string;
|
||||
private submitUrl: string;
|
||||
private submitUrl?: string;
|
||||
private clientSecret: string;
|
||||
private bind: boolean;
|
||||
|
||||
@ -93,7 +93,7 @@ export default class AddThreepid {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
|
||||
return MatrixClientPeg.get()
|
||||
.requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken)
|
||||
.then(
|
||||
@ -155,7 +155,7 @@ export default class AddThreepid {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
|
||||
return MatrixClientPeg.get()
|
||||
.requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken)
|
||||
.then(
|
||||
@ -184,7 +184,7 @@ export default class AddThreepid {
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> {
|
||||
public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null] | undefined> {
|
||||
try {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (this.bind) {
|
||||
@ -282,7 +282,7 @@ export default class AddThreepid {
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
|
||||
public async haveMsisdnToken(msisdnToken: string): Promise<any[] | undefined> {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
||||
|
||||
|
@ -546,7 +546,7 @@ export default class ContentMessages {
|
||||
if (upload.cancelled) throw new UploadCanceledError();
|
||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
||||
|
||||
const response = await matrixClient.sendMessage(roomId, threadId, content);
|
||||
const response = await matrixClient.sendMessage(roomId, threadId ?? null, content);
|
||||
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
sendRoundTripMetric(matrixClient, roomId, response.event_id);
|
||||
|
@ -1024,13 +1024,12 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||
}
|
||||
|
||||
public answerCall(roomId: string): void {
|
||||
const call = this.calls.get(roomId);
|
||||
|
||||
this.stopRingingIfPossible(call.callId);
|
||||
|
||||
// no call to answer
|
||||
if (!this.calls.has(roomId)) return;
|
||||
|
||||
const call = this.calls.get(roomId)!;
|
||||
this.stopRingingIfPossible(call.callId);
|
||||
|
||||
if (this.getAllActiveCalls().length > 1) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Too Many Calls"),
|
||||
|
@ -287,7 +287,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
|
||||
return MatrixClientPeg.get().store.deleteAllData();
|
||||
})
|
||||
.then(() => {
|
||||
PlatformPeg.get().reload();
|
||||
PlatformPeg.get()?.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -519,7 +519,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
|
||||
stopMatrixClient();
|
||||
const pickleKey =
|
||||
credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
if (pickleKey) {
|
||||
|
@ -166,7 +166,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
}
|
||||
|
||||
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;
|
||||
return diff / 36e5 <= hours;
|
||||
} catch (e) {
|
||||
@ -176,7 +176,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
|
||||
public userRegisteredAfter(timestamp: Date): boolean {
|
||||
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;
|
||||
} catch (e) {
|
||||
return false;
|
||||
@ -292,7 +292,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
}
|
||||
|
||||
public getCredentials(): IMatrixClientCreds {
|
||||
let copiedCredentials = this.currentClientCreds;
|
||||
let copiedCredentials: IMatrixClientCreds | null = this.currentClientCreds;
|
||||
if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) {
|
||||
// cached credentials belong to a different user - don't use them
|
||||
copiedCredentials = null;
|
||||
|
@ -64,7 +64,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||
*
|
||||
* @return Promise<IMediaDevices> The available media devices
|
||||
*/
|
||||
public static async getDevices(): Promise<IMediaDevices> {
|
||||
public static async getDevices(): Promise<IMediaDevices | undefined> {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const output: Record<MediaDeviceKindEnum, MediaDeviceInfo[]> = {
|
||||
|
@ -54,8 +54,8 @@ export default class PosthogTrackers {
|
||||
}
|
||||
|
||||
private view: Views = Views.LOADING;
|
||||
private pageType?: PageType = null;
|
||||
private override?: ScreenName = null;
|
||||
private pageType?: PageType;
|
||||
private override?: ScreenName;
|
||||
|
||||
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
|
||||
this.view = view;
|
||||
@ -66,7 +66,7 @@ export default class PosthogTrackers {
|
||||
|
||||
private trackPage(durationMs?: number): void {
|
||||
const screenName =
|
||||
this.view === Views.LOGGED_IN ? loggedInPageTypeMap[this.pageType] : notLoggedInMap[this.view];
|
||||
this.view === Views.LOGGED_IN ? loggedInPageTypeMap[this.pageType!] : notLoggedInMap[this.view];
|
||||
PosthogAnalytics.instance.trackEvent<ScreenEvent>({
|
||||
eventName: "$pageview",
|
||||
$current_url: screenName,
|
||||
@ -85,7 +85,7 @@ export default class PosthogTrackers {
|
||||
|
||||
public clearOverride(screenName: ScreenName): void {
|
||||
if (screenName !== this.override) return;
|
||||
this.override = null;
|
||||
this.override = undefined;
|
||||
this.trackPage();
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,6 @@ export const DEFAULTS: IConfigOptions = {
|
||||
brand: "Element",
|
||||
integrations_ui_url: "https://scalar.vector.im/",
|
||||
integrations_rest_url: "https://scalar.vector.im/api",
|
||||
bug_report_endpoint_url: null,
|
||||
uisi_autorageshake_app: "element-auto-uisi",
|
||||
|
||||
jitsi: {
|
||||
|
@ -198,7 +198,7 @@ function reject(error?: any): RunResult {
|
||||
return { error };
|
||||
}
|
||||
|
||||
function success(promise?: Promise<any>): RunResult {
|
||||
function success(promise: Promise<any> = Promise.resolve()): RunResult {
|
||||
return { promise };
|
||||
}
|
||||
|
||||
@ -221,7 +221,7 @@ export const Commands = [
|
||||
command: "spoiler",
|
||||
args: "<message>",
|
||||
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>`));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
@ -282,7 +282,7 @@ export const Commands = [
|
||||
command: "plain",
|
||||
args: "<message>",
|
||||
description: _td("Sends a message as plain text, without interpreting it as markdown"),
|
||||
runFn: function (roomId, messages) {
|
||||
runFn: function (roomId, messages = "") {
|
||||
return successSync(ContentHelpers.makeTextMessage(messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
@ -291,7 +291,7 @@ export const Commands = [
|
||||
command: "html",
|
||||
args: "<message>",
|
||||
description: _td("Sends a message as html, without interpreting it as markdown"),
|
||||
runFn: function (roomId, messages) {
|
||||
runFn: function (roomId, messages = "") {
|
||||
return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
|
@ -27,6 +27,7 @@ import { timeout } from "../utils/promise";
|
||||
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
|
||||
import SpaceProvider from "./SpaceProvider";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
import { filterBoolean } from "../utils/arrays";
|
||||
|
||||
export interface ISelectionRange {
|
||||
beginning?: boolean; // whether the selection is in the first block of the editor or not
|
||||
@ -55,7 +56,7 @@ const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||
export interface IProviderCompletions {
|
||||
completions: ICompletion[];
|
||||
provider: AutocompleteProvider;
|
||||
command: ICommand;
|
||||
command: Partial<ICommand>;
|
||||
}
|
||||
|
||||
export default class Autocompleter {
|
||||
@ -98,8 +99,8 @@ export default class Autocompleter {
|
||||
);
|
||||
|
||||
// map then filter to maintain the index for the map-operation, for this.providers to line up
|
||||
return completionsList
|
||||
.map((completions, i) => {
|
||||
return filterBoolean(
|
||||
completionsList.map((completions, i) => {
|
||||
if (!completions || !completions.length) return;
|
||||
|
||||
return {
|
||||
@ -112,7 +113,7 @@ export default class Autocompleter {
|
||||
*/
|
||||
command: this.providers[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as IProviderCompletions[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import SettingsStore from "../settings/SettingsStore";
|
||||
import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
import * as recent from "../emojipicker/recent";
|
||||
import { filterBoolean } from "../utils/arrays";
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
@ -94,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
shouldMatchWordsOnly: true,
|
||||
});
|
||||
|
||||
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))) as IEmoji[];
|
||||
this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode))));
|
||||
}
|
||||
|
||||
public async getCompletions(
|
||||
|
@ -46,7 +46,7 @@ interface IState {
|
||||
export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
private unmounted = false;
|
||||
private dispatcherRef: string = null;
|
||||
private dispatcherRef: string | null = null;
|
||||
|
||||
public constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||
super(props, context);
|
||||
@ -64,7 +64,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch(this.props.url, { method: "GET" });
|
||||
res = await fetch(this.props.url!, { method: "GET" });
|
||||
} catch (err) {
|
||||
if (this.unmounted) return;
|
||||
logger.warn(`Error loading page: ${err}`);
|
||||
@ -84,7 +84,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
|
||||
if (this.props.replaceMap) {
|
||||
Object.keys(this.props.replaceMap).forEach((key) => {
|
||||
body = body.split(key).join(this.props.replaceMap[key]);
|
||||
body = body.split(key).join(this.props.replaceMap![key]);
|
||||
});
|
||||
}
|
||||
|
||||
@ -123,8 +123,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
const client = this.context || MatrixClientPeg.get();
|
||||
const isGuest = client ? client.isGuest() : true;
|
||||
const className = this.props.className;
|
||||
const classes = classnames({
|
||||
[className]: true,
|
||||
const classes = classnames(className, {
|
||||
[`${className}_guest`]: isGuest,
|
||||
[`${className}_loggedIn`]: !!client,
|
||||
});
|
||||
|
@ -40,6 +40,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
||||
const onDragEnter = (ev: DragEvent): void => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
setState((state) => ({
|
||||
// We always increment the counter no matter the types, because dragging is
|
||||
@ -49,7 +50,8 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
||||
// https://docs.w3cub.com/dom/datatransfer/types
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
|
||||
dragging:
|
||||
ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")
|
||||
ev.dataTransfer!.types.includes("Files") ||
|
||||
ev.dataTransfer!.types.includes("application/x-moz-file")
|
||||
? true
|
||||
: state.dragging,
|
||||
}));
|
||||
@ -68,6 +70,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
||||
const onDragOver = (ev: DragEvent): void => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
ev.dataTransfer.dropEffect = "none";
|
||||
|
||||
@ -82,6 +85,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
||||
const onDrop = (ev: DragEvent): void => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!ev.dataTransfer) return;
|
||||
onFileDrop(ev.dataTransfer);
|
||||
|
||||
setState((state) => ({
|
||||
|
@ -66,7 +66,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||
|
||||
private onRoomTimeline = (
|
||||
ev: MatrixEvent,
|
||||
room: Room | null,
|
||||
room: Room | undefined,
|
||||
toStartOfTimeline: boolean,
|
||||
removed: boolean,
|
||||
data: IRoomTimelineData,
|
||||
@ -78,7 +78,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||
client.decryptEventIfNeeded(ev);
|
||||
|
||||
if (ev.isBeingDecrypted()) {
|
||||
this.decryptingEvents.add(ev.getId());
|
||||
this.decryptingEvents.add(ev.getId()!);
|
||||
} else {
|
||||
this.addEncryptedLiveEvent(ev);
|
||||
}
|
||||
@ -86,7 +86,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||
|
||||
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
|
||||
if (ev.getRoomId() !== this.props.roomId) return;
|
||||
const eventId = ev.getId();
|
||||
const eventId = ev.getId()!;
|
||||
|
||||
if (!this.decryptingEvents.delete(eventId)) return;
|
||||
if (err) return;
|
||||
@ -103,7 +103,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
|
||||
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
|
||||
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
|
||||
}
|
||||
}
|
||||
|
@ -56,15 +56,15 @@ const getOwnProfile = (
|
||||
userId: string,
|
||||
): {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
} => ({
|
||||
displayName: OwnProfileStore.instance.displayName || userId,
|
||||
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE),
|
||||
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE) ?? undefined,
|
||||
});
|
||||
|
||||
const UserWelcomeTop: React.FC = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const userId = cli.getUserId();
|
||||
const userId = cli.getUserId()!;
|
||||
const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId));
|
||||
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
|
||||
setOwnProfile(getOwnProfile(userId));
|
||||
|
@ -33,7 +33,7 @@ export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
|
||||
|
||||
type InteractiveAuthCallbackSuccess = (
|
||||
success: true,
|
||||
response: IAuthData,
|
||||
response?: IAuthData,
|
||||
extra?: { emailSid?: string; clientSecret?: string },
|
||||
) => void;
|
||||
type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void;
|
||||
@ -94,7 +94,7 @@ interface IState {
|
||||
|
||||
export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
|
||||
private readonly authLogic: InteractiveAuth;
|
||||
private readonly intervalId: number = null;
|
||||
private readonly intervalId: number | null = null;
|
||||
private readonly stageComponent = createRef<IStageComponent>();
|
||||
|
||||
private unmounted = false;
|
||||
@ -103,10 +103,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
errorCode: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
|
||||
@ -213,8 +210,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||
if (busy) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
errorCode: null,
|
||||
errorText: undefined,
|
||||
errorCode: undefined,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
|
@ -166,10 +166,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
>();
|
||||
|
||||
let lastTopHeader;
|
||||
let firstBottomHeader;
|
||||
let lastTopHeader: HTMLDivElement | undefined;
|
||||
let firstBottomHeader: HTMLDivElement | undefined;
|
||||
for (const sublist of sublists) {
|
||||
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable");
|
||||
if (!header) continue; // this should never occur
|
||||
header.style.removeProperty("display"); // always clear display:none first
|
||||
|
||||
// When an element is <=40% off screen, make it take over
|
||||
@ -196,7 +197,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
// cause a no-op update, as adding/removing properties that are/aren't there cause
|
||||
// layout updates.
|
||||
for (const header of targetStyles.keys()) {
|
||||
const style = targetStyles.get(header);
|
||||
const style = targetStyles.get(header)!;
|
||||
|
||||
if (style.makeInvisible) {
|
||||
// we will have already removed the 'display: none', so add it back.
|
||||
@ -324,7 +325,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderSearchDialExplore(): React.ReactNode {
|
||||
let dialPadButton = null;
|
||||
let dialPadButton: JSX.Element | undefined;
|
||||
|
||||
// If we have dialer support, show a button to bring up the dial pad
|
||||
// to start a new call
|
||||
@ -338,7 +339,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
);
|
||||
}
|
||||
|
||||
let rightButton: JSX.Element;
|
||||
let rightButton: JSX.Element | undefined;
|
||||
if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
|
||||
rightButton = <RecentlyViewedButton />;
|
||||
} else if (this.state.activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms)) {
|
||||
|
@ -35,7 +35,7 @@ const CONNECTING_STATES = [
|
||||
CallState.CreateAnswer,
|
||||
];
|
||||
|
||||
const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing];
|
||||
const SUPPORTED_STATES = [CallState.Connected, CallState.Ringing, CallState.Ended];
|
||||
|
||||
export enum CustomCallState {
|
||||
Missed = "missed",
|
||||
@ -72,7 +72,7 @@ export function buildLegacyCallEventGroupers(
|
||||
|
||||
export default class LegacyCallEventGrouper extends EventEmitter {
|
||||
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
|
||||
private call: MatrixCall;
|
||||
private call: MatrixCall | null = null;
|
||||
public state: CallState | CustomCallState;
|
||||
|
||||
public constructor() {
|
||||
@ -111,7 +111,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
|
||||
}
|
||||
|
||||
public get hangupReason(): string | null {
|
||||
return this.hangup?.getContent()?.reason;
|
||||
return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null;
|
||||
}
|
||||
|
||||
public get rejectParty(): string {
|
||||
|
@ -226,8 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
|
||||
private screenAfterLogin?: IScreen;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: number;
|
||||
private focusComposer: boolean;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
@ -296,9 +294,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
Lifecycle.loadSession();
|
||||
}
|
||||
|
||||
this.accountPassword = null;
|
||||
this.accountPasswordTimer = null;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
this.themeWatcher = new ThemeWatcher();
|
||||
@ -439,7 +434,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||
window.removeEventListener("resize", this.onWindowResized);
|
||||
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
this.stores.accountPasswordStore.clearPassword();
|
||||
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
|
||||
}
|
||||
|
||||
@ -1987,13 +1982,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
* this, as they instead jump straight into the app after `attemptTokenLogin`.
|
||||
*/
|
||||
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => {
|
||||
this.accountPassword = password;
|
||||
// self-destruct the password after 5mins
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
this.accountPasswordTimer = window.setTimeout(() => {
|
||||
this.accountPassword = null;
|
||||
this.accountPasswordTimer = null;
|
||||
}, 60 * 5 * 1000);
|
||||
this.stores.accountPasswordStore.setPassword(password);
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
@ -2037,7 +2026,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
view = (
|
||||
<E2eSetup
|
||||
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||
accountPassword={this.accountPassword}
|
||||
accountPassword={this.stores.accountPasswordStore.getPassword()}
|
||||
tokenLogin={!!this.tokenLogin}
|
||||
/>
|
||||
);
|
||||
|
@ -114,8 +114,11 @@ import { RoomSearchView } from "./RoomSearchView";
|
||||
import eventSearch from "../../Searching";
|
||||
import VoipUserMapper from "../../VoipUserMapper";
|
||||
import { isCallEvent } from "./LegacyCallEventGrouper";
|
||||
import { WidgetType } from "../../widgets/WidgetType";
|
||||
import WidgetUtils from "../../utils/WidgetUtils";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
let debuglog = function (msg: string): void {};
|
||||
|
||||
const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe");
|
||||
@ -483,6 +486,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private onWidgetStoreUpdate = (): void => {
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
this.doMaybeRemoveOwnJitsiWidget();
|
||||
};
|
||||
|
||||
private onWidgetEchoStoreUpdate = (): void => {
|
||||
@ -503,6 +507,56 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the Jitsi widget from the current user if
|
||||
* - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN}
|
||||
* - The last (server timestamp) of these widgets is from the currrent user
|
||||
* This solves the issue if some people decide to start a conference and click the call button at the same time.
|
||||
*/
|
||||
private doMaybeRemoveOwnJitsiWidget(): void {
|
||||
if (!this.state.roomId || !this.state.room || !this.context.client) return;
|
||||
|
||||
const apps = this.context.widgetStore.getApps(this.state.roomId);
|
||||
const jitsiApps = apps.filter((app) => app.eventId && WidgetType.JITSI.matches(app.type));
|
||||
|
||||
// less than two Jitsi widgets → nothing to do
|
||||
if (jitsiApps.length < 2) return;
|
||||
|
||||
const currentUserId = this.context.client.getSafeUserId();
|
||||
const createdByCurrentUser = jitsiApps.find((apps) => apps.creatorUserId === currentUserId);
|
||||
|
||||
// no Jitsi widget from current user → nothing to do
|
||||
if (!createdByCurrentUser) return;
|
||||
|
||||
const createdByCurrentUserEvent = this.state.room.findEventById(createdByCurrentUser.eventId!);
|
||||
|
||||
// widget event not found → nothing can be done
|
||||
if (!createdByCurrentUserEvent) return;
|
||||
|
||||
const createdByCurrentUserTs = createdByCurrentUserEvent.getTs();
|
||||
|
||||
// widget timestamp is empty → nothing can be done
|
||||
if (!createdByCurrentUserTs) return;
|
||||
|
||||
const lastCreatedByOtherTs = jitsiApps.reduce((maxByNow: number, app) => {
|
||||
if (app.eventId === createdByCurrentUser.eventId) return maxByNow;
|
||||
|
||||
const appCreateTs = this.state.room!.findEventById(app.eventId!)?.getTs() || 0;
|
||||
return Math.max(maxByNow, appCreateTs);
|
||||
}, 0);
|
||||
|
||||
// last widget timestamp from other is empty → nothing can be done
|
||||
if (!lastCreatedByOtherTs) return;
|
||||
|
||||
if (
|
||||
createdByCurrentUserTs > lastCreatedByOtherTs &&
|
||||
createdByCurrentUserTs - lastCreatedByOtherTs < PREVENT_MULTIPLE_JITSI_WITHIN
|
||||
) {
|
||||
// more than one Jitsi widget with the last one from the current user → remove it
|
||||
WidgetUtils.setRoomWidget(this.state.roomId, createdByCurrentUser.id);
|
||||
}
|
||||
}
|
||||
|
||||
private checkWidgets = (room: Room): void => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
|
||||
@ -1903,6 +1957,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
roomId={this.state.roomId}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@ -1932,7 +1987,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
invitedEmail={invitedEmail}
|
||||
oobData={this.props.oobData}
|
||||
signUrl={this.props.threepidInvite?.signUrl}
|
||||
room={this.state.room}
|
||||
roomId={this.state.roomId}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@ -1969,6 +2024,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
error={this.state.roomLoadError}
|
||||
joining={this.state.joining}
|
||||
rejecting={this.state.rejecting}
|
||||
roomId={this.state.roomId}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
@ -1998,6 +2054,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
canPreview={false}
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
roomId={this.state.roomId}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@ -2090,6 +2147,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
oobData={this.props.oobData}
|
||||
canPreview={this.state.canPeek}
|
||||
room={this.state.room}
|
||||
roomId={this.state.roomId}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
||||
|
@ -139,15 +139,15 @@ export default class ViewSource extends React.Component<IProps, IState> {
|
||||
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(mxEvent.getRoomId());
|
||||
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||
return !!room?.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
|
||||
const isEditing = this.state.isEditing;
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const eventId = mxEvent.getId();
|
||||
const roomId = mxEvent.getRoomId()!;
|
||||
const eventId = mxEvent.getId()!;
|
||||
const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
|
||||
return (
|
||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
|
||||
|
@ -39,6 +39,7 @@ import AuthBody from "../../views/auth/AuthBody";
|
||||
import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
|
||||
// These are used in several places, and come from the js-sdk's autodiscovery
|
||||
// stuff. We define them here so that they'll be picked up by i18n.
|
||||
@ -120,15 +121,11 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
busyLoggingIn: null,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
canTryLogin: true,
|
||||
|
||||
flows: null,
|
||||
|
||||
username: props.defaultUsername ? props.defaultUsername : "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
|
||||
serverIsAlive: true,
|
||||
@ -167,7 +164,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
}
|
||||
}
|
||||
|
||||
public isBusy = (): boolean => this.state.busy || this.props.busy;
|
||||
public isBusy = (): boolean => !!this.state.busy || !!this.props.busy;
|
||||
|
||||
public onPasswordLogin: OnPasswordLogin = async (
|
||||
username: string | undefined,
|
||||
@ -349,7 +346,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const ssoKind = ssoFlow.type === "m.login.sso" ? "sso" : "cas";
|
||||
PlatformPeg.get().startSingleSignOn(
|
||||
PlatformPeg.get()?.startSingleSignOn(
|
||||
this.loginLogic.createTemporaryClient(),
|
||||
ssoKind,
|
||||
this.props.fragmentAfterLogin,
|
||||
@ -511,13 +508,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
return errorText;
|
||||
}
|
||||
|
||||
public renderLoginComponentForFlows(): JSX.Element {
|
||||
public renderLoginComponentForFlows(): ReactNode {
|
||||
if (!this.state.flows) return null;
|
||||
|
||||
// this is the ideal order we want to show the flows in
|
||||
const order = ["m.login.password", "m.login.sso"];
|
||||
|
||||
const flows = order.map((type) => this.state.flows.find((flow) => flow.type === type)).filter(Boolean);
|
||||
const flows = filterBoolean(order.map((type) => this.state.flows.find((flow) => flow.type === type)));
|
||||
return (
|
||||
<React.Fragment>
|
||||
{flows.map((flow) => {
|
||||
|
@ -65,7 +65,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
|
||||
"src",
|
||||
`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);
|
||||
this.captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
this.captchaWidgetId = global.grecaptcha?.render(divId, {
|
||||
sitekey: publicKey,
|
||||
callback: this.props.onCaptchaResponse,
|
||||
});
|
||||
@ -113,7 +113,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
|
||||
this.renderRecaptcha(DIV_ID);
|
||||
// clear error if re-rendered
|
||||
this.setState({
|
||||
errorText: null,
|
||||
errorText: undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
@ -123,7 +123,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let error = null;
|
||||
let error: JSX.Element | undefined;
|
||||
if (this.state.errorText) {
|
||||
error = <div className="error">{this.state.errorText}</div>;
|
||||
}
|
||||
|
@ -50,12 +50,12 @@ class EmailField extends PureComponent<IProps> {
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelRequired),
|
||||
invalid: () => _t(this.props.labelRequired!),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t(this.props.labelInvalid),
|
||||
invalid: () => _t(this.props.labelInvalid!),
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -80,7 +80,7 @@ class EmailField extends PureComponent<IProps> {
|
||||
id={this.props.id}
|
||||
ref={this.props.fieldRef}
|
||||
type="text"
|
||||
label={_t(this.props.label)}
|
||||
label={_t(this.props.label!)}
|
||||
value={this.props.value}
|
||||
autoFocus={this.props.autoFocus}
|
||||
onChange={this.props.onChange}
|
||||
|
@ -36,7 +36,7 @@ interface IProps {
|
||||
phoneNumber: string;
|
||||
|
||||
serverConfig: ValidatedServerConfig;
|
||||
loginIncorrect?: boolean;
|
||||
loginIncorrect: boolean;
|
||||
disableSubmit?: boolean;
|
||||
busy?: boolean;
|
||||
|
||||
@ -67,9 +67,9 @@ const enum LoginField {
|
||||
* The email/username/phone fields are fully-controlled, the password field is not.
|
||||
*/
|
||||
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
private [LoginField.Email]: Field;
|
||||
private [LoginField.Phone]: Field;
|
||||
private [LoginField.MatrixId]: Field;
|
||||
private [LoginField.Email]: Field | null;
|
||||
private [LoginField.Phone]: Field | null;
|
||||
private [LoginField.MatrixId]: Field | null;
|
||||
|
||||
public static defaultProps = {
|
||||
onUsernameChanged: function () {},
|
||||
@ -93,7 +93,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
private onForgotPasswordClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onForgotPasswordClick();
|
||||
this.props.onForgotPasswordClick?.();
|
||||
};
|
||||
|
||||
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 => {
|
||||
this.props.onUsernameChanged(ev.target.value);
|
||||
this.props.onUsernameChanged?.(ev.target.value);
|
||||
};
|
||||
|
||||
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 => {
|
||||
const loginType = ev.target.value as IState["loginType"];
|
||||
this.setState({ loginType });
|
||||
this.props.onUsernameChanged(""); // Reset because email and username use the same state
|
||||
this.props.onUsernameChanged?.(""); // Reset because email and username use the same state
|
||||
};
|
||||
|
||||
private onPhoneCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
||||
this.props.onPhoneCountryChanged(country.iso2);
|
||||
this.props.onPhoneCountryChanged?.(country.iso2);
|
||||
};
|
||||
|
||||
private onPhoneNumberChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
this.props.onPhoneNumberChanged?.(ev.target.value);
|
||||
};
|
||||
|
||||
private onPasswordChanged = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
@ -199,7 +199,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private markFieldValid(fieldID: LoginField, valid: boolean): void {
|
||||
private markFieldValid(fieldID: LoginField, valid?: boolean): void {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
@ -368,7 +368,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let forgotPasswordJsx;
|
||||
let forgotPasswordJsx: JSX.Element | undefined;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
|
@ -34,7 +34,7 @@ interface IProps {}
|
||||
export default class Welcome extends React.PureComponent<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
const pagesConfig = SdkConfig.getObject("embedded_pages");
|
||||
let pageUrl = null;
|
||||
let pageUrl!: string;
|
||||
if (pagesConfig) {
|
||||
pageUrl = pagesConfig.get("welcome_url");
|
||||
}
|
||||
|
@ -158,12 +158,12 @@ const BeaconViewDialog: React.FC<IProps> = ({ initialFocusedBeacon, roomId, matr
|
||||
)}
|
||||
{mapDisplayError && <MapError error={mapDisplayError.message as LocationShareError} isMinimised />}
|
||||
{!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>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={onFinished}
|
||||
data-test-id="beacon-view-dialog-fallback-close"
|
||||
data-testid="beacon-view-dialog-fallback-close"
|
||||
>
|
||||
{_t("Close")}
|
||||
</AccessibleButton>
|
||||
@ -179,7 +179,7 @@ const BeaconViewDialog: React.FC<IProps> = ({ initialFocusedBeacon, roomId, matr
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
data-test-id="beacon-view-dialog-open-sidebar"
|
||||
data-testid="beacon-view-dialog-open-sidebar"
|
||||
className="mx_BeaconViewDialog_viewListButton"
|
||||
>
|
||||
<LiveLocationIcon height={12} />
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
import React, { useContext } from "react";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||
import { ChevronFace } from "../../structures/ContextMenu";
|
||||
@ -34,6 +35,8 @@ import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
app: IApp;
|
||||
@ -45,7 +48,7 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
onEditClick?(): void;
|
||||
}
|
||||
|
||||
const WidgetContextMenu: React.FC<IProps> = ({
|
||||
export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
onFinished,
|
||||
app,
|
||||
userWidget,
|
||||
@ -158,24 +161,31 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
const isLocalWidget = WidgetType.JITSI.matches(app.type);
|
||||
let revokeButton;
|
||||
if (!userWidget && !isLocalWidget && isAllowedWidget) {
|
||||
const onRevokeClick = (): void => {
|
||||
logger.info("Revoking permission for widget to load: " + app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
if (app.eventId !== undefined) current[app.eventId] = false;
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch((err) => {
|
||||
logger.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
|
||||
|
||||
revokeButton = <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;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
onFinished();
|
||||
};
|
||||
@ -207,5 +217,3 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetContextMenu;
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,14 +15,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
||||
interface IProps {
|
||||
export interface AskInviteAnywayDialogProps {
|
||||
unknownProfileUsers: Array<{
|
||||
userId: string;
|
||||
errorText: string;
|
||||
@ -31,57 +32,58 @@ interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||
private onInviteClicked = (): void => {
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
export default function AskInviteAnywayDialog({
|
||||
onFinished,
|
||||
onGiveUp,
|
||||
onInviteAnyways,
|
||||
unknownProfileUsers,
|
||||
}: AskInviteAnywayDialogProps): JSX.Element {
|
||||
const onInviteClicked = useCallback((): void => {
|
||||
onInviteAnyways();
|
||||
onFinished(true);
|
||||
}, [onInviteAnyways, onFinished]);
|
||||
|
||||
private onInviteNeverWarnClicked = (): void => {
|
||||
const onInviteNeverWarnClicked = useCallback((): void => {
|
||||
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
onInviteAnyways();
|
||||
onFinished(true);
|
||||
}, [onInviteAnyways, onFinished]);
|
||||
|
||||
private onGiveUpClicked = (): void => {
|
||||
this.props.onGiveUp();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
const onGiveUpClicked = useCallback((): void => {
|
||||
onGiveUp();
|
||||
onFinished(false);
|
||||
}, [onGiveUp, onFinished]);
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const errorList = this.props.unknownProfileUsers.map((address) => (
|
||||
<li key={address.userId}>
|
||||
{address.userId}: {address.errorText}
|
||||
</li>
|
||||
));
|
||||
const errorList = unknownProfileUsers.map((address) => (
|
||||
<li key={address.userId}>
|
||||
{address.userId}: {address.errorText}
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_RetryInvitesDialog"
|
||||
onFinished={this.onGiveUpClicked}
|
||||
title={_t("The following users may not exist")}
|
||||
contentId="mx_Dialog_content"
|
||||
>
|
||||
<div id="mx_Dialog_content">
|
||||
<p>
|
||||
{_t(
|
||||
"Unable to find profiles for the Matrix IDs listed below - " +
|
||||
"would you like to invite them anyway?",
|
||||
)}
|
||||
</p>
|
||||
<ul>{errorList}</ul>
|
||||
</div>
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_RetryInvitesDialog"
|
||||
onFinished={onGiveUpClicked}
|
||||
title={_t("The following users may not exist")}
|
||||
contentId="mx_Dialog_content"
|
||||
>
|
||||
<div id="mx_Dialog_content">
|
||||
<p>
|
||||
{_t(
|
||||
"Unable to find profiles for the Matrix IDs listed below - " +
|
||||
"would you like to invite them anyway?",
|
||||
)}
|
||||
</p>
|
||||
<ul>{errorList}</ul>
|
||||
</div>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onGiveUpClicked}>{_t("Close")}</button>
|
||||
<button onClick={this.onInviteNeverWarnClicked}>
|
||||
{_t("Invite anyway and never warn me again")}
|
||||
</button>
|
||||
<button onClick={this.onInviteClicked} autoFocus={true}>
|
||||
{_t("Invite anyway")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={onGiveUpClicked}>{_t("Close")}</button>
|
||||
<button onClick={onInviteNeverWarnClicked}>{_t("Invite anyway and never warn me again")}</button>
|
||||
<button onClick={onInviteClicked} autoFocus={true}>
|
||||
{_t("Invite anyway")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ interface IProps {
|
||||
interface IState {
|
||||
shouldLoadBackupStatus: boolean;
|
||||
loading: boolean;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
backupInfo: IKeyBackupInfo | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -55,7 +55,6 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
loading: shouldLoadBackupStatus,
|
||||
backupInfo: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
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
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
Modal.createDialog(RestoreKeyBackupDialog, null, null, /* priority = */ false, /* static = */ true);
|
||||
Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createDialogAsync(
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
|
||||
ComponentType<{}>
|
||||
>,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ import React, { useContext } from "react";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { useNotificationState } from "../../../../hooks/useRoomNotificationState";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { _t, _td } from "../../../../languageHandler";
|
||||
import { determineUnreadState } from "../../../../RoomNotifs";
|
||||
import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread";
|
||||
@ -39,22 +39,38 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme
|
||||
<h2>{_t("Room status")}</h2>
|
||||
<ul>
|
||||
<li>
|
||||
{_t("Room unread status: ")}
|
||||
<strong>{humanReadableNotificationColor(color)}</strong>
|
||||
{count > 0 && (
|
||||
<>
|
||||
{_t(", count:")} <strong>{count}</strong>
|
||||
</>
|
||||
{_t(
|
||||
"Room unread status: <strong>%(status)s</strong>, count: <strong>%(count)s</strong>",
|
||||
{
|
||||
status: humanReadableNotificationColor(color),
|
||||
count,
|
||||
},
|
||||
{
|
||||
strong: (sub) => <strong>{sub}</strong>,
|
||||
},
|
||||
)}
|
||||
</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>
|
||||
{_t("Room is ")}
|
||||
<strong>
|
||||
{cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")}
|
||||
</strong>
|
||||
{_t(
|
||||
cli.isRoomEncrypted(room.roomId!)
|
||||
? _td("Room is <strong>encrypted ✅</strong>")
|
||||
: _td("Room is <strong>not encrypted 🚨</strong>"),
|
||||
{},
|
||||
{
|
||||
strong: (sub) => <strong>{sub}</strong>,
|
||||
},
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
|
||||
import { IDialogProps } from "../IDialogProps";
|
||||
import { PollHistoryList } from "./PollHistoryList";
|
||||
import { PollHistoryFilter } from "./types";
|
||||
import { usePolls } from "./usePollHistory";
|
||||
import { usePollsWithRelations } from "./usePollHistory";
|
||||
import { useFetchPastPolls } from "./fetchPastPolls";
|
||||
|
||||
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
||||
roomId: string;
|
||||
@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
|
||||
const filterPolls =
|
||||
(filter: PollHistoryFilter) =>
|
||||
(poll: Poll): boolean =>
|
||||
(filter === "ACTIVE") !== poll.isEnded;
|
||||
// exclude polls while they are still loading
|
||||
// to avoid jitter in list
|
||||
!poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded;
|
||||
|
||||
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
|
||||
return [...polls.values()]
|
||||
.filter(filterPolls(filter))
|
||||
@ -43,18 +47,24 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
|
||||
};
|
||||
|
||||
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 [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
|
||||
|
||||
useEffect(() => {
|
||||
setPollStartEvents(filterAndSortPolls(polls, filter));
|
||||
}, [filter, polls]);
|
||||
const pollStartEvents = filterAndSortPolls(polls, filter);
|
||||
const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
|
||||
|
||||
return (
|
||||
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
||||
<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>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
@ -15,19 +15,41 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import PollListItem from "./PollListItem";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { FilterTabGroup } from "../../elements/FilterTabGroup";
|
||||
import InlineSpinner from "../../elements/InlineSpinner";
|
||||
import { PollHistoryFilter } from "./types";
|
||||
import { PollListItem } from "./PollListItem";
|
||||
import { PollListItemEnded } from "./PollListItemEnded";
|
||||
|
||||
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
|
||||
<div
|
||||
className={classNames("mx_PollHistoryList_loading", {
|
||||
mx_PollHistoryList_noResultsYet: noResultsYet,
|
||||
})}
|
||||
>
|
||||
<InlineSpinner />
|
||||
{_t("Loading polls")}
|
||||
</div>
|
||||
);
|
||||
|
||||
type PollHistoryListProps = {
|
||||
pollStartEvents: MatrixEvent[];
|
||||
polls: Map<string, Poll>;
|
||||
filter: PollHistoryFilter;
|
||||
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 (
|
||||
<div className="mx_PollHistoryList">
|
||||
<FilterTabGroup<PollHistoryFilter>
|
||||
@ -39,19 +61,30 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
|
||||
{ id: "ENDED", label: "Past polls" },
|
||||
]}
|
||||
/>
|
||||
{!!pollStartEvents.length ? (
|
||||
<ol className="mx_PollHistoryList_list">
|
||||
{pollStartEvents.map((pollStartEvent) => (
|
||||
<PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} />
|
||||
))}
|
||||
{!!pollStartEvents.length && (
|
||||
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
|
||||
{pollStartEvents.map((pollStartEvent) =>
|
||||
filter === "ACTIVE" ? (
|
||||
<PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} />
|
||||
) : (
|
||||
<PollListItemEnded
|
||||
key={pollStartEvent.getId()!}
|
||||
event={pollStartEvent}
|
||||
poll={polls.get(pollStartEvent.getId()!)!}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{isLoading && <LoadingPolls />}
|
||||
</ol>
|
||||
) : (
|
||||
)}
|
||||
{!pollStartEvents.length && !isLoading && (
|
||||
<span className="mx_PollHistoryList_noResults">
|
||||
{filter === "ACTIVE"
|
||||
? _t("There are no active polls in this room")
|
||||
: _t("There are no past polls in this room")}
|
||||
</span>
|
||||
)}
|
||||
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ interface Props {
|
||||
event: MatrixEvent;
|
||||
}
|
||||
|
||||
const PollListItem: React.FC<Props> = ({ event }) => {
|
||||
export const PollListItem: React.FC<Props> = ({ event }) => {
|
||||
const pollEvent = event.unstableExtensibleEvent as unknown as PollStartEvent;
|
||||
if (!pollEvent) {
|
||||
return null;
|
||||
@ -39,5 +39,3 @@ const PollListItem: React.FC<Props> = ({ event }) => {
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollListItem;
|
||||
|
127
src/components/views/dialogs/polls/PollListItemEnded.tsx
Normal file
127
src/components/views/dialogs/polls/PollListItemEnded.tsx
Normal 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>
|
||||
);
|
||||
};
|
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal file
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal 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 };
|
||||
};
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
|
||||
|
||||
/**
|
||||
* Get poll instances from a room
|
||||
* Updates to include new polls
|
||||
* @param roomId - id of room to retrieve polls for
|
||||
* @param matrixClient - client
|
||||
* @returns {Map<string, Poll>} - Map of Poll instances
|
||||
@ -37,9 +39,58 @@ export const usePolls = (
|
||||
throw new Error("Cannot find room");
|
||||
}
|
||||
|
||||
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls);
|
||||
|
||||
// @TODO(kerrya) watch polls for end events, trigger refiltering
|
||||
// copy room.polls map so changes can be detected
|
||||
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.polls));
|
||||
|
||||
return { polls };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all poll instances from a room
|
||||
* Fetch their responses (using cached poll responses)
|
||||
* Updates on:
|
||||
* - new polls added to room
|
||||
* - new responses added to polls
|
||||
* - changes to poll ended state
|
||||
* @param roomId - id of room to retrieve polls for
|
||||
* @param matrixClient - client
|
||||
* @returns {Map<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 };
|
||||
};
|
||||
|
@ -93,6 +93,7 @@ import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||
import { shouldShowFeedback } from "../../../../utils/Feedback";
|
||||
import RoomAvatar from "../../avatars/RoomAvatar";
|
||||
import { useFeatureEnabled } from "../../../../hooks/useSettings";
|
||||
import { filterBoolean } from "../../../../utils/arrays";
|
||||
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||
@ -173,13 +174,13 @@ const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResul
|
||||
publicRoom,
|
||||
section: Section.PublicRooms,
|
||||
filter: [Filter.PublicRooms],
|
||||
query: [
|
||||
query: filterBoolean([
|
||||
publicRoom.room_id.toLowerCase(),
|
||||
publicRoom.canonical_alias?.toLowerCase(),
|
||||
publicRoom.name?.toLowerCase(),
|
||||
sanitizeHtml(publicRoom.topic?.toLowerCase() ?? "", { allowedTags: [] }),
|
||||
...(publicRoom.aliases?.map((it) => it.toLowerCase()) || []),
|
||||
].filter(Boolean) as string[],
|
||||
]),
|
||||
});
|
||||
|
||||
const toRoomResult = (room: Room): IRoomResult => {
|
||||
|
@ -23,6 +23,7 @@ import classNames from "classnames";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@ -36,7 +37,7 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
@ -50,6 +51,7 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
@ -162,6 +164,9 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
private hasPermissionToLoad = (props: IProps): boolean => {
|
||||
if (this.usingLocalWidget()) return true;
|
||||
if (!props.room) return true; // user widgets always have permissions
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(this.props.app));
|
||||
if (opts.approved) return true;
|
||||
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);
|
||||
|
@ -32,7 +32,7 @@ interface IProps {
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit?: (value: string) => Promise<{} | void>;
|
||||
onSubmit: (value: string) => Promise<{} | void>;
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit?: boolean;
|
||||
@ -40,7 +40,7 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
errorString: string;
|
||||
errorString: string | null;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export default class EditableTextContainer extends React.Component<IProps, IStat
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorString: null,
|
||||
value: props.initialValue,
|
||||
value: props.initialValue ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24);
|
||||
const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24) ?? undefined;
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||
@ -31,13 +31,13 @@ export enum WarningKind {
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
isRoomEncrypted: boolean;
|
||||
isRoomEncrypted?: boolean;
|
||||
kind: WarningKind;
|
||||
}
|
||||
|
||||
export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element {
|
||||
if (!isRoomEncrypted) return null;
|
||||
if (EventIndexPeg.get()) return null;
|
||||
if (!isRoomEncrypted) return <></>;
|
||||
if (EventIndexPeg.get()) return <></>;
|
||||
|
||||
if (EventIndexPeg.error) {
|
||||
return (
|
||||
@ -69,8 +69,8 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
|
||||
const brand = SdkConfig.get("brand");
|
||||
const desktopBuilds = SdkConfig.getObject("desktop_builds");
|
||||
|
||||
let text = null;
|
||||
let logo = null;
|
||||
let text: ReactNode | undefined;
|
||||
let logo: JSX.Element | undefined;
|
||||
if (desktopBuilds.get("available")) {
|
||||
logo = <img src={desktopBuilds.get("logo")} />;
|
||||
const buildUrl = desktopBuilds.get("url");
|
||||
@ -116,7 +116,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
|
||||
// for safety
|
||||
if (!text) {
|
||||
logger.warn("Unknown desktop builds warning kind: ", kind);
|
||||
return null;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -26,7 +26,7 @@ interface IResult {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IRule<T, D = void> {
|
||||
interface IRule<T, D = undefined> {
|
||||
key: string;
|
||||
final?: 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,
|
||||
): Promise<IValidationResult> {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
valid: null,
|
||||
feedback: null,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
const derivedData: D | undefined = deriveData ? await deriveData.call(this, data) : undefined;
|
||||
// We know that if deriveData is set then D will not be undefined
|
||||
const derivedData: D = (await deriveData?.call(this, data)) as D;
|
||||
|
||||
const results: IResult[] = [];
|
||||
let valid = true;
|
||||
@ -149,10 +147,7 @@ export default function withValidation<T = void, D = void>({
|
||||
|
||||
// Hide feedback when not focused
|
||||
if (!focused) {
|
||||
return {
|
||||
valid,
|
||||
feedback: null,
|
||||
};
|
||||
return { valid };
|
||||
}
|
||||
|
||||
let details;
|
||||
|
@ -38,7 +38,7 @@ interface IProps {
|
||||
id: string;
|
||||
name: string;
|
||||
emojis: IEmoji[];
|
||||
selectedEmojis: Set<string>;
|
||||
selectedEmojis?: Set<string>;
|
||||
heightBefore: number;
|
||||
viewportHeight: number;
|
||||
scrollTop: number;
|
||||
|
@ -26,6 +26,7 @@ import Search from "./Search";
|
||||
import Preview from "./Preview";
|
||||
import QuickReactions from "./QuickReactions";
|
||||
import Category, { ICategory, CategoryKey } from "./Category";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 20;
|
||||
export const EMOJI_HEIGHT = 35;
|
||||
@ -62,13 +63,12 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
|
||||
this.state = {
|
||||
filter: "",
|
||||
previewEmoji: null,
|
||||
scrollTop: 0,
|
||||
viewportHeight: 280,
|
||||
};
|
||||
|
||||
// Convert recent emoji characters to emoji data, removing unknowns and duplicates
|
||||
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
|
||||
this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode))));
|
||||
this.memoizedDataByCategory = {
|
||||
recent: this.recentlyUsed,
|
||||
...DATA_BY_CATEGORY,
|
||||
@ -230,9 +230,9 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
private onHoverEmojiEnd = (emoji: IEmoji): void => {
|
||||
private onHoverEmojiEnd = (): void => {
|
||||
this.setState({
|
||||
previewEmoji: null,
|
||||
previewEmoji: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -42,9 +42,7 @@ interface IState {
|
||||
class QuickReactions extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: null,
|
||||
};
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
private onMouseEnter = (emoji: IEmoji): void => {
|
||||
@ -55,7 +53,7 @@ class QuickReactions extends React.Component<IProps, IState> {
|
||||
|
||||
private onMouseLeave = (): void => {
|
||||
this.setState({
|
||||
hover: null,
|
||||
hover: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -77,8 +77,8 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
||||
if (!this.props.reactions) {
|
||||
return {};
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || new Set<MatrixEvent>();
|
||||
const userId = MatrixClientPeg.get().getUserId()!;
|
||||
const myAnnotations = this.props.reactions.getAnnotationsBySender()?.[userId] ?? new Set<MatrixEvent>();
|
||||
return Object.fromEntries(
|
||||
[...myAnnotations]
|
||||
.filter((event) => !event.isRedacted())
|
||||
@ -97,9 +97,9 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
||||
this.props.onFinished();
|
||||
const myReactions = this.getReactions();
|
||||
if (myReactions.hasOwnProperty(reaction)) {
|
||||
if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return;
|
||||
if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return false;
|
||||
|
||||
MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId(), myReactions[reaction]);
|
||||
MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]);
|
||||
dis.dispatch<FocusComposerPayload>({
|
||||
action: Action.FocusAComposer,
|
||||
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.
|
||||
return false;
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), EventType.Reaction, {
|
||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
|
@ -32,7 +32,7 @@ class Search extends React.PureComponent<IProps> {
|
||||
|
||||
public componentDidMount(): void {
|
||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout
|
||||
window.setTimeout(() => this.inputRef.current.focus(), 0);
|
||||
window.setTimeout(() => this.inputRef.current?.focus(), 0);
|
||||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||
|
@ -29,7 +29,7 @@ interface Props {
|
||||
export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
|
||||
const [isEnabled, setEnabled] = useState(false);
|
||||
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" />
|
||||
<Heading className="mx_EnableLiveShare_heading" size="h3">
|
||||
{_t("Live location sharing")}
|
||||
@ -43,13 +43,13 @@ export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
|
||||
)}
|
||||
</p>
|
||||
<LabelledToggleSwitch
|
||||
data-test-id="enable-live-share-toggle"
|
||||
data-testid="enable-live-share-toggle"
|
||||
value={isEnabled}
|
||||
onChange={setEnabled}
|
||||
label={_t("Enable live location sharing")}
|
||||
/>
|
||||
<AccessibleButton
|
||||
data-test-id="enable-live-share-submit"
|
||||
data-testid="enable-live-share-submit"
|
||||
className="mx_EnableLiveShare_button"
|
||||
element="button"
|
||||
kind="primary"
|
||||
|
@ -62,7 +62,7 @@ const LiveDurationDropdown: React.FC<Props> = ({ timeout, onChange }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
id="live-duration"
|
||||
data-test-id="live-duration-dropdown"
|
||||
data-testid="live-duration-dropdown"
|
||||
label={getLabel(timeout)}
|
||||
value={timeout.toString()}
|
||||
onOptionChange={onOptionChange}
|
||||
|
@ -231,7 +231,7 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||
<LiveDurationDropdown onChange={this.onTimeoutChange} timeout={this.state.timeout} />
|
||||
)}
|
||||
<AccessibleButton
|
||||
data-test-id="location-picker-submit-button"
|
||||
data-testid="location-picker-submit-button"
|
||||
type="submit"
|
||||
element="button"
|
||||
kind="primary"
|
||||
|
@ -33,7 +33,7 @@ export interface MapErrorProps {
|
||||
|
||||
export const MapError: React.FC<MapErrorProps> = ({ error, isMinimised, className, onFinished, onClick }) => (
|
||||
<div
|
||||
data-test-id="map-rendering-error"
|
||||
data-testid="map-rendering-error"
|
||||
className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -19,6 +19,7 @@ import React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { Icon as BackIcon } from "../../../../res/img/element-icons/caret-left.svg";
|
||||
import { Icon as CloseIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
@ -32,7 +33,8 @@ const ShareDialogButtons: React.FC<Props> = ({ onBack, onCancel, displayBack })
|
||||
{displayBack && (
|
||||
<AccessibleButton
|
||||
className="mx_ShareDialogButtons_button left"
|
||||
data-test-id="share-dialog-buttons-back"
|
||||
data-testid="share-dialog-buttons-back"
|
||||
aria-label={_t("Back")}
|
||||
onClick={onBack}
|
||||
element="button"
|
||||
>
|
||||
@ -41,7 +43,8 @@ const ShareDialogButtons: React.FC<Props> = ({ onBack, onCancel, displayBack })
|
||||
)}
|
||||
<AccessibleButton
|
||||
className="mx_ShareDialogButtons_button right"
|
||||
data-test-id="share-dialog-buttons-cancel"
|
||||
data-testid="share-dialog-buttons-cancel"
|
||||
aria-label={_t("Close")}
|
||||
onClick={onCancel}
|
||||
element="button"
|
||||
>
|
||||
|
@ -73,7 +73,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this.settingWatcherRef);
|
||||
if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef);
|
||||
}
|
||||
|
||||
private onContextMenuOpenClick = (e: React.MouseEvent): void => {
|
||||
@ -89,7 +89,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
|
||||
private closeMenu = (): void => {
|
||||
this.setState({
|
||||
contextMenuPosition: null,
|
||||
contextMenuPosition: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@ -181,7 +181,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private renderJumpToDateMenu(): React.ReactElement {
|
||||
let contextMenu: JSX.Element;
|
||||
let contextMenu: JSX.Element | undefined;
|
||||
if (this.state.contextMenuPosition) {
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
|
@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
|
||||
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 { IBodyProps } from "./IBodyProps";
|
||||
|
||||
// A placeholder element for messages that could not be decrypted
|
||||
export default class DecryptionFailureBody extends React.Component<Partial<IBodyProps>> {
|
||||
public render(): ReactNode {
|
||||
return <div className="mx_DecryptionFailureBody mx_EventTile_content">{_t("Unable to decrypt message")}</div>;
|
||||
}
|
||||
function getErrorMessage(mxEvent?: MatrixEvent): string {
|
||||
return mxEvent?.isEncryptedDisabledForUnverifiedDevices
|
||||
? _t("The sender has blocked you from receiving this message")
|
||||
: _t("Unable to decrypt message");
|
||||
}
|
||||
|
||||
// A placeholder element for messages that could not be decrypted
|
||||
export function DecryptionFailureBody({ mxEvent }: Partial<IBodyProps>): JSX.Element {
|
||||
return <div className="mx_DecryptionFailureBody mx_EventTile_content">{getErrorMessage(mxEvent)}</div>;
|
||||
}
|
||||
|
@ -191,6 +191,13 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
|
||||
{this.props.timestamp}
|
||||
</div>
|
||||
);
|
||||
} else if (hangupReason === CallErrorCode.AnsweredElsewhere) {
|
||||
return (
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{_t("Answered elsewhere")}
|
||||
{this.props.timestamp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let reason;
|
||||
|
@ -182,12 +182,14 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||
private addListeners(): void {
|
||||
this.state.poll?.on(PollEvent.Responses, this.onResponsesChange);
|
||||
this.state.poll?.on(PollEvent.End, this.onRelationsChange);
|
||||
this.state.poll?.on(PollEvent.UndecryptableRelations, this.render.bind(this));
|
||||
}
|
||||
|
||||
private removeListeners(): void {
|
||||
if (this.state.poll) {
|
||||
this.state.poll.off(PollEvent.Responses, this.onResponsesChange);
|
||||
this.state.poll.off(PollEvent.End, this.onRelationsChange);
|
||||
this.state.poll.off(PollEvent.UndecryptableRelations, this.render.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,7 +299,9 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||
const showResults = poll.isEnded || (disclosed && myVote !== undefined);
|
||||
|
||||
let totalText: string;
|
||||
if (poll.isEnded) {
|
||||
if (showResults && poll.undecryptableRelationsCount) {
|
||||
totalText = _t("Due to decryption errors, some votes may not be counted");
|
||||
} else if (poll.isEnded) {
|
||||
totalText = _t("Final result based on %(count)s votes", { count: totalVotes });
|
||||
} else if (!disclosed) {
|
||||
totalText = _t("Results will be visible when the poll is ended");
|
||||
@ -384,7 +388,7 @@ export function allVotes(voteRelations: Relations): Array<UserVote> {
|
||||
* @param {string?} selected Local echo selected option for the userId
|
||||
* @returns a Map of user ID to their vote info
|
||||
*/
|
||||
function collectUserVotes(
|
||||
export function collectUserVotes(
|
||||
userResponses: Array<UserVote>,
|
||||
userId?: string | null | undefined,
|
||||
selected?: string | null | undefined,
|
||||
@ -405,7 +409,7 @@ function collectUserVotes(
|
||||
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>();
|
||||
|
||||
for (const response of userVotes.values()) {
|
||||
|
@ -21,7 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Icon as PollIcon } from "../../../../res/img/element-icons/room/composer/poll.svg";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { textForEvent } from "../../../TextForEvent";
|
||||
import { Caption } from "../typography/Caption";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import MPollBody from "./MPollBody";
|
||||
|
||||
@ -105,5 +107,10 @@ export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...pro
|
||||
);
|
||||
}
|
||||
|
||||
return <MPollBody mxEvent={pollStartEvent} {...props} />;
|
||||
return (
|
||||
<div>
|
||||
<Caption>{_t("Ended a poll")}</Caption>
|
||||
<MPollBody mxEvent={pollStartEvent} {...props} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ import { MPollEndBody } from "./MPollEndBody";
|
||||
import MLocationBody from "./MLocationBody";
|
||||
import MjolnirBody from "./MjolnirBody";
|
||||
import MBeaconBody from "./MBeaconBody";
|
||||
import DecryptionFailureBody from "./DecryptionFailureBody";
|
||||
import { DecryptionFailureBody } from "./DecryptionFailureBody";
|
||||
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
|
||||
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
|
||||
|
||||
|
@ -40,7 +40,7 @@ import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "./PinnedMessagesCard";
|
||||
|
@ -24,7 +24,7 @@ import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useWidgets } from "./RoomSummaryCard";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
|
@ -399,7 +399,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
|
||||
return (
|
||||
<div className="mx_AliasSettings">
|
||||
<SettingsFieldset
|
||||
data-test-id="published-address-fieldset"
|
||||
data-testid="published-address-fieldset"
|
||||
legend={_t("Published Addresses")}
|
||||
description={
|
||||
<>
|
||||
@ -450,7 +450,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
|
||||
/>
|
||||
</SettingsFieldset>
|
||||
<SettingsFieldset
|
||||
data-test-id="local-address-fieldset"
|
||||
data-testid="local-address-fieldset"
|
||||
legend={_t("Local Addresses")}
|
||||
description={
|
||||
isSpaceRoom
|
||||
|
@ -35,7 +35,7 @@ import { Layout } from "../../../settings/enums/Layout";
|
||||
import { formatTime } from "../../../DateUtils";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import DecryptionFailureBody from "../messages/DecryptionFailureBody";
|
||||
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
|
||||
import { E2EState } from "./E2EIcon";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
@ -1270,7 +1270,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{this.props.mxEvent.isRedacted() ? (
|
||||
<RedactedBody mxEvent={this.props.mxEvent} />
|
||||
) : this.props.mxEvent.isDecryptionFailure() ? (
|
||||
<DecryptionFailureBody />
|
||||
<DecryptionFailureBody mxEvent={this.props.mxEvent} />
|
||||
) : (
|
||||
MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent)
|
||||
)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,8 +21,9 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import useHover from "../../../hooks/useHover";
|
||||
|
||||
interface IProps {
|
||||
interface ExtraTileProps {
|
||||
isMinimized: boolean;
|
||||
isSelected: boolean;
|
||||
displayName: string;
|
||||
@ -31,83 +32,68 @@ interface IProps {
|
||||
onClick: (ev: ButtonEvent) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
export default function ExtraTile({
|
||||
isSelected,
|
||||
isMinimized,
|
||||
notificationState,
|
||||
displayName,
|
||||
onClick,
|
||||
avatar,
|
||||
}: ExtraTileProps): JSX.Element {
|
||||
const [, { onMouseOver, onMouseLeave }] = useHover(() => false);
|
||||
|
||||
export default class ExtraTile extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
// XXX: We copy classes because it's easier
|
||||
const classes = classNames({
|
||||
mx_ExtraTile: true,
|
||||
mx_RoomTile: true,
|
||||
mx_RoomTile_selected: isSelected,
|
||||
mx_RoomTile_minimized: isMinimized,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
let badge: JSX.Element | null = null;
|
||||
if (notificationState) {
|
||||
badge = <NotificationBadge notification={notificationState} forceCount={false} />;
|
||||
}
|
||||
|
||||
private onTileMouseEnter = (): void => {
|
||||
this.setState({ hover: true });
|
||||
};
|
||||
let name = displayName;
|
||||
if (typeof name !== "string") name = "";
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
private onTileMouseLeave = (): void => {
|
||||
this.setState({ hover: false });
|
||||
};
|
||||
const nameClasses = classNames({
|
||||
mx_RoomTile_title: true,
|
||||
mx_RoomTile_titleHasUnreadEvents: notificationState?.isUnread,
|
||||
});
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// XXX: We copy classes because it's easier
|
||||
const classes = classNames({
|
||||
mx_ExtraTile: true,
|
||||
mx_RoomTile: true,
|
||||
mx_RoomTile_selected: this.props.isSelected,
|
||||
mx_RoomTile_minimized: this.props.isMinimized,
|
||||
});
|
||||
let nameContainer: JSX.Element | null = (
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (isMinimized) nameContainer = null;
|
||||
|
||||
let badge;
|
||||
if (this.props.notificationState) {
|
||||
badge = <NotificationBadge notification={this.props.notificationState} forceCount={false} />;
|
||||
}
|
||||
let Button = RovingAccessibleButton;
|
||||
if (isMinimized) {
|
||||
Button = RovingAccessibleTooltipButton;
|
||||
}
|
||||
|
||||
let name = this.props.displayName;
|
||||
if (typeof name !== "string") name = "";
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
const nameClasses = classNames({
|
||||
mx_RoomTile_title: true,
|
||||
mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{name}
|
||||
return (
|
||||
<Button
|
||||
className={classes}
|
||||
onMouseEnter={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={onClick}
|
||||
role="treeitem"
|
||||
title={isMinimized ? name : undefined}
|
||||
>
|
||||
<div className="mx_RoomTile_avatarContainer">{avatar}</div>
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
{nameContainer}
|
||||
<div className="mx_RoomTile_badgeContainer">{badge}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
|
||||
private waitForOwnMember(): void {
|
||||
// If we have the member already, do that
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!);
|
||||
if (me) {
|
||||
this.setState({ me });
|
||||
return;
|
||||
@ -250,14 +250,14 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
// The members should already be loading, and loadMembersIfNeeded
|
||||
// will return the promise for the existing operation
|
||||
this.props.room.loadMembersIfNeeded().then(() => {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()!);
|
||||
this.setState({ me });
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
|
||||
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
|
||||
@ -268,12 +268,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
private onTombstoneClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
|
||||
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
|
||||
const replacementRoomId = this.context.tombstone?.getContent()["replacement_room"];
|
||||
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
|
||||
let createEventId = null;
|
||||
let createEventId: string | undefined;
|
||||
if (replacementRoom) {
|
||||
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
|
||||
if (createEvent?.getId()) createEventId = createEvent.getId();
|
||||
}
|
||||
|
||||
const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")];
|
||||
@ -408,7 +408,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
|
||||
private onRecordingEndingSoon = ({ secondsLeft }: { secondsLeft: number }): void => {
|
||||
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
||||
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
||||
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: undefined }), 3000);
|
||||
};
|
||||
|
||||
private setStickerPickerOpen = (isStickerPickerOpen: boolean): void => {
|
||||
|
@ -38,7 +38,7 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) {
|
||||
return null;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
||||
@ -54,8 +54,8 @@ export function StatelessNotificationBadge({ symbol, count, color, ...props }: P
|
||||
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
|
||||
mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
|
||||
mx_NotificationBadge_dot: isEmptyBadge,
|
||||
mx_NotificationBadge_2char: symbol?.length > 0 && symbol?.length < 3,
|
||||
mx_NotificationBadge_3char: symbol?.length > 2,
|
||||
mx_NotificationBadge_2char: symbol && symbol.length > 0 && symbol.length < 3,
|
||||
mx_NotificationBadge_3char: symbol && symbol.length > 2,
|
||||
});
|
||||
|
||||
if (props.onClick) {
|
||||
|
@ -62,7 +62,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
||||
eventId: string,
|
||||
relationType: RelationType | string,
|
||||
eventType: EventType | string,
|
||||
): Relations => {
|
||||
): Relations | undefined => {
|
||||
if (eventId === this.props.event.getId()) {
|
||||
return this.relations.get(relationType)?.get(eventType);
|
||||
}
|
||||
@ -71,7 +71,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
const sender = this.props.event.getSender();
|
||||
|
||||
let unpinButton = null;
|
||||
let unpinButton: JSX.Element | undefined;
|
||||
if (this.props.onUnpinClicked) {
|
||||
unpinButton = (
|
||||
<AccessibleTooltipButton
|
||||
|
@ -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) {
|
||||
return _t("%(members)s and more", {
|
||||
members: members.join(", "),
|
||||
@ -78,8 +78,6 @@ export function readReceiptTooltip(members: string[], hasMore: boolean): string
|
||||
});
|
||||
} else if (members.length) {
|
||||
return members[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +132,7 @@ export function ReadReceiptGroup({
|
||||
const { hidden, position } = determineAvatarPosition(index, maxAvatars);
|
||||
|
||||
const userId = receipt.userId;
|
||||
let readReceiptInfo: IReadReceiptInfo;
|
||||
let readReceiptInfo: IReadReceiptInfo | undefined;
|
||||
|
||||
if (readReceiptMap) {
|
||||
readReceiptInfo = readReceiptMap[userId];
|
||||
@ -161,7 +159,7 @@ export function ReadReceiptGroup({
|
||||
})
|
||||
.reverse();
|
||||
|
||||
let remText: JSX.Element;
|
||||
let remText: JSX.Element | undefined;
|
||||
const remainder = readReceipts.length - maxAvatars;
|
||||
if (remainder > 0) {
|
||||
remText = (
|
||||
|
@ -133,7 +133,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
|
||||
|
||||
target.top = 0;
|
||||
target.right = 0;
|
||||
target.parent = null;
|
||||
target.parent = undefined;
|
||||
return target;
|
||||
}
|
||||
// this is the mx_ReadReceiptsGroup
|
||||
@ -146,7 +146,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
|
||||
|
||||
target.top = 0;
|
||||
target.right = 0;
|
||||
target.parent = null;
|
||||
target.parent = undefined;
|
||||
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
|
||||
-READ_AVATAR_SIZE;
|
||||
|
||||
const startStyles = [];
|
||||
const startStyles: IReadReceiptMarkerStyle[] = [];
|
||||
if (oldInfo?.right) {
|
||||
startStyles.push({
|
||||
top: oldPosition - newPosition,
|
||||
@ -210,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
|
||||
return (
|
||||
<NodeAnimator startStyles={this.state.startStyles}>
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
member={this.props.member ?? null}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
aria-hidden="true"
|
||||
aria-live="off"
|
||||
|
@ -168,7 +168,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||
...this.props,
|
||||
|
||||
// overrides
|
||||
ref: null,
|
||||
ref: undefined,
|
||||
showUrlPreview: false,
|
||||
overrideBodyTypes: msgtypeOverrides,
|
||||
overrideEventTypes: evOverrides,
|
||||
|
@ -37,5 +37,5 @@ export function RoomContextDetails<T extends keyof ReactHTML>({ room, component,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return <></>;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
|
||||
roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
|
||||
}
|
||||
|
||||
let members: JSX.Element;
|
||||
let members: JSX.Element | undefined;
|
||||
if (membership === "invite" && summary) {
|
||||
// Don't trust local state and instead use the summary API
|
||||
members = (
|
||||
|
@ -117,7 +117,7 @@ const auxButtonContextMenuPosition = (handle: RefObject<HTMLDivElement>): MenuPr
|
||||
|
||||
const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = defaultDispatcher }) => {
|
||||
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;
|
||||
});
|
||||
|
||||
@ -125,7 +125,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
|
||||
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
|
||||
|
||||
if (activeSpace && (showCreateRooms || showInviteUsers)) {
|
||||
let contextMenu: JSX.Element;
|
||||
let contextMenu: JSX.Element | undefined;
|
||||
if (menuDisplayed) {
|
||||
const canInvite = shouldShowSpaceInvite(activeSpace);
|
||||
|
||||
@ -208,7 +208,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
|
||||
|
||||
const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
||||
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;
|
||||
});
|
||||
|
||||
@ -216,11 +216,11 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
|
||||
|
||||
let contextMenuContent: JSX.Element | null = null;
|
||||
let contextMenuContent: JSX.Element | undefined;
|
||||
if (menuDisplayed && activeSpace) {
|
||||
const canAddRooms = activeSpace.currentState.maySendStateEvent(
|
||||
EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId(),
|
||||
MatrixClientPeg.get().getUserId()!,
|
||||
);
|
||||
|
||||
contextMenuContent = (
|
||||
@ -469,13 +469,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
SettingsStore.unwatchSetting(this.favouriteMessageWatcher);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = (): void => {
|
||||
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);
|
||||
|
||||
return TAG_ORDER.map((orderedTagId) => {
|
||||
let extraTiles = null;
|
||||
let extraTiles: ReactComponentElement<typeof ExtraTile>[] | undefined;
|
||||
if (orderedTagId === DefaultTagID.Suggested) {
|
||||
extraTiles = this.renderSuggestedRooms();
|
||||
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {
|
||||
|
@ -137,12 +137,10 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
}
|
||||
}, [closeMainMenu, canShowMainMenu, mainMenuDisplayed]);
|
||||
|
||||
const spaceName = useTypedEventEmitterState(activeSpace, RoomEvent.Name, () => activeSpace?.name);
|
||||
const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name);
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisibilityChange) {
|
||||
onVisibilityChange();
|
||||
}
|
||||
onVisibilityChange?.();
|
||||
}, [onVisibilityChange]);
|
||||
|
||||
const canExploreRooms = shouldShowComponent(UIComponent.ExploreRooms);
|
||||
@ -151,7 +149,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
|
||||
const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent(
|
||||
EventType.SpaceChild,
|
||||
cli.getUserId(),
|
||||
cli.getUserId()!,
|
||||
);
|
||||
const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms;
|
||||
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.
|
||||
const canShowPlusMenu = canCreateRooms || canExploreRooms || canCreateSpaces || activeSpace;
|
||||
|
||||
let contextMenu: JSX.Element;
|
||||
let contextMenu: JSX.Element | undefined;
|
||||
if (mainMenuDisplayed && mainMenuHandle.current) {
|
||||
let ContextMenuComponent;
|
||||
if (activeSpace) {
|
||||
@ -179,7 +177,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
/>
|
||||
);
|
||||
} else if (plusMenuDisplayed && activeSpace) {
|
||||
let inviteOption: JSX.Element;
|
||||
let inviteOption: JSX.Element | undefined;
|
||||
if (shouldShowSpaceInvite(activeSpace)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
@ -195,8 +193,8 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
);
|
||||
}
|
||||
|
||||
let newRoomOptions: JSX.Element;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
|
||||
let newRoomOptions: JSX.Element | undefined;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()!)) {
|
||||
newRoomOptions = (
|
||||
<>
|
||||
<IconizedContextMenuOption
|
||||
@ -265,7 +263,9 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
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 && (
|
||||
<IconizedContextMenuOption
|
||||
@ -278,7 +278,11 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
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 />
|
||||
</IconizedContextMenuOption>
|
||||
@ -287,8 +291,8 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
} else if (plusMenuDisplayed) {
|
||||
let newRoomOpts: JSX.Element;
|
||||
let joinRoomOpt: JSX.Element;
|
||||
let newRoomOpts: JSX.Element | undefined;
|
||||
let joinRoomOpt: JSX.Element | undefined;
|
||||
|
||||
if (canCreateRooms) {
|
||||
newRoomOpts = (
|
||||
@ -366,7 +370,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
}
|
||||
|
||||
let title: string;
|
||||
if (activeSpace) {
|
||||
if (activeSpace && spaceName) {
|
||||
title = spaceName;
|
||||
} else {
|
||||
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
@ -76,6 +76,12 @@ interface IProps {
|
||||
|
||||
canPreview?: 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;
|
||||
|
||||
loading?: boolean;
|
||||
@ -215,25 +221,27 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
return { memberName, reason };
|
||||
}
|
||||
|
||||
private joinRule(): JoinRule {
|
||||
return this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")
|
||||
?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
private joinRule(): JoinRule | null {
|
||||
return (
|
||||
this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")
|
||||
?.getContent<IJoinRuleEventContent>().join_rule ?? null
|
||||
);
|
||||
}
|
||||
|
||||
private getMyMember(): RoomMember {
|
||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||
private getMyMember(): RoomMember | null {
|
||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId()!) ?? null;
|
||||
}
|
||||
|
||||
private getInviteMember(): RoomMember {
|
||||
private getInviteMember(): RoomMember | null {
|
||||
const { room } = this.props;
|
||||
if (!room) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const myUserId = MatrixClientPeg.get().getUserId()!;
|
||||
const inviteEvent = room.currentState.getMember(myUserId);
|
||||
if (!inviteEvent) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const inviterUserId = inviteEvent.events.member.getSender();
|
||||
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;
|
||||
|
||||
let showSpinner = false;
|
||||
let title;
|
||||
let subTitle;
|
||||
let reasonElement;
|
||||
let primaryActionHandler;
|
||||
let primaryActionLabel;
|
||||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
let footer;
|
||||
const extraComponents = [];
|
||||
let title: string | undefined;
|
||||
let subTitle: string | ReactNode[] | undefined;
|
||||
let reasonElement: JSX.Element | undefined;
|
||||
let primaryActionHandler: (() => void) | undefined;
|
||||
let primaryActionLabel: string | undefined;
|
||||
let secondaryActionHandler: (() => void) | undefined;
|
||||
let secondaryActionLabel: string | undefined;
|
||||
let footer: JSX.Element | undefined;
|
||||
const extraComponents: JSX.Element[] = [];
|
||||
|
||||
const messageCase = this.getMessageCase();
|
||||
switch (messageCase) {
|
||||
@ -310,18 +318,14 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
const opts: RoomPreviewOpts = { canJoin: false };
|
||||
if (this.props.room?.roomId) {
|
||||
ModuleRunner.instance.invoke(
|
||||
RoomViewLifecycle.PreviewRoomNotLoggedIn,
|
||||
opts,
|
||||
this.props.room.roomId,
|
||||
);
|
||||
if (this.props.roomId) {
|
||||
ModuleRunner.instance.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.roomId);
|
||||
}
|
||||
if (opts.canJoin) {
|
||||
title = _t("Join the room to participate");
|
||||
primaryActionLabel = _t("Join");
|
||||
primaryActionHandler = () => {
|
||||
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.room.roomId);
|
||||
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.roomId);
|
||||
};
|
||||
} else {
|
||||
title = _t("Join the conversation with an account");
|
||||
@ -349,7 +353,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
} else {
|
||||
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) {
|
||||
primaryActionLabel = _t("Forget this space");
|
||||
@ -374,7 +378,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
} else {
|
||||
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) {
|
||||
primaryActionLabel = _t("Forget this space");
|
||||
} else {
|
||||
@ -497,7 +501,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
primaryActionLabel = _t("Accept");
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const myUserId = MatrixClientPeg.get().getUserId()!;
|
||||
const member = this.props.room?.currentState.getMember(myUserId);
|
||||
const memberEventContent = member?.events.member?.getContent();
|
||||
|
||||
|
@ -22,7 +22,7 @@ import { Dispatcher } from "flux";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
import * as React from "react";
|
||||
import { ComponentType, createRef, ReactComponentElement } from "react";
|
||||
import { ComponentType, createRef, ReactComponentElement, ReactNode } from "react";
|
||||
|
||||
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
@ -82,7 +82,7 @@ interface IProps {
|
||||
alwaysVisible?: boolean;
|
||||
forceExpanded?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[] | null;
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ interface ResizeDelta {
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
||||
|
||||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
contextMenuPosition?: PartialDOMRect;
|
||||
isResizing: boolean;
|
||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
@ -123,7 +123,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
this.heightAtStart = 0;
|
||||
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
isResizing: false,
|
||||
isExpanded: !this.layout.isCollapsed,
|
||||
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 {
|
||||
if (this.props.extraTiles) {
|
||||
return this.props.extraTiles;
|
||||
}
|
||||
return null;
|
||||
return this.props.extraTiles ?? null;
|
||||
}
|
||||
|
||||
private get numTiles(): number {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -390,7 +386,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onCloseMenu = (): void => {
|
||||
this.setState({ contextMenuPosition: null });
|
||||
this.setState({ contextMenuPosition: undefined });
|
||||
};
|
||||
|
||||
private onUnreadFirstChanged = (): void => {
|
||||
@ -506,7 +502,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
// On ArrowLeft go to the sublist header
|
||||
case KeyBindingAction.ArrowLeft:
|
||||
ev.stopPropagation();
|
||||
this.headerButton.current.focus();
|
||||
this.headerButton.current?.focus();
|
||||
break;
|
||||
// Consume ArrowRight so it doesn't cause focus to get sent to composer
|
||||
case KeyBindingAction.ArrowRight:
|
||||
@ -557,10 +553,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private renderMenu(): React.ReactElement {
|
||||
private renderMenu(): ReactNode {
|
||||
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) {
|
||||
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
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.
|
||||
let otherSections = null;
|
||||
let otherSections: JSX.Element | undefined;
|
||||
if (this.props.tagId !== DefaultTagID.Invite) {
|
||||
otherSections = (
|
||||
<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) {
|
||||
const AuxButtonComponent = this.props.AuxButtonComponent;
|
||||
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
|
||||
@ -747,7 +743,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
mx_RoomSublist_hidden: hidden,
|
||||
});
|
||||
|
||||
let content = null;
|
||||
let content: JSX.Element | undefined;
|
||||
if (this.state.roomsLoading) {
|
||||
content = <div className="mx_RoomSublist_skeletonUI" />;
|
||||
} 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
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
let showNButton = null;
|
||||
let showNButton: JSX.Element | undefined;
|
||||
const hasMoreSlidingSync =
|
||||
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;
|
||||
}
|
||||
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;
|
||||
showNButton = (
|
||||
<RovingAccessibleButton
|
||||
@ -804,7 +800,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
|
||||
// we have all tiles visible - add a button to 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;
|
||||
showNButton = (
|
||||
<RovingAccessibleButton
|
||||
|
@ -72,13 +72,13 @@ export default class SearchBar extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private searchIfQuery(): void {
|
||||
if (this.searchTerm.current.value) {
|
||||
if (this.searchTerm.current?.value) {
|
||||
this.onSearch();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
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) {
|
||||
return; // errored
|
||||
}
|
||||
@ -425,7 +430,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
||||
|
||||
const prom = doMaybeLocalRoomAction(
|
||||
roomId,
|
||||
(actualRoomId: string) => this.props.mxClient.sendMessage(actualRoomId, threadId, content),
|
||||
(actualRoomId: string) => this.props.mxClient.sendMessage(actualRoomId, threadId ?? null, content!),
|
||||
this.props.mxClient,
|
||||
);
|
||||
if (replyToEvent) {
|
||||
@ -439,7 +444,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
||||
}
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CHAT_EFFECTS.forEach((effect) => {
|
||||
if (containsEmoji(content, effect.emojis)) {
|
||||
if (containsEmoji(content!, effect.emojis)) {
|
||||
// For initial threads launch, chat effects are disabled
|
||||
// see #19731
|
||||
const isNotThread = this.props.relation?.rel_type !== THREAD_RELATION_TYPE.name;
|
||||
|
@ -52,9 +52,9 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
imError: string;
|
||||
stickerpickerWidget: IWidgetEvent;
|
||||
widgetId: string;
|
||||
imError: string | null;
|
||||
stickerpickerWidget: IWidgetEvent | null;
|
||||
widgetId: string | null;
|
||||
}
|
||||
|
||||
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 popoverHeight = 300;
|
||||
// This is loaded by _acquireScalarClient on an as-needed basis.
|
||||
private scalarClient: ScalarAuthClient = null;
|
||||
private scalarClient: ScalarAuthClient | null = null;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
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);
|
||||
// TODO: Pick the right manager for the widget
|
||||
if (IntegrationManagers.sharedInstance().hasManager()) {
|
||||
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
|
||||
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager()?.getScalarClient() ?? null;
|
||||
return this.scalarClient
|
||||
.connect()
|
||||
?.connect()
|
||||
.then(() => {
|
||||
this.forceUpdate();
|
||||
return this.scalarClient;
|
||||
@ -170,21 +170,14 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||
private updateWidget = (): void => {
|
||||
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
|
||||
if (!stickerpickerWidget) {
|
||||
Stickerpicker.currentWidget = null;
|
||||
Stickerpicker.currentWidget = undefined;
|
||||
this.setState({ stickerpickerWidget: null, widgetId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWidget = Stickerpicker.currentWidget;
|
||||
let currentUrl = null;
|
||||
if (currentWidget && currentWidget.content && currentWidget.content.url) {
|
||||
currentUrl = currentWidget.content.url;
|
||||
}
|
||||
|
||||
let newUrl = null;
|
||||
if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
|
||||
newUrl = stickerpickerWidget.content.url;
|
||||
}
|
||||
const currentUrl = currentWidget?.content?.url ?? null;
|
||||
const newUrl = stickerpickerWidget?.content?.url ?? null;
|
||||
|
||||
if (newUrl !== currentUrl) {
|
||||
// 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 {
|
||||
if (!this.state.stickerpickerWidget) return;
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForUid(
|
||||
WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id, null),
|
||||
WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id),
|
||||
);
|
||||
if (messaging && visible !== this.prevSentVisibility) {
|
||||
messaging.updateVisibility(visible).catch((err) => {
|
||||
@ -300,8 +293,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||
room={this.props.room}
|
||||
threadId={this.props.threadId}
|
||||
fullWidth={true}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
userId={MatrixClientPeg.get().credentials.userId!}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId!}
|
||||
waitForIframeLoad={true}
|
||||
showMenubar={true}
|
||||
onEditClick={this.launchManageIntegrations}
|
||||
@ -347,8 +340,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||
private launchManageIntegrations = (): void => {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
IntegrationManagers.sharedInstance()
|
||||
.getPrimaryManager()
|
||||
.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId);
|
||||
?.getPrimaryManager()
|
||||
?.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId ?? undefined);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
@ -45,19 +45,19 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class ThirdPartyMemberInfo extends React.Component<IProps, IState> {
|
||||
private room: Room;
|
||||
private readonly room: Room | null;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = this.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const me = this.room?.getMember(MatrixClientPeg.get().getUserId()!);
|
||||
const powerLevels = this.room?.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
||||
let kickLevel = powerLevels ? powerLevels.getContent().kick : 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 = {
|
||||
stateKey: this.props.event.getStateKey(),
|
||||
@ -121,7 +121,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let adminTools = null;
|
||||
let adminTools: JSX.Element | undefined;
|
||||
if (this.state.canKick && this.state.invited) {
|
||||
adminTools = (
|
||||
<div className="mx_MemberInfo_container">
|
||||
@ -135,8 +135,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
||||
);
|
||||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
let scopeHeader: JSX.Element | undefined;
|
||||
if (this.room?.isSpaceRoom()) {
|
||||
scopeHeader = (
|
||||
<div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
|
@ -77,18 +77,18 @@ interface IPreviewProps {
|
||||
export const ThreadMessagePreview: React.FC<IPreviewProps> = ({ thread, showDisplayname = false }) => {
|
||||
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
|
||||
const [content, setContent] = useState<IContent>(lastReply?.getContent());
|
||||
const [content, setContent] = useState<IContent | undefined>(lastReply?.getContent());
|
||||
useTypedEventEmitter(lastReply, MatrixEventEvent.Replaced, () => {
|
||||
setContent(lastReply.getContent());
|
||||
setContent(lastReply!.getContent());
|
||||
});
|
||||
const awaitDecryption = lastReply?.shouldAttemptDecryption() || lastReply?.isBeingDecrypted();
|
||||
useTypedEventEmitter(awaitDecryption ? lastReply : null, MatrixEventEvent.Decrypted, () => {
|
||||
setContent(lastReply.getContent());
|
||||
useTypedEventEmitter(awaitDecryption ? lastReply : undefined, MatrixEventEvent.Decrypted, () => {
|
||||
setContent(lastReply!.getContent());
|
||||
});
|
||||
|
||||
const preview = useAsyncMemo(async (): Promise<string> => {
|
||||
const preview = useAsyncMemo(async (): Promise<string | undefined> => {
|
||||
if (!lastReply) return;
|
||||
await cli.decryptEventIfNeeded(lastReply);
|
||||
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
|
||||
|
@ -20,8 +20,8 @@ import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
onScrollUpClick?: (e: React.MouseEvent) => void;
|
||||
onCloseClick?: (e: React.MouseEvent) => void;
|
||||
onScrollUpClick: (e: React.MouseEvent) => void;
|
||||
onCloseClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {
|
||||
|
@ -48,7 +48,7 @@ import { createVoiceMessageContent } from "../../../utils/createVoiceMessageCont
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
relation?: IEventRelation;
|
||||
replyToEvent?: MatrixEvent;
|
||||
}
|
||||
@ -70,9 +70,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
recorder: null, // no recording started by default
|
||||
};
|
||||
this.state = {};
|
||||
|
||||
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);
|
||||
|
||||
// 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> => {
|
||||
@ -220,7 +218,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
|
||||
try {
|
||||
// stop any noises which might be happening
|
||||
PlaybackManager.instance.pauseAllExcept(null);
|
||||
PlaybackManager.instance.pauseAllExcept();
|
||||
const recorder = VoiceRecordingStore.instance.startRecording(this.voiceRecordingId);
|
||||
await recorder.start();
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user