Merge branch 'develop' into PlaybackContainer

This commit is contained in:
Suguru Hirahara 2023-03-02 18:16:04 +00:00 committed by GitHub
commit a0f1cb4c47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
307 changed files with 4618 additions and 3366 deletions

View file

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

View file

@ -1,3 +1,50 @@
Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28)
=====================================================================================================
## ✨ Features
* Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner.
* Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald.
* Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt.
* Poll history: detail screen ([\#10172](https://github.com/matrix-org/matrix-react-sdk/pull/10172)). Contributed by @kerryarchibald.
* Provide a more detailed error message than "No known servers" ([\#6048](https://github.com/matrix-org/matrix-react-sdk/pull/6048)). Fixes vector-im/element-web#13247. Contributed by @aaronraimist.
* Say when a call was answered from a different device ([\#10224](https://github.com/matrix-org/matrix-react-sdk/pull/10224)).
* Widget permissions customizations using module api ([\#10121](https://github.com/matrix-org/matrix-react-sdk/pull/10121)). Contributed by @maheichyk.
* Fix copy button icon overlapping with copyable text ([\#10227](https://github.com/matrix-org/matrix-react-sdk/pull/10227)). Contributed by @Adesh-Pandey.
* Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk.
* The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)).
* Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)).
* Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros.
* Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald.
* Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald.
* Poll history - ended polls list items ([\#10119](https://github.com/matrix-org/matrix-react-sdk/pull/10119)). Contributed by @kerryarchibald.
* Remove threads labs flag and the ability to disable threads ([\#9878](https://github.com/matrix-org/matrix-react-sdk/pull/9878)). Fixes vector-im/element-web#24365.
* Show a success dialog after setting up the key backup ([\#10177](https://github.com/matrix-org/matrix-react-sdk/pull/10177)). Fixes vector-im/element-web#24487.
* Release Sign in with QR out of labs ([\#10066](https://github.com/matrix-org/matrix-react-sdk/pull/10066)). Contributed by @hughns.
* Hide indent button in rte ([\#10149](https://github.com/matrix-org/matrix-react-sdk/pull/10149)). Contributed by @alunturner.
* Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)).
* Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)).
* Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)).
* Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33.
* Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009.
* Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul.
* Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt.
* Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald.
* Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne.
* Re-focus the composer on dialogue quit ([\#10007](https://github.com/matrix-org/matrix-react-sdk/pull/10007)). Fixes vector-im/element-web#22832. Contributed by @Ashu999.
* Try to resolve emails before creating a DM ([\#10164](https://github.com/matrix-org/matrix-react-sdk/pull/10164)).
* Disable poll response loading test ([\#10168](https://github.com/matrix-org/matrix-react-sdk/pull/10168)). Contributed by @justjanne.
* Fix email lookup in invite dialog ([\#10150](https://github.com/matrix-org/matrix-react-sdk/pull/10150)). Fixes vector-im/element-web#23353.
* Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul.
* Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul.
* Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138.
* Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul.
* Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra.
* Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh.
* Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul.
Changes in [3.66.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0) (2023-02-14)
=====================================================================================================

View file

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

View file

@ -164,7 +164,7 @@ describe("Decryption Failure Bar", () => {
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
cy.get(".mx_DecryptionFailureBar").percySnapshotElement(
"DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button",
"DecryptionFailureBar prompts user to open another device, without Resend Key Requests button",
{
widths: [320, 640],
},

View file

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

View file

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

View file

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

View file

@ -54,9 +54,20 @@ describe("Threads", () => {
cy.visit("/#/room/" + roomId);
});
// --MessageTimestamp-color = #acacac = rgb(172, 172, 172)
// See: _MessageTimestamp.pcss
const MessageTimestampColor = "rgb(172, 172, 172)";
// User sends message
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Check the colour of timestamp on the main timeline
cy.get(".mx_RoomView_body .mx_EventTile_last .mx_EventTile_line .mx_MessageTimestamp").should(
"have.css",
"color",
MessageTimestampColor,
);
// Wait for message to send, get its ID and save as @threadId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.invoke("attr", "data-scroll-tokens")
@ -78,6 +89,13 @@ describe("Threads", () => {
// User responds in thread
cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}");
// Check the colour of timestamp on EventTile in a thread (mx_ThreadView)
cy.get(".mx_ThreadView .mx_EventTile_last .mx_EventTile_line .mx_MessageTimestamp").should(
"have.css",
"color",
MessageTimestampColor,
);
// User asserts summary was updated correctly
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
@ -130,6 +148,10 @@ describe("Threads", () => {
cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => {
cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot");
cy.get(".mx_ThreadSummary_content").should("contain", "How are things?");
// Check the colour of timestamp on thread list
cy.get(".mx_EventTile_details .mx_MessageTimestamp").should("have.css", "color", MessageTimestampColor);
// User opens thread via threads list
cy.get(".mx_EventTile_line").click();
});

View file

@ -185,9 +185,8 @@ describe("Timeline", () => {
.should("have.css", "margin-inline-start", "104px")
.should("have.css", "inset-inline-start", "0px");
// Exclude timestamp from snapshot
const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
// Exclude timestamp and read marker from snapshot
const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
percyCSS,
});
@ -213,8 +212,8 @@ describe("Timeline", () => {
// Click timestamp to highlight hidden event line
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; }";
// Exclude timestamp and read marker from snapshot
const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { 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);
@ -239,7 +238,11 @@ describe("Timeline", () => {
});
});
it("should click top left of view source event toggle", () => {
it("should click view source event toggle", () => {
// This test checks:
// 1. clickability of top left of view source event toggle
// 2. clickability of view source toggle on IRC layout
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
@ -255,8 +258,10 @@ describe("Timeline", () => {
});
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist");
// 1. clickability of top left of view source event toggle
// Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
cy.get(".mx_EventTile:not(:first-child) .mx_ViewSourceEvent")
cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent")
.should("exist")
.realHover()
.within(() => {
@ -264,7 +269,41 @@ describe("Timeline", () => {
});
// Make sure the expand toggle worked
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible");
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded").should("be.visible");
// Click again to collapse the source
cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent")
.should("exist")
.realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
});
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded").should("not.exist");
// 2. clickability of view source toggle on IRC layout
// Enable IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Exclude timestamp from snapshot
const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
// Hover the view source toggle on IRC layout
cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
.should("exist")
.realHover()
.percySnapshotElement("Hovered hidden event line on IRC layout", { percyCSS });
// Click view source event toggle
cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
.should("exist")
.realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
});
// Make sure the expand toggle worked
cy.get(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded").should("be.visible");
});
it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => {
@ -336,9 +375,8 @@ describe("Timeline", () => {
cy.checkA11y();
// Exclude timestamp from snapshot
const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }";
// Exclude timestamp and read marker from snapshot
const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
percyCSS,
widths: [800, 400],

View file

@ -41,7 +41,9 @@ 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.get(".mx_Spinner", { log: false }).should("not.exist");
// But like really no more spinners please
cy.get(".mx_Spinner", { log: false }).should("not.exist");
}
cy.percySnapshot(name, {
domTransformation: (documentClone) => scope(documentClone, subject.selector),

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.66.0",
"version": "3.67.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -54,6 +54,10 @@
"test:cypress:open": "cypress open",
"coverage": "yarn test --coverage"
},
"resolutions": {
"@types/react-dom": "17.0.19",
"@types/react": "17.0.53"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.4.0",
@ -92,7 +96,7 @@
"lodash": "^4.17.20",
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "2.0.0",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5",
@ -143,7 +147,6 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1",
"@types/css-font-loading-module": "^0.0.7",
@ -164,9 +167,9 @@
"@types/pako": "^2.0.0",
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "17.0.49",
"@types/react": "17.0.53",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "17.0.17",
"@types/react-dom": "17.0.19",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.8.0",
"@types/tar-js": "^0.3.2",
@ -191,8 +194,9 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.10.0",
"eslint-plugin-matrix-org": "1.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",
@ -211,10 +215,10 @@
"postcss-scss": "^4.0.4",
"prettier": "2.8.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stylelint": "^14.9.1",
"rimraf": "^4.0.0",
"stylelint": "^15.0.0",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0",
"stylelint-config-standard": "^30.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "4.9.3",
"walk": "^2.3.14"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -130,12 +130,17 @@ $irc-line-height: $font-18px;
.mx_TextualEvent,
.mx_ViewSourceEvent,
.mx_MTextBody {
display: inline-block;
/* add a 1px padding top and bottom because our larger
emoji font otherwise gets cropped by anti-zalgo */
padding: var(--EventTile_irc_line-padding-block) 0;
}
.mx_EventTile_e2eIcon,
.mx_TextualEvent,
.mx_MTextBody {
display: inline-block;
}
.mx_ReplyTile {
.mx_MTextBody {
display: -webkit-box; /* Enable -webkit-line-clamp */

View file

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

View file

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

View file

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

View file

@ -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] | undefined> {
public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
try {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
if (this.bind) {
@ -202,7 +202,7 @@ export default class AddThreepid {
// The spec has always required this to use UI auth but synapse briefly
// implemented it without, so this may just succeed and that's OK.
return;
return [true];
} catch (e) {
if (e.httpStatus !== 401 || !e.data || !e.data.flows) {
// doesn't look like an interactive-auth failure
@ -213,8 +213,7 @@ export default class AddThreepid {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t(
"Confirm adding this email address by using " +
"Single Sign On to prove your identity.",
"Confirm adding this email address by using Single Sign On to prove your identity.",
),
continueText: _t("Single Sign On"),
continueKind: "primary",
@ -226,19 +225,16 @@ export default class AddThreepid {
continueKind: "primary",
},
};
const { finished } = Modal.createDialog<[boolean, IAuthData | Error | null]>(
InteractiveAuthDialog,
{
title: _t("Add Email Address"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("Add Email Address"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
);
});
return finished;
}
}
@ -260,6 +256,7 @@ export default class AddThreepid {
}
throw err;
}
return [];
}
/**
@ -333,7 +330,7 @@ export default class AddThreepid {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t(
"Confirm adding this phone number by using " + "Single Sign On to prove your identity.",
"Confirm adding this phone number by using Single Sign On to prove your identity.",
),
continueText: _t("Single Sign On"),
continueKind: "primary",

View file

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

View file

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

View file

@ -389,7 +389,7 @@ export default class ContentMessages {
}
if (tooBigFiles.length > 0) {
const { finished } = Modal.createDialog<[boolean]>(UploadFailureDialog, {
const { finished } = Modal.createDialog(UploadFailureDialog, {
badFiles: tooBigFiles,
totalFiles: files.length,
contentMessages: this,
@ -407,7 +407,7 @@ export default class ContentMessages {
const loopPromiseBefore = promBefore;
if (!uploadAll) {
const { finished } = Modal.createDialog<[boolean, boolean]>(UploadConfirmDialog, {
const { finished } = Modal.createDialog(UploadConfirmDialog, {
file,
currentIndex: i,
totalFiles: okFiles.length,

View file

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

View file

@ -58,7 +58,7 @@ import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts
import ToastStore from "./stores/ToastStore";
import Resend from "./Resend";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes";
import { InviteKind } from "./components/views/dialogs/InviteDialogTypes";
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
@ -737,7 +737,7 @@ export default class LegacyCallHandler extends EventEmitter {
);
if (!stats) {
logger.debug(
"Call statistics are undefined. The call has " + "probably failed before a peerConn was established",
"Call statistics are undefined. The call has probably failed before a peerConn was established",
);
return;
}
@ -1214,7 +1214,7 @@ export default class LegacyCallHandler extends EventEmitter {
call.setRemoteOnHold(true);
dis.dispatch<OpenInviteDialogPayload>({
action: Action.OpenInviteDialog,
kind: KIND_CALL_TRANSFER,
kind: InviteKind.CallTransfer,
call,
analyticsName: "Transfer Call",
className: "mx_InviteDialog_transferWrapper",

View file

@ -265,7 +265,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
.then(() => {
const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) {
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve,
});
@ -275,7 +275,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
// between LL/non-LL version on same host.
// as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app)
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve,
host: window.location.host,

View file

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

View file

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

View file

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

View file

@ -551,7 +551,7 @@ export const Commands = [
) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("Use an identity server"),
description: (
<p>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentType, createRef } from "react";
import React, { createRef } from "react";
import {
ClientEvent,
createClient,
@ -38,6 +38,8 @@ import "focus-visible";
// what-input helps improve keyboard accessibility
import "what-input";
import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog";
import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import PosthogTrackers from "../../PosthogTrackers";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
@ -140,6 +142,7 @@ import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/Spotli
import { findDMForUser } from "../../utils/dm/findDMForUser";
import { Linkify } from "../../HtmlUtils";
import { NotificationColor } from "../../stores/notifications/NotificationColor";
import { UserTab } from "../views/dialogs/UserTab";
// legacy export
export { default as Views } from "../../Views";
@ -705,7 +708,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const tabPayload = payload as OpenToTabPayload;
Modal.createDialog(
UserSettingsDialog,
{ initialTabId: tabPayload.initialTabId },
{ initialTabId: tabPayload.initialTabId as UserTab },
/*className=*/ null,
/*isPriority=*/ false,
/*isStatic=*/ true,
@ -1629,14 +1632,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<ComponentType<{}>>,
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
{ newVersionInfo },
);
} else {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
) as unknown as Promise<ComponentType<{}>>,
) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
);
}
});

View file

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

View file

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

View file

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

View file

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

View file

@ -23,15 +23,15 @@ import { _t } from "../../languageHandler";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import BaseDialog from "../views/dialogs/BaseDialog";
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
import CopyableText from "../views/elements/CopyableText";
interface IProps extends IDialogProps {
interface IProps {
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
onFinished(): void;
}
interface IState {

View file

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

View file

@ -454,7 +454,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
}
let errorText: ReactNode =
_t("There was a problem communicating with the homeserver, " + "please try again later.") +
_t("There was a problem communicating with the homeserver, please try again later.") +
(errCode ? " (" + errCode + ")" : "");
if (err instanceof ConnectionError) {

View file

@ -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 { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton";
@ -26,7 +26,8 @@ import { ErrorMessage } from "../../ErrorMessage";
interface Props {
email: string;
errorText: string | null;
errorText: ReactNode | null;
onFinished(): void; // This modal is weird in that the way you close it signals intent
onCloseClick: () => void;
onReEnterEmailClick: () => void;
onResendClick: () => Promise<boolean>;

View file

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

View file

@ -91,7 +91,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
const publicKey = this.props.sitePublicKey;
if (!publicKey) {
logger.error("No public key for recaptcha!");
throw new Error("This server has not supplied enough information for Recaptcha " + "authentication");
throw new Error("This server has not supplied enough information for Recaptcha authentication");
}
logger.info("Rendering to %s", divId);

View file

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

View file

@ -32,13 +32,13 @@ import { toPx } from "../../../utils/units";
import { _t } from "../../../languageHandler";
interface IProps {
name: string; // The name (first initial used as default)
name?: string; // The name (first initial used as default)
idName?: string; // ID for generating hash colours
title?: string; // onHover title text
url?: string; // highest priority of them all, shortcut to set in urls[0]
url?: string | null; // highest priority of them all, shortcut to set in urls[0]
urls?: string[]; // [highest_priority, ... , lowest_priority]
width?: number;
height?: number;
width: number;
height: number;
// XXX: resizeMethod not actually used.
resizeMethod?: ResizeMethod;
defaultToInitialLetter?: boolean; // true to add default url
@ -48,7 +48,7 @@ interface IProps {
tabIndex?: number;
}
const calculateUrls = (url?: string, urls?: string[], lowBandwidth = false): string[] => {
const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
@ -66,7 +66,7 @@ const calculateUrls = (url?: string, urls?: string[], lowBandwidth = false): str
return Array.from(new Set(_urls));
};
const useImageUrl = ({ url, urls }: { url?: string; urls?: string[] }): [string, () => void] => {
const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => {
// Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);

View file

@ -62,7 +62,7 @@ enum Icon {
PresenceBusy = "BUSY",
}
function tooltipText(variant: Icon): string {
function tooltipText(variant: Icon): string | undefined {
switch (variant) {
case Icon.Globe:
return _t("This room is public");
@ -78,7 +78,7 @@ function tooltipText(variant: Icon): string {
}
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User;
private _dmUser: User | null;
private isUnmounted = false;
private isWatchingTimeline = false;
@ -103,11 +103,11 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
return joinRule === JoinRule.Public;
}
private get dmUser(): User {
private get dmUser(): User | null {
return this._dmUser;
}
private set dmUser(val: User) {
private set dmUser(val: User | null) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
@ -120,7 +120,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
}
}
private onRoomTimeline = (ev: MatrixEvent, room: Room | null): void => {
private onRoomTimeline = (ev: MatrixEvent, room?: Room): void => {
if (this.isUnmounted) return;
if (this.props.room.roomId !== room?.roomId) return;
@ -182,7 +182,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
public render(): React.ReactNode {
let badge: React.ReactNode;
if (this.props.displayBadge) {
if (this.props.displayBadge && this.state.notificationState) {
badge = (
<NotificationBadge
notification={this.state.notificationState}
@ -192,7 +192,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
);
}
let icon;
let icon: JSX.Element | undefined;
if (this.state.icon !== Icon.None) {
icon = (
<TextWithTooltip

View file

@ -65,7 +65,7 @@ export default function MemberAvatar({
const name = member?.name ?? fallbackUserId;
let title: string | undefined = props.title;
let imageUrl: string | undefined;
let imageUrl: string | null | undefined;
if (member?.name) {
if (member.getMxcAvatarUrl()) {
imageUrl = mediaFromMxc(member.getMxcAvatarUrl() ?? "").getThumbnailOfSourceHttp(

View file

@ -30,13 +30,14 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import { LocalRoom } from "../../../models/LocalRoom";
import { filterBoolean } from "../../../utils/arrays";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is,
// oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from)
room?: Room;
oobData?: IOOBData & {
oobData: IOOBData & {
roomId?: string;
};
viewAvatarOnClick?: boolean;
@ -86,7 +87,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
};
private static getImageUrls(props: IProps): string[] {
let oobAvatar = null;
let oobAvatar: string | null = null;
if (props.oobData.avatarUrl) {
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
props.width,
@ -94,28 +95,27 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
props.resizeMethod,
);
}
return [
return filterBoolean([
oobAvatar, // highest priority
RoomAvatar.getRoomAvatarUrl(props),
].filter(function (url) {
return url !== null && url !== "";
});
]);
}
private static getRoomAvatarUrl(props: IProps): string {
private static getRoomAvatarUrl(props: IProps): string | null {
if (!props.room) return null;
return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod);
}
private onRoomAvatarClick = (): void => {
const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
const avatarUrl = Avatar.avatarUrlForRoom(this.props.room ?? null, undefined, undefined, undefined);
const params = {
src: avatarUrl,
name: this.props.room.name,
name: this.props.room?.name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
};
private get roomIdName(): string | undefined {
@ -137,7 +137,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
public render(): React.ReactNode {
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
const roomName = room?.name ?? oobData.name;
const roomName = room?.name ?? oobData.name ?? "?";
return (
<BaseAvatar

View file

@ -21,8 +21,10 @@ import { IApp } from "../../../stores/WidgetStore";
import BaseAvatar, { BaseAvatarType } from "./BaseAvatar";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls" | "height" | "width"> {
app: IApp;
height?: number;
width?: number;
}
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
@ -44,7 +46,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
name={app.id}
className={classNames("mx_WidgetAvatar", className)}
// MSC2765
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined}
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : null}
urls={iconUrls}
width={width}
height={height}

View file

@ -42,14 +42,14 @@ const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ..
const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(beacon.roomId);
if (!latestLocationState || !beacon.isLive) {
if (!latestLocationState || !beacon.isLive || !room) {
return null;
}
const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self;
const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : undefined;
const isSelfLocation = beacon.beaconInfo?.assetType === LocationAssetType.Self;
const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : null;
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
const humanizedUpdateTime = (latestLocationState.timestamp && humanizeTime(latestLocationState.timestamp)) || "";
return (
<li className="mx_BeaconListItem" {...rest}>
@ -62,7 +62,7 @@ const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ..
<BeaconStatus
className="mx_BeaconListItem_status"
beacon={beacon}
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
label={beaconMember?.name || beacon.beaconInfo?.description || beacon.beaconInfoOwner}
displayStatus={BeaconDisplayStatus.Active}
>
{/* eat events from interactive share buttons

View file

@ -27,11 +27,11 @@ interface Props {
beacon: Beacon;
}
const useBeaconName = (beacon: Beacon): string => {
const useBeaconName = (beacon: Beacon): string | undefined => {
const matrixClient = useContext(MatrixClientContext);
if (beacon.beaconInfo.assetType !== LocationAssetType.Self) {
return beacon.beaconInfo.description;
if (beacon.beaconInfo?.assetType !== LocationAssetType.Self) {
return beacon.beaconInfo?.description;
}
const room = matrixClient.getRoom(beacon.roomId);
const member = room?.getMember(beacon.beaconInfoOwner);

View file

@ -23,7 +23,6 @@ import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-loca
import { useLiveBeacons } from "../../../utils/beacon/useLiveBeacons";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import BaseDialog from "../dialogs/BaseDialog";
import { IDialogProps } from "../dialogs/IDialogProps";
import Map from "../location/Map";
import ZoomButtons from "../location/ZoomButtons";
import BeaconMarker from "./BeaconMarker";
@ -38,11 +37,12 @@ import MapFallback from "../location/MapFallback";
import { MapError } from "../location/MapError";
import { LocationShareError } from "../../../utils/location";
interface IProps extends IDialogProps {
interface IProps {
roomId: Room["roomId"];
matrixClient: MatrixClient;
// open the map centered on this beacon's location
initialFocusedBeacon?: Beacon;
onFinished(): void;
}
// track the 'focused time' as ts
@ -54,7 +54,7 @@ interface FocusedBeaconState {
beacon?: Beacon;
}
const getBoundsCenter = (bounds: Bounds): string | undefined => {
const getBoundsCenter = (bounds?: Bounds): string | undefined => {
if (!bounds) {
return;
}
@ -70,10 +70,10 @@ const useMapPosition = (
{ beacon, ts }: FocusedBeaconState,
): {
bounds?: Bounds;
centerGeoUri: string;
centerGeoUri?: string;
} => {
const [bounds, setBounds] = useState<Bounds | undefined>(getBeaconBounds(liveBeacons));
const [centerGeoUri, setCenterGeoUri] = useState<string>(
const [centerGeoUri, setCenterGeoUri] = useState<string | undefined>(
beacon?.latestLocationState?.uri || getBoundsCenter(bounds),
);

View file

@ -45,12 +45,12 @@ const DialogOwnBeaconStatus: React.FC<Props> = ({ roomId }) => {
const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(roomId);
if (!beacon?.isLive) {
if (!beacon?.isLive || !room) {
return null;
}
const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self;
const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : undefined;
const isSelfLocation = beacon.beaconInfo?.assetType === LocationAssetType.Self;
const beaconMember = isSelfLocation ? room.getMember(beacon.beaconInfoOwner) : null;
return (
<div className="mx_DialogOwnBeaconStatus">

View file

@ -25,7 +25,7 @@ import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-loca
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import dispatcher from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
interface Props {
isMinimized?: boolean;
@ -121,7 +121,7 @@ const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
);
const onWarningClick = relevantBeacon
? () => {
? (_e: ButtonEvent) => {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: relevantBeacon.roomId,
@ -131,7 +131,7 @@ const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
highlighted: true,
});
}
: undefined;
: null;
const label = getLabel(hasStoppingErrors, hasLocationPublishErrors);

View file

@ -40,13 +40,19 @@ const getUpdateInterval = (ms: number): number => {
const useMsRemaining = (beacon: Beacon): number => {
const beaconInfo = useEventEmitterState(beacon, BeaconEvent.Update, () => beacon.beaconInfo);
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo));
const [msRemaining, setMsRemaining] = useState(() => (beaconInfo ? getBeaconMsUntilExpiry(beaconInfo) : 0));
useEffect(() => {
if (!beaconInfo) {
return;
}
setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
}, [beaconInfo]);
const updateMsRemaining = useCallback(() => {
if (!beaconInfo) {
return;
}
const ms = getBeaconMsUntilExpiry(beaconInfo);
setMsRemaining(ms);
}, [beaconInfo]);

View file

@ -42,7 +42,7 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({ beacon,
stoppingInProgress,
onStopSharing,
onResetLocationPublishError,
} = useOwnLiveBeacons([beacon?.identifier]);
} = useOwnLiveBeacons(beacon?.identifier ? [beacon?.identifier] : []);
// combine display status with errors that only occur for user's own beacons
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ? BeaconDisplayStatus.Error : displayStatus;

View file

@ -28,9 +28,9 @@ interface Props {
}
const ShareLatestLocation: React.FC<Props> = ({ latestLocationState }) => {
const [coords, setCoords] = useState(null);
const [coords, setCoords] = useState<GeolocationCoordinates | undefined>();
useEffect(() => {
if (!latestLocationState) {
if (!latestLocationState?.uri) {
return;
}
const coords = parseGeoUri(latestLocationState.uri);

View file

@ -40,7 +40,5 @@ export const getBeaconDisplayStatus = (
if (!latestLocationState) {
return BeaconDisplayStatus.Loading;
}
if (latestLocationState) {
return BeaconDisplayStatus.Active;
}
return BeaconDisplayStatus.Active;
};

View file

@ -25,13 +25,16 @@ import AccessibleButton from "../elements/AccessibleButton";
import QRCode from "../elements/QRCode";
import Heading from "../typography/Heading";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
const fallbackAppStore = "https://apps.apple.com/app/vector/id1083446067";
const fallbackGooglePlay = "https://play.google.com/store/apps/details?id=im.vector.app";
const fallbackFDroid = "https://f-droid.org/repository/browse/?fdid=im.vector.app";
export const AppDownloadDialog: FC<IDialogProps> = ({ onFinished }: IDialogProps) => {
interface Props {
onFinished(): void;
}
export const AppDownloadDialog: FC<Props> = ({ onFinished }) => {
const brand = SdkConfig.get("brand");
const desktopBuilds = SdkConfig.getObject("desktop_builds");
const mobileBuilds = SdkConfig.getObject("mobile_builds");

View file

@ -21,17 +21,16 @@ import FocusLock from "react-focus-lock";
import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import Heading from "../typography/Heading";
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps extends IDialogProps {
interface IProps {
// Whether the dialog should have a 'close' button that will
// cause the dialog to be cancelled. This should only be set
// to false if there is nothing the app can sensibly do if the
@ -75,6 +74,7 @@ interface IProps extends IDialogProps {
// optional Posthog ScreenName to supply during the lifetime of this dialog
"screenName"?: ScreenName;
onFinished(): void;
}
/*
@ -86,7 +86,7 @@ interface IProps extends IDialogProps {
export default class BaseDialog extends React.Component<IProps> {
private matrixClient: MatrixClient;
public static defaultProps = {
public static defaultProps: Partial<IProps> = {
hasCancel: true,
fixedWidth: true,
};
@ -98,9 +98,7 @@ export default class BaseDialog extends React.Component<IProps> {
}
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
this.props.onKeyDown?.(e);
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
@ -109,13 +107,13 @@ export default class BaseDialog extends React.Component<IProps> {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
this.props.onFinished();
break;
}
};
private onCancelClick = (e: ButtonEvent): void => {
this.props.onFinished(false);
private onCancelClick = (): void => {
this.props.onFinished();
};
public render(): React.ReactNode {

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
@ -27,8 +26,9 @@ import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas
interface IProps extends IDialogProps {
interface IProps {
featureId: string;
onFinished(sendFeedback?: boolean): void;
}
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
@ -49,7 +49,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
<AccessibleButton
kind="link_inline"
onClick={() => {
onFinished(false);
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,

View file

@ -26,19 +26,19 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "../dialogs/BaseDialog";
import InfoDialog from "../dialogs/InfoDialog";
import DialogButtons from "../elements/DialogButtons";
import StyledCheckbox from "../elements/StyledCheckbox";
interface IBulkRedactDialogProps extends IDialogProps {
interface Props {
matrixClient: MatrixClient;
room: Room;
member: RoomMember;
onFinished(redact?: boolean): void;
}
const BulkRedactDialog: React.FC<IBulkRedactDialogProps> = (props) => {
const BulkRedactDialog: React.FC<Props> = (props) => {
const { matrixClient: cli, room, member, onFinished } = props;
const [keepStateEvents, setKeepStateEvents] = useState(true);

View file

@ -24,7 +24,7 @@ import Spinner from "../elements/Spinner";
interface IProps {
redact: () => Promise<void>;
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
}
interface IState {

View file

@ -39,7 +39,7 @@ interface IProps {
children?: ReactNode;
className?: string;
roomId?: string;
onFinished: (success: boolean, reason?: string) => void;
onFinished: (success?: boolean, reason?: string) => void;
}
interface IState {
@ -55,7 +55,7 @@ interface IState {
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
public static defaultProps = {
public static defaultProps: Partial<IProps> = {
danger: false,
askReason: false,
};

View file

@ -21,7 +21,7 @@ import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps {
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
}
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {

View file

@ -41,7 +41,7 @@ interface IProps {
defaultName?: string;
parentSpace?: Room;
defaultEncrypted?: boolean;
onFinished(proceed: boolean, opts?: IOpts): void;
onFinished(proceed?: boolean, opts?: IOpts): void;
}
interface IState {

View file

@ -24,9 +24,10 @@ import Modal from "../../../Modal";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
interface IProps {
onFinished(logout?: boolean): void;
}
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
const brand = SdkConfig.get().brand;
@ -72,7 +73,7 @@ const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
<DialogButtons
primaryButton={_t("Continue With Encryption Disabled")}
hasCancel={false}
onPrimaryButtonClick={props.onFinished}
onPrimaryButtonClick={() => props.onFinished(false)}
>
<button onClick={_onLogoutClicked}>{_t("Sign out")}</button>
</DialogButtons>

View file

@ -39,7 +39,7 @@ type DialogAesthetics = Partial<{
}>;
interface IProps {
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
}
interface IState {

View file

@ -65,7 +65,7 @@ const Tools: Record<Category, [label: string, tool: Tool][]> = {
interface IProps {
roomId: string;
onFinished(finished: boolean): void;
onFinished(finished?: boolean): void;
}
type ToolInfo = [label: string, tool: Tool];

View file

@ -20,17 +20,16 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import QuestionDialog from "./QuestionDialog";
import { findTopAnswer } from "../messages/MPollBody";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import { GetRelationsForEvent } from "../rooms/EventTile";
interface IProps extends IDialogProps {
interface IProps {
matrixClient: MatrixClient;
event: MatrixEvent;
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
getRelationsForEvent?: GetRelationsForEvent;
}

View file

@ -31,7 +31,7 @@ import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
interface IProps {
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
title?: string;
description?: React.ReactNode;
button?: string;
@ -44,7 +44,7 @@ interface IState {
}
export default class ErrorDialog extends React.Component<IProps, IState> {
public static defaultProps = {
public static defaultProps: Partial<IProps> = {
focus: true,
};

View file

@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import Field from "../elements/Field";
@ -44,8 +43,9 @@ import InfoDialog from "./InfoDialog";
import ChatExport from "../../../customisations/ChatExport";
import { validateNumberInRange } from "../../../utils/validate";
interface IProps extends IDialogProps {
interface IProps {
room: Room;
onFinished(doExport?: boolean): void;
}
interface ExportConfig {

View file

@ -24,7 +24,6 @@ import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import BugReportDialog from "./BugReportDialog";
import InfoDialog from "./InfoDialog";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import { useStateToggle } from "../../../hooks/useStateToggle";
import StyledCheckbox from "../elements/StyledCheckbox";
@ -33,8 +32,9 @@ const existingIssuesUrl =
"https://github.com/vector-im/element-web/issues" + "?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
interface IProps extends IDialogProps {
interface IProps {
feature?: string;
onFinished(): void;
}
const FeedbackDialog: React.FC<IProps> = (props: IProps) => {

View file

@ -29,7 +29,6 @@ import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { useSettingValue } from "../../../hooks/useSettings";
import { Layout } from "../../../settings/enums/Layout";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import { avatarUrlForUser } from "../../../Avatar";
import EventTile from "../rooms/EventTile";
@ -55,13 +54,14 @@ import { RoomContextDetails } from "../rooms/RoomContextDetails";
const AVATAR_SIZE = 30;
interface IProps extends IDialogProps {
interface IProps {
matrixClient: MatrixClient;
// The event to forward
event: MatrixEvent;
// We need a permalink creator for the source room to pass through to EventTile
// in case the event is a reply (even though the user can't get at the link)
permalinkCreator: RoomPermalinkCreator;
onFinished(): void;
}
interface IEntryProps {

View file

@ -20,17 +20,17 @@ import QuestionDialog from "./QuestionDialog";
import { _t } from "../../../languageHandler";
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
interface IProps extends IDialogProps {
interface IProps {
title: string;
subheading: string;
rageshakeLabel: string;
rageshakeData?: Record<string, string>;
onFinished(sendFeedback?: boolean): void;
}
const GenericFeatureFeedbackDialog: React.FC<IProps> = ({

View file

@ -1,280 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import QuestionDialog from "./QuestionDialog";
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { HostSignupStore } from "../../../stores/HostSignupStore";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { IPostmessage, IPostmessageResponseData, PostmessageAction } from "./HostSignupDialogTypes";
import { IConfigOptions } from "../../../IConfigOptions";
import { SnakedObject } from "../../../utils/SnakedObject";
interface IProps {}
interface IState {
completed: boolean;
error: string;
minimized: boolean;
}
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
private readonly config: SnakedObject<IConfigOptions["host_signup"]>;
public constructor(props: IProps) {
super(props);
this.state = {
completed: false,
error: null,
minimized: false,
};
this.config = SdkConfig.getObject("host_signup");
}
private messageHandler = async (message: IPostmessage): Promise<void> => {
if (!this.config.get("url").startsWith(message.origin)) {
return;
}
switch (message.data.action) {
case PostmessageAction.HostSignupAccountDetailsRequest:
this.onAccountDetailsRequest();
break;
case PostmessageAction.Maximize:
this.setState({
minimized: false,
});
break;
case PostmessageAction.Minimize:
this.setState({
minimized: true,
});
break;
case PostmessageAction.SetupComplete:
this.setState({
completed: true,
});
break;
case PostmessageAction.CloseDialog:
return this.closeDialog();
}
};
private maximizeDialog = (): void => {
this.setState({
minimized: false,
});
// Send this action to the iframe so it can act accordingly
this.sendMessage({
action: PostmessageAction.Maximize,
});
};
private minimizeDialog = (): void => {
this.setState({
minimized: true,
});
// Send this action to the iframe so it can act accordingly
this.sendMessage({
action: PostmessageAction.Minimize,
});
};
private closeDialog = async (): Promise<void> => {
window.removeEventListener("message", this.messageHandler);
// Finally clear the flag in
return HostSignupStore.instance.setHostSignupActive(false);
};
private onCloseClick = async (): Promise<void> => {
if (this.state.completed) {
// We're done, close
return this.closeDialog();
} else {
Modal.createDialog(QuestionDialog, {
title: _t("Confirm abort of host creation"),
description: _t(
"Are you sure you wish to abort creation of the host? The process cannot be continued.",
),
button: _t("Abort"),
onFinished: (result) => {
if (result) {
return this.closeDialog();
}
},
});
}
};
private sendMessage = (message: IPostmessageResponseData): void => {
this.iframeRef.current.contentWindow.postMessage(message, this.config.get("url"));
};
private async sendAccountDetails(): Promise<void> {
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
if (!openIdToken || !openIdToken.access_token) {
logger.warn("Failed to connect to homeserver for OpenID token.");
this.setState({
completed: true,
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
});
return;
}
this.sendMessage({
action: PostmessageAction.HostSignupAccountDetails,
account: {
accessToken: await MatrixClientPeg.get().getAccessToken(),
name: OwnProfileStore.instance.displayName,
openIdToken: openIdToken.access_token,
serverName: await MatrixClientPeg.get().getDomain(),
userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(),
termsAccepted: true,
},
});
}
private onAccountDetailsDialogFinished = async (result: boolean): Promise<void> => {
if (result) {
return this.sendAccountDetails();
}
return this.closeDialog();
};
private onAccountDetailsRequest = (): void => {
const cookiePolicyUrl = this.config.get("cookie_policy_url");
const privacyPolicyUrl = this.config.get("privacy_policy_url");
const tosUrl = this.config.get("terms_of_service_url");
const textComponent = (
<>
<p>
{_t(
"Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
"account to fetch verified email addresses. This data is not stored.",
{
hostSignupBrand: this.config.get("brand"),
},
)}
</p>
<p>
{_t(
"Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
{},
{
cookiePolicyLink: () => (
<a href={cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
{_t("Cookie Policy")}
</a>
),
privacyPolicyLink: () => (
<a href={privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
{_t("Privacy Policy")}
</a>
),
termsOfServiceLink: () => (
<a href={tosUrl} target="_blank" rel="noreferrer noopener">
{_t("Terms of Service")}
</a>
),
},
)}
</p>
</>
);
Modal.createDialog(QuestionDialog, {
title: _t("You should know"),
description: textComponent,
button: _t("Continue"),
onFinished: this.onAccountDetailsDialogFinished,
});
};
public componentDidMount(): void {
window.addEventListener("message", this.messageHandler);
}
public componentWillUnmount(): void {
if (HostSignupStore.instance.isHostSignupActive) {
// Run the close dialog actions if we're still active, otherwise good to go
this.closeDialog();
}
}
public render(): React.ReactNode {
return (
<div className={classNames({ mx_Dialog_wrapper: !this.state.minimized })}>
<div
className={classNames("mx_Dialog", {
mx_HostSignupDialog_minimized: this.state.minimized,
mx_HostSignupDialog: !this.state.minimized,
})}
>
{this.state.minimized && (
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
<div className="mx_Dialog_title">
{_t("%(hostSignupBrand)s Setup", {
hostSignupBrand: this.config.get("brand"),
})}
</div>
<AccessibleButton
className="mx_HostSignup_maximize_button"
onClick={this.maximizeDialog}
aria-label={_t("Maximise dialog")}
title={_t("Maximise dialog")}
/>
</div>
)}
{!this.state.minimized && (
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
<AccessibleButton
onClick={this.minimizeDialog}
className="mx_HostSignup_minimize_button"
aria-label={_t("Minimise dialog")}
title={_t("Minimise dialog")}
/>
<AccessibleButton
onClick={this.onCloseClick}
className="mx_Dialog_cancelButton"
aria-label={_t("Close dialog")}
title={_t("Close dialog")}
/>
</div>
)}
{this.state.error && <div>{this.state.error}</div>}
{!this.state.error && (
<iframe
title={_t("Upgrade to %(hostSignupBrand)s", {
hostSignupBrand: this.config.get("brand"),
})}
src={this.config.get("url")}
ref={this.iframeRef}
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
/>
)}
</div>
</div>
);
}
}

View file

@ -1,47 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum PostmessageAction {
CloseDialog = "close_dialog",
HostSignupAccountDetails = "host_signup_account_details",
HostSignupAccountDetailsRequest = "host_signup_account_details_request",
Minimize = "host_signup_minimize",
Maximize = "host_signup_maximize",
SetupComplete = "setup_complete",
}
interface IAccountData {
accessToken: string;
name: string;
openIdToken: string;
serverName: string;
userLocalpart: string;
termsAccepted: boolean;
}
export interface IPostmessageRequestData {
action: PostmessageAction;
}
export interface IPostmessageResponseData {
action: PostmessageAction;
account?: IAccountData;
}
export interface IPostmessage {
data: IPostmessageRequestData;
origin: string;
}

View file

@ -29,7 +29,6 @@ import Spinner from "../elements/Spinner";
import VerificationShowSas from "../verification/VerificationShowSas";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
@ -37,8 +36,9 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4;
interface IProps extends IDialogProps {
interface IProps {
verifier: VerificationBase<SasEvent, any>;
onFinished(verified?: boolean): void;
}
interface IState {

View file

@ -19,11 +19,10 @@ import React, { ReactNode, KeyboardEvent } from "react";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
interface IProps {
top?: ReactNode;
title?: string;
description?: ReactNode;
@ -32,10 +31,11 @@ interface IProps extends IDialogProps {
hasCloseButton?: boolean;
fixedWidth?: boolean;
onKeyDown?(event: KeyboardEvent): void;
onFinished(): void;
}
export default class InfoDialog extends React.Component<IProps> {
public static defaultProps = {
public static defaultProps: Partial<IProps> = {
title: "",
description: "",
hasCloseButton: false,

View file

@ -21,9 +21,10 @@ import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
interface IProps {
onFinished(): void;
}
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {

View file

@ -18,11 +18,12 @@ import React from "react";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {}
interface IProps {
onFinished(): void;
}
export default class IntegrationsImpossibleDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {

View file

@ -25,7 +25,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import InteractiveAuth, { ERROR_USER_CANCELLED, InteractiveAuthCallback } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
type DialogAesthetics = Partial<{
[x in AuthType]: {
@ -38,7 +37,7 @@ type DialogAesthetics = Partial<{
};
}>;
export interface InteractiveAuthDialogProps extends IDialogProps {
export interface InteractiveAuthDialogProps {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
@ -72,6 +71,8 @@ export interface InteractiveAuthDialogProps extends IDialogProps {
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases?: DialogAesthetics;
onFinished(success?: boolean, result?: IAuthData | Error | null): void;
}
interface IState {
@ -152,14 +153,14 @@ export default class InteractiveAuthDialog extends React.Component<InteractiveAu
if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
if (aesthetics && aesthetics.title) title = aesthetics.title;
if (aesthetics && aesthetics.body) body = aesthetics.body;
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
if (aesthetics?.title) title = aesthetics.title;
if (aesthetics?.body) body = aesthetics.body;
if (aesthetics?.continueText) continueText = aesthetics.continueText;
if (aesthetics?.continueKind) continueKind = aesthetics.continueKind;
}
}
let content;
let content: JSX.Element;
if (this.state.authError) {
content = (
<div id="mx_Dialog_content">

View file

@ -70,7 +70,7 @@ import {
startDmOnFirstMessage,
ThreepidMember,
} from "../../../utils/direct-messages";
import { KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from "./InviteDialogTypes";
import { InviteKind } from "./InviteDialogTypes";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
@ -253,32 +253,32 @@ interface BaseProps {
// Takes a boolean which is true if a user / users were invited /
// a call transfer was initiated or false if the dialog was cancelled
// with no action taken.
onFinished: (success: boolean) => void;
onFinished: (success?: boolean) => void;
// Initial value to populate the filter with
initialText?: string;
}
interface InviteDMProps extends BaseProps {
// The kind of invite being performed. Assumed to be KIND_DM if not provided.
kind?: typeof KIND_DM;
// The kind of invite being performed. Assumed to be InviteKind.Dm if not provided.
kind?: InviteKind.Dm;
}
interface InviteRoomProps extends BaseProps {
kind: typeof KIND_INVITE;
kind: InviteKind.Invite;
// The room ID this dialog is for. Only required for KIND_INVITE.
// The room ID this dialog is for. Only required for InviteKind.Invite.
roomId: string;
}
function isRoomInvite(props: Props): props is InviteRoomProps {
return props.kind === KIND_INVITE;
return props.kind === InviteKind.Invite;
}
interface InviteCallProps extends BaseProps {
kind: typeof KIND_CALL_TRANSFER;
kind: InviteKind.CallTransfer;
// The call to transfer. Only required for KIND_CALL_TRANSFER.
// The call to transfer. Only required for InviteKind.CallTransfer.
call: MatrixCall;
}
@ -305,8 +305,8 @@ interface IInviteDialogState {
}
export default class InviteDialog extends React.PureComponent<Props, IInviteDialogState> {
public static defaultProps = {
kind: KIND_DM,
public static defaultProps: Partial<Props> = {
kind: InviteKind.Dm,
initialText: "",
};
@ -318,10 +318,10 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
public constructor(props: Props) {
super(props);
if (props.kind === KIND_INVITE && !props.roomId) {
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
if (props.kind === InviteKind.Invite && !props.roomId) {
throw new Error("When using InviteKind.Invite a roomId is required for an InviteDialog");
} else if (props.kind === InviteKind.CallTransfer && !props.call) {
throw new Error("When using InviteKind.CallTransfer a call is required for an InviteDialog");
}
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId()!, SdkConfig.get("welcome_user_id")]);
@ -493,7 +493,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
};
private inviteUsers = async (): Promise<void> => {
if (this.props.kind !== KIND_INVITE) return;
if (this.props.kind !== InviteKind.Invite) return;
this.setState({ busy: true });
this.convertFilter();
const targets = this.convertFilter();
@ -528,7 +528,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
};
private transferCall = async (): Promise<void> => {
if (this.props.kind !== KIND_CALL_TRANSFER) return;
if (this.props.kind !== InviteKind.CallTransfer) return;
if (this.state.currentTabId == TabId.UserDirectory) {
this.convertFilter();
const targets = this.convertFilter();
@ -657,7 +657,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
this.setState({ tryingIdentityServer: true });
return;
}
if (term.indexOf("@") > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
if (Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
// Start off by suggesting the plain email while we try and resolve it
// to a real account.
this.setState({
@ -737,7 +737,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
if (idx >= 0) {
targets.splice(idx, 1);
} else {
if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) {
if (this.props.kind === InviteKind.CallTransfer && targets.length > 0) {
targets = [];
}
targets.push(member);
@ -796,7 +796,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
continue;
}
if (address.indexOf("@") > 0 && Email.looksValid(address)) {
if (Email.looksValid(address)) {
toAdd.push(new ThreepidMember(address));
continue;
}
@ -869,7 +869,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
const lastActive = (m: Result): number | undefined => (kind === "recents" ? m.lastActive : undefined);
let sectionName = kind === "recents" ? _t("Recent Conversations") : _t("Suggestions");
if (this.props.kind === KIND_INVITE) {
if (this.props.kind === InviteKind.Invite) {
sectionName = kind === "recents" ? _t("Recently Direct Messaged") : _t("Suggestions");
}
@ -959,7 +959,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
private renderEditor(): JSX.Element {
const hasPlaceholder =
this.props.kind == KIND_CALL_TRANSFER &&
this.props.kind == InviteKind.CallTransfer &&
this.state.targets.length === 0 &&
this.state.filterText.length === 0;
const targets = this.state.targets.map((t) => (
@ -974,7 +974,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
ref={this.editorRef}
onPaste={this.onPaste}
autoFocus={true}
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
disabled={
this.state.busy || (this.props.kind == InviteKind.CallTransfer && this.state.targets.length > 0)
}
autoComplete="off"
placeholder={hasPlaceholder ? _t("Search") : undefined}
data-testid="invite-dialog-input"
@ -1085,7 +1087,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
private get screenName(): ScreenName | undefined {
switch (this.props.kind) {
case KIND_DM:
case InviteKind.Dm:
return "StartChat";
}
}
@ -1112,7 +1114,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
const cli = MatrixClientPeg.get();
const userId = cli.getUserId()!;
if (this.props.kind === KIND_DM) {
if (this.props.kind === InviteKind.Dm) {
title = _t("Direct Messages");
if (identityServersEnabled) {
@ -1164,7 +1166,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
</CopyableText>
</div>
);
} else if (this.props.kind === KIND_INVITE) {
} else if (this.props.kind === InviteKind.Invite) {
const roomId = this.props.roomId;
const room = MatrixClientPeg.get()?.getRoom(roomId);
const isSpace = room?.isSpaceRoom();
@ -1235,7 +1237,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
);
}
}
} else if (this.props.kind === KIND_CALL_TRANSFER) {
} else if (this.props.kind === InviteKind.CallTransfer) {
title = _t("Transfer");
consultConnectSection = (
@ -1264,7 +1266,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}
const goButton =
this.props.kind == KIND_CALL_TRANSFER ? null : (
this.props.kind == InviteKind.CallTransfer ? null : (
<AccessibleButton
kind="primary"
onClick={goButtonFn}
@ -1298,7 +1300,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
);
let dialogContent;
if (this.props.kind === KIND_CALL_TRANSFER) {
if (this.props.kind === InviteKind.CallTransfer) {
const tabs: Tab[] = [];
tabs.push(
new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection),
@ -1363,8 +1365,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
return (
<BaseDialog
className={classNames({
mx_InviteDialog_transfer: this.props.kind === KIND_CALL_TRANSFER,
mx_InviteDialog_other: this.props.kind !== KIND_CALL_TRANSFER,
mx_InviteDialog_transfer: this.props.kind === InviteKind.CallTransfer,
mx_InviteDialog_other: this.props.kind !== InviteKind.CallTransfer,
mx_InviteDialog_hasFooter: !!footer,
})}
hasCancel={true}

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