From e350b4c2c2dcdbafe0f04c06d9e714b157365859 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:17:46 +0100 Subject: [PATCH 001/105] Deflakey `threads` cypress test (#10632) `inviteUser` is asynchronous so you can't just call it and plough straight on. Also some logging improvements which helped debug this --- cypress/e2e/threads/threads.spec.ts | 31 +++++++++++++++++------------ cypress/support/client.ts | 4 +++- cypress/support/homeserver.ts | 4 +++- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index ee1fd78d082..1d36cbdd850 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -31,7 +31,6 @@ describe("Threads", () => { }); cy.startHomeserver("default").then((data) => { homeserver = data; - cy.initTestUser(homeserver, "Tom"); }); }); @@ -50,12 +49,15 @@ describe("Threads", () => { }); let roomId: string; - cy.createRoom({}).then((_roomId) => { - roomId = _roomId; - cy.inviteUser(roomId, bot.getUserId()); - bot.joinRoom(roomId); - cy.visit("/#/room/" + roomId); - }); + cy.createRoom({}) + .then((_roomId) => { + roomId = _roomId; + return cy.inviteUser(roomId, bot.getUserId()); + }) + .then(async () => { + await bot.joinRoom(roomId); + cy.visit("/#/room/" + roomId); + }); // Around 200 characters const MessageLong = @@ -407,12 +409,15 @@ describe("Threads", () => { }); let roomId: string; - cy.createRoom({}).then((_roomId) => { - roomId = _roomId; - cy.inviteUser(roomId, bot.getUserId()); - bot.joinRoom(roomId); - cy.visit("/#/room/" + roomId); - }); + cy.createRoom({}) + .then((_roomId) => { + roomId = _roomId; + return cy.inviteUser(roomId, bot.getUserId()); + }) + .then(async () => { + await bot.joinRoom(roomId); + cy.visit("/#/room/" + roomId); + }); // Exclude timestamp, read marker, and mapboxgl-map from snapshots const percyCSS = diff --git a/cypress/support/client.ts b/cypress/support/client.ts index c56608fadca..535669d6be4 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -174,7 +174,9 @@ Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable => { return cy.getClient().then(async (cli: MatrixClient) => { - return cli.invite(roomId, userId); + const res = await cli.invite(roomId, userId); + Cypress.log({ name: "inviteUser", message: `sent invite in ${roomId} for ${userId}` }); + return res; }); }); diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index 3026c94b064..f233c4d41e6 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -58,7 +58,9 @@ declare global { function startHomeserver(template: string): Chainable { const homeserverName = Cypress.env("HOMESERVER"); - return cy.task(homeserverName + "Start", template); + return cy.task(homeserverName + "Start", template, { log: false }).then((x) => { + Cypress.log({ name: "startHomeserver", message: `Started homeserver instance ${x.serverId}` }); + }); } function stopHomeserver(homeserver?: HomeserverInstance): Chainable { From 7632f366248187f094acb44623a8ec76e562ed87 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 18 Apr 2023 13:38:41 +0200 Subject: [PATCH 002/105] Implement new toast UI (#10467) * Implement new toast UI * Use PCSS vars and Caption component * Add GenericToast-test * Tweak call toast * Fix code style --- res/css/_font-weights.pcss | 1 + res/css/compound/_Icon.pcss | 4 + res/css/structures/_ToastContainer.pcss | 75 +++++++++---------- .../toasts/_IncomingLegacyCallToast.pcss | 30 +++++--- res/img/compound/encryption-24px.svg | 5 ++ src/components/structures/MatrixChat.tsx | 6 +- src/components/structures/ToastContainer.tsx | 5 +- src/components/views/toasts/GenericToast.tsx | 3 +- src/components/views/typography/Caption.tsx | 5 +- src/stores/ToastStore.ts | 9 ++- src/toasts/IncomingLegacyCallToast.tsx | 2 +- .../views/toasts/GenericToast-test.tsx | 47 ++++++++++++ .../__snapshots__/GenericToast-test.tsx.snap | 72 ++++++++++++++++++ .../UnverifiedSessionToast-test.tsx.snap | 6 +- 14 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 res/img/compound/encryption-24px.svg create mode 100644 test/components/views/toasts/GenericToast-test.tsx create mode 100644 test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap diff --git a/res/css/_font-weights.pcss b/res/css/_font-weights.pcss index 3e2b19d516f..6999aa31507 100644 --- a/res/css/_font-weights.pcss +++ b/res/css/_font-weights.pcss @@ -14,4 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ +$font-normal: 400; $font-semi-bold: 600; diff --git a/res/css/compound/_Icon.pcss b/res/css/compound/_Icon.pcss index 07f9eb5a0e7..5edac93a7b6 100644 --- a/res/css/compound/_Icon.pcss +++ b/res/css/compound/_Icon.pcss @@ -24,6 +24,10 @@ limitations under the License. box-sizing: border-box; } +.mx_Icon_secondary-content { + color: $secondary-content; +} + .mx_Icon_accent { color: $accent; } diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 9a1e3b5e198..65a92e552f1 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -16,10 +16,9 @@ limitations under the License. .mx_ToastContainer { position: absolute; - top: 0; + top: $spacing-4; left: 70px; z-index: 101; - padding: 4px; display: grid; grid-template-rows: 1fr 14px 6px; @@ -34,25 +33,29 @@ limitations under the License. } .mx_Toast_toast { - grid-row: 1 / 3; - grid-column: 1; background-color: $system; - color: $primary-content; - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; - overflow: hidden; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); + color: $primary-content; + column-gap: $spacing-8; display: grid; - grid-template-columns: 22px 1fr; - column-gap: 8px; - row-gap: 4px; - padding: 8px; + grid-column: 1; + grid-row: 1 / 3; + grid-template-columns: 24px 1fr; + overflow: hidden; + padding: $spacing-16; &.mx_Toast_hasIcon { + .mx_Toast_icon { + grid-column: 1; + grid-row: 1; + } + &::before, &::after { content: ""; - width: 22px; - height: 22px; + width: 24px; + height: 24px; grid-column: 1; grid-row: 1; mask-size: 100%; @@ -62,11 +65,6 @@ limitations under the License. background-repeat: no-repeat; } - &.mx_Toast_icon_verification::after { - mask-image: url("$(res)/img/e2e/normal.svg"); - background-color: $primary-content; - } - &.mx_Toast_icon_verification_warning { /* white infill for the hollow svg mask */ &::before { @@ -96,6 +94,7 @@ limitations under the License. grid-column: 2; } } + &:not(.mx_Toast_hasIcon) { padding-left: 12px; @@ -104,24 +103,19 @@ limitations under the License. } } - .mx_Toast_title, - .mx_Toast_description { - padding-right: 8px; - } - .mx_Toast_title { - display: flex; align-items: center; + box-sizing: border-box; column-gap: 8px; + display: flex; + margin-bottom: $spacing-16; width: 100%; - box-sizing: border-box; h2 { + color: $primary-content; margin: 0; - font-size: $font-15px; - font-weight: 600; - display: inline; - width: auto; + font-size: $font-18px; + font-weight: $font-semi-bold; } .mx_Toast_title_countIndicator { @@ -135,25 +129,21 @@ limitations under the License. .mx_Toast_body { grid-column: 1 / 3; grid-row: 2; + position: relative; } .mx_Toast_buttons { + column-gap: $spacing-8; display: flex; justify-content: flex-end; - column-gap: 5px; - - .mx_AccessibleButton { - min-width: 96px; - box-sizing: border-box; - } + margin-top: $spacing-32; } .mx_Toast_description { - max-width: 272px; - overflow: hidden; - text-overflow: ellipsis; - margin: 4px 0 11px 0; - font-size: $font-12px; + color: $primary-content; + font-size: $font-15px; + font-weight: $font-semi-bold; + max-width: 300px; a { text-decoration: none; @@ -161,7 +151,10 @@ limitations under the License. } .mx_Toast_detail { - color: $secondary-content; + display: block; + font-weight: $font-normal; + margin-top: $spacing-4; + max-width: 300px; } .mx_Toast_deviceID { diff --git a/res/css/views/toasts/_IncomingLegacyCallToast.pcss b/res/css/views/toasts/_IncomingLegacyCallToast.pcss index e2092ef0064..0306aba8ac5 100644 --- a/res/css/views/toasts/_IncomingLegacyCallToast.pcss +++ b/res/css/views/toasts/_IncomingLegacyCallToast.pcss @@ -84,20 +84,19 @@ limitations under the License. } .mx_IncomingLegacyCallToast_buttons { - margin-top: 8px; - display: flex; - flex-direction: row; - gap: 12px; - .mx_IncomingLegacyCallToast_button { - @mixin LegacyCallButton; - padding: 0px 8px; - flex-shrink: 0; - flex-grow: 1; - font-size: $font-15px; - span { - padding: 8px 0; + align-items: center; + display: flex; + + &::before { + background-color: $button-fg-color; + content: ""; + display: inline-block; + margin-right: 8px; + mask-position: center; + mask-repeat: no-repeat; + } } &.mx_IncomingLegacyCallToast_button_accept span::before { @@ -133,6 +132,13 @@ limitations under the License. } } + .mx_IncomingLegacyCallToast_silence, + .mx_IncomingLegacyCallToast_unSilence { + position: absolute; + right: 0; + top: 0; + } + .mx_IncomingLegacyCallToast_silence::before { mask-image: url("$(res)/img/voip/silence.svg"); } diff --git a/res/img/compound/encryption-24px.svg b/res/img/compound/encryption-24px.svg new file mode 100644 index 00000000000..c5f8b67fca5 --- /dev/null +++ b/res/img/compound/encryption-24px.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 575ff07925b..a503895058b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -143,7 +143,7 @@ import { findDMForUser } from "../../utils/dm/findDMForUser"; import { Linkify } from "../../HtmlUtils"; import { NotificationColor } from "../../stores/notifications/NotificationColor"; import { UserTab } from "../views/dialogs/UserTab"; - +import { Icon as EncryptionIcon } from "../../../res/img/compound/encryption-24px.svg"; // legacy export export { default as Views } from "../../Views"; @@ -1669,7 +1669,9 @@ export default class MatrixChat extends React.PureComponent { ToastStore.sharedInstance().addOrReplaceToast({ key: "verifreq_" + request.channel.transactionId, title: _t("Verification requested"), - icon: "verification", + iconElement: ( + + ), props: { request }, component: VerificationRequestToast, priority: 90, diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index c95f5a10993..d829adfa80d 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -57,10 +57,10 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, bodyClassName, props } = topToast; + const { title, icon, iconElement, key, component, className, bodyClassName, props } = topToast; const bodyClasses = classNames("mx_Toast_body", bodyClassName); const toastClasses = classNames("mx_Toast_toast", className, { - mx_Toast_hasIcon: icon, + mx_Toast_hasIcon: icon || iconElement, [`mx_Toast_icon_${icon}`]: icon, }); const toastProps = Object.assign({}, props, { @@ -86,6 +86,7 @@ export default class ToastContainer extends React.Component<{}, IState> { toast = (
+ {iconElement} {titleElement}
{content}
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index c40eaab8e88..8808ee68013 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -18,6 +18,7 @@ import React, { ReactNode } from "react"; import AccessibleButton from "../elements/AccessibleButton"; import { XOR } from "../../../@types/common"; +import { Caption } from "../typography/Caption"; export interface IProps { description: ReactNode; @@ -40,7 +41,7 @@ const GenericToast: React.FC> = ({ onAccept, onReject, }) => { - const detailContent = detail ?
{detail}
: null; + const detailContent = detail ? {detail} : null; return (
diff --git a/src/components/views/typography/Caption.tsx b/src/components/views/typography/Caption.tsx index 69e7714b223..89fd551ed5f 100644 --- a/src/components/views/typography/Caption.tsx +++ b/src/components/views/typography/Caption.tsx @@ -19,13 +19,14 @@ import React, { HTMLAttributes } from "react"; interface Props extends Omit, "className"> { children: React.ReactNode; + className?: string; isError?: boolean; } -export const Caption: React.FC = ({ children, isError, ...rest }) => { +export const Caption: React.FC = ({ children, className, isError, ...rest }) => { return ( { // higher priority number will be shown on top of lower priority priority: number; title?: string; + /** + * Icon class. + * + * @deprecated Use iconElement instead. + */ icon?: string; + /** Icon element. Displayed left of the title. */ + iconElement?: ReactElement; component: C; className?: string; bodyClassName?: string; diff --git a/src/toasts/IncomingLegacyCallToast.tsx b/src/toasts/IncomingLegacyCallToast.tsx index 26ea393332d..147a35a1d03 100644 --- a/src/toasts/IncomingLegacyCallToast.tsx +++ b/src/toasts/IncomingLegacyCallToast.tsx @@ -119,7 +119,7 @@ export default class IncomingLegacyCallToast extends React.Component {isVoice ? _t("Voice call") : _t("Video call")}
-
+
> = {}): RenderResult => { + const propsWithDefaults = { + acceptLabel: "Accept", + description:
Description
, + onAccept: () => {}, + onReject: () => {}, + rejectLabel: "Reject", + ...props, + }; + + return render(); +}; + +describe("GenericToast", () => { + it("should render as expected with detail content", () => { + const { asFragment } = renderGenericToast(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render as expected without detail content", () => { + const { asFragment } = renderGenericToast({ + detail: "Detail", + }); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap new file mode 100644 index 00000000000..b6e47abff79 --- /dev/null +++ b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenericToast should render as expected with detail content 1`] = ` + +
+
+
+ Description +
+
+
+
+ Reject +
+
+ Accept +
+
+
+
+`; + +exports[`GenericToast should render as expected without detail content 1`] = ` + +
+
+
+ Description +
+ + Detail + +
+
+
+ Reject +
+
+ Accept +
+
+
+
+`; diff --git a/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap b/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap index 0aef2390306..07cd6cc3659 100644 --- a/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap +++ b/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap @@ -27,8 +27,8 @@ exports[`UnverifiedSessionToast when rendering the toast should render as expect
-
ABC123 -
+
Date: Tue, 18 Apr 2023 12:21:07 +0000 Subject: [PATCH 003/105] Update tj-actions/changed-files digest to 7ecfc67 (#10641) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/i18n_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index 1518c0fbc0b..9e699d7343d 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -12,7 +12,7 @@ jobs: - name: "Get modified files" id: changed_files if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' - uses: tj-actions/changed-files@79adacd43ea069e57037edc891ea8d33013bc3da # v35 + uses: tj-actions/changed-files@7ecfc6730dff8072d1cc5215a24cc9478f55264d # v35 with: files: | src/i18n/strings/* From 77a54619dc23237030da82488eef61b7b00f5aab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:17:22 +0100 Subject: [PATCH 004/105] Update dependency @types/react to v17.0.58 (#10643) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 76d5a803d80..6e28a1a2f1b 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ }, "resolutions": { "@types/react-dom": "17.0.19", - "@types/react": "17.0.55" + "@types/react": "17.0.58" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -164,7 +164,7 @@ "@types/pako": "^2.0.0", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.55", + "@types/react": "17.0.58", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "17.0.19", "@types/react-transition-group": "^4.4.0", diff --git a/yarn.lock b/yarn.lock index 6f210b53731..2602b60d348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2332,10 +2332,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@17.0.55", "@types/react@^17": - version "17.0.55" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.55.tgz#f94eac1a37929cd86d1cc084c239c08dcfd10e5f" - integrity sha512-kBcAhmT8RivFDYxHdy8QfPKu+WyfiiGjdPb9pIRtd6tj05j0zRHq5DBGW5Ogxv5cwSKd93BVgUk/HZ4I9p3zNg== +"@types/react@*", "@types/react@17.0.58", "@types/react@^17": + version "17.0.58" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.58.tgz#c8bbc82114e5c29001548ebe8ed6c4ba4d3c9fb0" + integrity sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" From 707dc36cc2391d13973a2edc6442b8796cd66b96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 12:40:27 +0000 Subject: [PATCH 005/105] Update browser-actions/setup-chrome digest to 5971308 (#10638) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 8c91f56ad6c..f64e035d640 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -97,7 +97,7 @@ jobs: # Run 4 instances in Parallel runner: [1, 2, 3, 4] steps: - - uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1 + - uses: browser-actions/setup-chrome@597130847c84cdac5acceccbd676d612e6f8beb8 - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - uses: tecolicom/actions-use-apt-tools@ceaf289fdbc6169fd2406a0f0365a584ffba003b # v1 From 83359f84d19aebe9c895797b88836753a7e46ca1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 18 Apr 2023 17:00:30 +0100 Subject: [PATCH 006/105] Revert "Update browser-actions/setup-chrome digest to 5971308" (#10655) This reverts commit adaaa94271a47b15a8593862c0e7bee39828a308. --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index f64e035d640..8c91f56ad6c 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -97,7 +97,7 @@ jobs: # Run 4 instances in Parallel runner: [1, 2, 3, 4] steps: - - uses: browser-actions/setup-chrome@597130847c84cdac5acceccbd676d612e6f8beb8 + - uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1 - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - uses: tecolicom/actions-use-apt-tools@ceaf289fdbc6169fd2406a0f0365a584ffba003b # v1 From 0db40e327cd7ca12aa1332f91109573fa4162ff3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 Apr 2023 17:00:55 +0100 Subject: [PATCH 007/105] Fix create subspace dialog not working (#10652) --- src/components/views/dialogs/CreateSubspaceDialog.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index d994b9fc642..19a47789141 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -56,18 +56,22 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick const onCreateSubspaceClick = async (e: ButtonEvent): Promise => { e.preventDefault(); - if (busy || !spaceNameField.current || !spaceAliasField.current) return; + if (busy) return; setBusy(true); // require & validate the space name field - if (!(await spaceNameField.current.validate({ allowEmpty: false }))) { + if (spaceNameField.current && !(await spaceNameField.current.validate({ allowEmpty: false }))) { spaceNameField.current.focus(); spaceNameField.current.validate({ allowEmpty: false, focused: true }); setBusy(false); return; } // validate the space name alias field but do not require it - if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) { + if ( + spaceAliasField.current && + joinRule === JoinRule.Public && + (await spaceAliasField.current.validate({ allowEmpty: true })) + ) { spaceAliasField.current.focus(); spaceAliasField.current.validate({ allowEmpty: true, focused: true }); setBusy(false); From 869856df2d92c215f5f0aa296ffb5913197cdd54 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 18 Apr 2023 20:59:18 +0000 Subject: [PATCH 008/105] Remove obsolete class name and declarations on `General` user settings tab (#10659) * Remove an obsolete class name: `mx_EmailAddresses_new` The class name was added by aa7afe819f5fedad0d21c534906c7c64d3f4dd0e for an initial implementation and became obsolete thanks to 1090b7d9124b3e0d51dc14d07325443a144bb48d. Signed-off-by: Suguru Hirahara * Remove obsolete declarations: `mx_ExistingPhoneNumber_delete` The declarations have been obsolete since 11f2b4320d7c1cf4ed852e74f26e7cb110b38ef4. Signed-off-by: Suguru Hirahara * Remove obsolete declarations: `mx_ExistingEmailAddress_delete` The declarations have also been deprecated by 11f2b4320d7c1cf4ed852e74f26e7cb110b38ef4. Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- res/css/views/settings/_EmailAddresses.pcss | 6 ------ res/css/views/settings/_PhoneNumbers.pcss | 6 ------ src/components/views/settings/account/EmailAddresses.tsx | 2 +- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/res/css/views/settings/_EmailAddresses.pcss b/res/css/views/settings/_EmailAddresses.pcss index 1c9ce724d1d..f401c64484d 100644 --- a/res/css/views/settings/_EmailAddresses.pcss +++ b/res/css/views/settings/_EmailAddresses.pcss @@ -21,12 +21,6 @@ limitations under the License. margin-bottom: 5px; } -.mx_ExistingEmailAddress_delete { - margin-right: 5px; - cursor: pointer; - vertical-align: middle; -} - .mx_ExistingEmailAddress_email, .mx_ExistingEmailAddress_promptText { flex: 1; diff --git a/res/css/views/settings/_PhoneNumbers.pcss b/res/css/views/settings/_PhoneNumbers.pcss index 507b07334ed..d64d1a63338 100644 --- a/res/css/views/settings/_PhoneNumbers.pcss +++ b/res/css/views/settings/_PhoneNumbers.pcss @@ -21,12 +21,6 @@ limitations under the License. margin-bottom: 5px; } -.mx_ExistingPhoneNumber_delete { - margin-right: 5px; - cursor: pointer; - vertical-align: middle; -} - .mx_ExistingPhoneNumber_address, .mx_ExistingPhoneNumber_promptText { flex: 1; diff --git a/src/components/views/settings/account/EmailAddresses.tsx b/src/components/views/settings/account/EmailAddresses.tsx index 5605bfacb66..3d3cb8fb187 100644 --- a/src/components/views/settings/account/EmailAddresses.tsx +++ b/src/components/views/settings/account/EmailAddresses.tsx @@ -276,7 +276,7 @@ export default class EmailAddresses extends React.Component { return (
{existingEmailElements} -
+ Date: Wed, 19 Apr 2023 03:24:18 +0000 Subject: [PATCH 009/105] Update dependency rimraf to v5 (#10649) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Kerry --- package.json | 2 +- yarn.lock | 89 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 6e28a1a2f1b..f24e0358a61 100644 --- a/package.json +++ b/package.json @@ -208,7 +208,7 @@ "postcss-scss": "^4.0.4", "prettier": "2.8.7", "raw-loader": "^4.0.2", - "rimraf": "^4.0.0", + "rimraf": "^5.0.0", "stylelint": "^15.0.0", "stylelint-config-standard": "^32.0.0", "stylelint-scss": "^4.2.0", diff --git a/yarn.lock b/yarn.lock index 2602b60d348..cf717cad2ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1885,6 +1885,11 @@ resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.23.0.tgz#7e3eb8a128952c548b3cc7213c65f620e3d09ec7" integrity sha512-FPXMOsK7SIh6NuK+wnr/O35bPN8cyYDGsXqkE5EWwf7Frs+QXFIejlYM9t1SckoxlDpS1YsPYJABv+CUsuJIlQ== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@sentry-internal/tracing@7.47.0": version "7.47.0" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.47.0.tgz#45e92eb4c8d049d93bd4fab961eaa38a4fb680f3" @@ -3229,6 +3234,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -4623,6 +4637,14 @@ foreachasync@^3.0.0: resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -4801,6 +4823,18 @@ glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^10.0.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.1.tgz#44288e9186b5cd5baa848728533ba21a94aa8f33" + integrity sha512-ngom3wq2UhjdbmRE/krgkD8BQyi1KZ5l+D2dVm4+Yj+jJIBp74/ZGunL6gNGc/CYuQmvUBiavWEXIotRiv5R6A== + dependencies: + foreground-child "^3.1.0" + fs.realpath "^1.0.0" + jackspeak "^2.0.3" + minimatch "^9.0.0" + minipass "^5.0.0" + path-scurry "^1.7.0" + glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -4813,16 +4847,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.3.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" - integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== - dependencies: - fs.realpath "^1.0.0" - minimatch "^8.0.2" - minipass "^4.2.4" - path-scurry "^1.6.1" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -5469,6 +5493,15 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.0.3.tgz#672eb397b97744a265b5862d7762b96e8dad6e61" + integrity sha512-0Jud3OMUdMbrlr3PyUMKESq51LXVAB+a239Ywdvd+Kgxj3MaBRml/nVRxf8tQFyfthMjuRkxkv7Vg58pmIMfuQ== + dependencies: + cliui "^7.0.4" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-canvas-mock@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341" @@ -6529,10 +6562,10 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^8.0.2: - version "8.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" - integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== +minimatch@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" + integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== dependencies: brace-expansion "^2.0.1" @@ -6550,11 +6583,6 @@ minimist@>=1.2.2, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^4.2.4: - version "4.2.7" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.7.tgz#14c6fc0dcab54d9c4dd64b2b7032fef04efec218" - integrity sha512-ScVIgqHcXRMyfflqHmEW0bm8z8rb5McHyOY3ewX9JBgZaR77G7nxq9L/mtV96/QbAAwtbCAHVVLzD1kkyfFQEw== - minipass@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" @@ -6927,10 +6955,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.6.1: - version "1.6.4" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.4.tgz#020a9449e5382a4acb684f9c7e1283bc5695de66" - integrity sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg== +path-scurry@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.7.0.tgz#99c741a2cfbce782294a39994d63748b5a24f6db" + integrity sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg== dependencies: lru-cache "^9.0.0" minipass "^5.0.0" @@ -7589,12 +7617,12 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^4.0.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" - integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== +rimraf@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.0.tgz#5bda14e410d7e4dd522154891395802ce032c2cb" + integrity sha512-Jf9llaP+RvaEVS5nPShYFhtXIrb3LRKP281ib3So0KkeZKo2wIKyq0Re7TOSwanasA423PSr6CCIL4bP6T040g== dependencies: - glob "^9.2.0" + glob "^10.0.0" rrweb-snapshot@^1.1.14: version "1.1.14" @@ -7754,6 +7782,11 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.1.tgz#96a61033896120ec9335d96851d902cc98f0ba2a" + integrity sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw== + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" From aff3bd6e32cdf81b66e5e6f42610646f56f0115c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 03:29:21 +0000 Subject: [PATCH 010/105] Update all non-major dependencies (#10644) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 ++--- yarn.lock | 70 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index f24e0358a61..e5c132d4885 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.51.5", + "posthog-js": "1.53.2", "qrcode": "1.5.1", "re-resizable": "^6.9.0", "react": "17.0.2", @@ -183,7 +183,7 @@ "cypress-axe": "^1.0.0", "cypress-multi-reporters": "^1.6.1", "cypress-real-events": "^1.7.1", - "eslint": "8.37.0", + "eslint": "8.38.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-deprecate": "^0.7.0", @@ -213,7 +213,7 @@ "stylelint-config-standard": "^32.0.0", "stylelint-scss": "^4.2.0", "ts-node": "^10.9.1", - "typescript": "5.0.3", + "typescript": "5.0.4", "walk": "^2.3.14" }, "@casualbot/jest-sonar-reporter": { diff --git a/yarn.lock b/yarn.lock index cf717cad2ac..70d505d34af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1140,7 +1140,7 @@ resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== -"@csstools/media-query-list-parser@^2.0.1": +"@csstools/media-query-list-parser@^2.0.2": version "2.0.4" resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz#466bd254041530dfd1e88bcb1921e8ca4af75b6a" integrity sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA== @@ -1209,10 +1209,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.37.0": - version "8.37.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d" - integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A== +"@eslint/js@8.38.0": + version "8.38.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.38.0.tgz#73a8a0d8aa8a8e6fe270431c5e72ae91b5337892" + integrity sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g== "@humanwhocodes/config-array@^0.11.8": version "0.11.8" @@ -4246,15 +4246,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== -eslint@8.37.0: - version "8.37.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.37.0.tgz#1f660ef2ce49a0bfdec0b0d698e0b8b627287412" - integrity sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw== +eslint@8.38.0: + version "8.38.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.38.0.tgz#a62c6f36e548a5574dd35728ac3c6209bd1e2f1a" + integrity sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.37.0" + "@eslint/js" "8.38.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -5015,7 +5015,7 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -html-tags@^3.2.0: +html-tags@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== @@ -6626,7 +6626,7 @@ murmurhash-js@^1.0.0: resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== -nanoid@^3.3.4: +nanoid@^3.3.4, nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== @@ -7090,7 +7090,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.21: +postcss@^8.3.11: version "8.4.21" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== @@ -7099,10 +7099,19 @@ postcss@^8.3.11, postcss@^8.4.21: picocolors "^1.0.0" source-map-js "^1.0.2" -posthog-js@1.51.5: - version "1.51.5" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.51.5.tgz#23f11f5e75690312301d596b6f1f795e5b423eb9" - integrity sha512-hhOreF51vvg97iKFZ4GFF4lwQVq1WWJXOJ59NbQVsXj+bVxDcX4vog0Yx40rfp4uWNnE/xRWQQEOwlKM2WkcjQ== +postcss@^8.4.21: + version "8.4.22" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.22.tgz#c29e6776b60ab3af602d4b513d5bd2ff9aa85dc1" + integrity sha512-XseknLAfRHzVWjCEtdviapiBtfLdgyzExD50Rg2ePaucEesyh8Wv4VPdW0nbyDa1ydbrAxV19jvMT4+LFmcNUA== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +posthog-js@1.53.2: + version "1.53.2" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.53.2.tgz#ea185f23d36e5fa0543b6e43a2df12cca4b9131e" + integrity sha512-/vSGeDEWNX8ZVvXu4DA+tdZXcc8gHjZl8Tb5cU97KXngQCOumsSimJTBeG/PI8X8R/mRWBbOmnllo72YWTrl1g== dependencies: fflate "^0.4.1" rrweb-snapshot "^1.1.14" @@ -7732,13 +7741,20 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.4.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318" integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw== dependencies: lru-cache "^6.0.0" +semver@^7.3.4: + version "7.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" + integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -8044,13 +8060,13 @@ stylelint-scss@^4.2.0: postcss-value-parser "^4.2.0" stylelint@^15.0.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.4.0.tgz#3958fff41fbcd68cf947fdecb329762d45f87013" - integrity sha512-TlOvpG3MbcFwHmK0q2ykhmpKo7Dq892beJit0NPdpyY9b1tFah/hGhqnAz/bRm2PDhDbJLKvjzkEYYBEz7Dxcg== + version "15.5.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.5.0.tgz#f16c238231f3f32e62da8a88969821d237eae8a6" + integrity sha512-jyMO3R1QtE5mUS4v40+Gg+sIQBqe7CF1xPslxycDzNVkIBCUD4O+5F1vLPq16VmunUTv4qG9o2rUKLnU5KkVeQ== dependencies: "@csstools/css-parser-algorithms" "^2.1.0" "@csstools/css-tokenizer" "^2.1.0" - "@csstools/media-query-list-parser" "^2.0.1" + "@csstools/media-query-list-parser" "^2.0.2" "@csstools/selector-specificity" "^2.2.0" balanced-match "^2.0.0" colord "^2.9.3" @@ -8064,7 +8080,7 @@ stylelint@^15.0.0: global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" - html-tags "^3.2.0" + html-tags "^3.3.1" ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" @@ -8399,10 +8415,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.3.tgz#fe976f0c826a88d0a382007681cbb2da44afdedf" - integrity sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA== +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== ua-parser-js@^1.0.2: version "1.0.35" From 957945b356a95b59885a2b3e2350eb43041fa2ed Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 19 Apr 2023 19:45:27 +1200 Subject: [PATCH 011/105] remove old debug logging in AsyncWrapper (#10661) --- src/AsyncWrapper.tsx | 3 --- test/components/structures/auth/ForgotPassword-test.tsx | 2 -- test/components/views/dialogs/InviteDialog-test.tsx | 1 - test/components/views/rooms/MessageComposer-test.tsx | 3 --- test/toasts/UnverifiedSessionToast-test.tsx | 2 +- .../components/molecules/VoiceBroadcastRecordingPip-test.tsx | 4 +--- test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx | 1 - 7 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 770f5f027b7..1173ca3fd06 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -45,9 +45,6 @@ export default class AsyncWrapper extends React.Component { public state: IState = {}; public componentDidMount(): void { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/element-web/issues/3148 - logger.log("Starting load of AsyncWrapper for modal"); this.props.prom .then((result) => { if (this.unmounted) return; diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index c193342a41c..7feef7d6a1a 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -70,8 +70,6 @@ describe("", () => { filterConsole( // not implemented by js-dom https://github.com/jsdom/jsdom/issues/1937 "Not implemented: HTMLFormElement.prototype.requestSubmit", - // not of interested for this test - "Starting load of AsyncWrapper for modal", ); beforeEach(() => { diff --git a/test/components/views/dialogs/InviteDialog-test.tsx b/test/components/views/dialogs/InviteDialog-test.tsx index 89be4f80300..9b13d9fc77f 100644 --- a/test/components/views/dialogs/InviteDialog-test.tsx +++ b/test/components/views/dialogs/InviteDialog-test.tsx @@ -104,7 +104,6 @@ describe("InviteDialog", () => { "Error retrieving profile for userId @carol:example.com", "Error retrieving profile for userId @localpart:server.tld", "Error retrieving profile for userId @localpart:server:tld", - "Starting load of AsyncWrapper for modal", "[Invite:Recents] Excluding @alice:example.org from recents", ); diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index d2a6e9c12ee..e076b8ae4a5 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -23,7 +23,6 @@ import userEvent from "@testing-library/user-event"; import { clearAllModals, createTestClient, - filterConsole, flushPromises, mkEvent, mkStubRoom, @@ -94,8 +93,6 @@ describe("MessageComposer", () => { stubClient(); const cli = createTestClient(); - filterConsole("Starting load of AsyncWrapper for modal"); - beforeEach(() => { mockPlatformPeg(); }); diff --git a/test/toasts/UnverifiedSessionToast-test.tsx b/test/toasts/UnverifiedSessionToast-test.tsx index 7a6dae4079b..a65f7dec79f 100644 --- a/test/toasts/UnverifiedSessionToast-test.tsx +++ b/test/toasts/UnverifiedSessionToast-test.tsx @@ -37,7 +37,7 @@ describe("UnverifiedSessionToast", () => { let client: Mocked; let renderResult: RenderResult; - filterConsole("Starting load of AsyncWrapper for modal", "Dismissing unverified sessions: ABC123"); + filterConsole("Dismissing unverified sessions: ABC123"); beforeAll(() => { client = mocked(stubClient()); diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx index dddaa858272..240d5732886 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx @@ -28,7 +28,7 @@ import { VoiceBroadcastRecording, VoiceBroadcastRecordingPip, } from "../../../../src/voice-broadcast"; -import { filterConsole, flushPromises, stubClient } from "../../../test-utils"; +import { flushPromises, stubClient } from "../../../test-utils"; import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; import { requestMediaPermissions } from "../../../../src/utils/media/requestMediaPermissions"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler"; @@ -85,8 +85,6 @@ describe("VoiceBroadcastRecordingPip", () => { }); }; - filterConsole("Starting load of AsyncWrapper for modal"); - beforeAll(() => { client = stubClient(); mocked(requestMediaPermissions).mockResolvedValue({ diff --git a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx index 9ee165d87af..1c7e6b66c12 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx @@ -220,7 +220,6 @@ describe("VoiceBroadcastPlayback", () => { }; filterConsole( - "Starting load of AsyncWrapper for modal", // expected for some tests "Unable to load broadcast playback", ); From 83e6a6057d613d64cfc5ec5d65757e214f4dc095 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 19 Apr 2023 13:34:27 +0200 Subject: [PATCH 012/105] Add developer tools option to room list context menu (#10635) * Make developer tools more accessible * Extend tests * Use settings hook * Trigger CI --- .../context_menus/_IconizedContextMenu.pcss | 6 ++- res/css/views/rooms/_RoomTile.pcss | 6 +-- .../context_menus/DeveloperToolsOption.tsx | 47 +++++++++++++++++++ .../views/context_menus/RoomContextMenu.tsx | 25 +++------- .../context_menus/RoomGeneralContextMenu.tsx | 13 ++++- .../tabs/user/HelpUserSettingsTab.tsx | 2 +- .../context_menus/RoomContextMenu-test.tsx | 26 ++++++++-- .../RoomGeneralContextMenu-test.tsx | 33 ++++++++++++- 8 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 src/components/views/context_menus/DeveloperToolsOption.tsx diff --git a/res/css/views/context_menus/_IconizedContextMenu.pcss b/res/css/views/context_menus/_IconizedContextMenu.pcss index e6600620acc..29b2eef7a4f 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.pcss +++ b/res/css/views/context_menus/_IconizedContextMenu.pcss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -180,6 +180,10 @@ limitations under the License. margin-right: -5px; } + .mx_IconizedContextMenu_developerTools::before { + mask-image: url("$(res)/img/element-icons/settings/flask.svg"); + } + .mx_IconizedContextMenu_checked::before { mask-image: url("$(res)/img/element-icons/roomlist/checkmark.svg"); } diff --git a/res/css/views/rooms/_RoomTile.pcss b/res/css/views/rooms/_RoomTile.pcss index c8bc6b2c928..700c55b0462 100644 --- a/res/css/views/rooms/_RoomTile.pcss +++ b/res/css/views/rooms/_RoomTile.pcss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -235,10 +235,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/export.svg"); } - .mx_RoomTile_iconDeveloperTools::before { - mask-image: url("$(res)/img/element-icons/settings/flask.svg"); - } - .mx_RoomTile_iconCopyLink::before { mask-image: url("$(res)/img/element-icons/link.svg"); } diff --git a/src/components/views/context_menus/DeveloperToolsOption.tsx b/src/components/views/context_menus/DeveloperToolsOption.tsx new file mode 100644 index 00000000000..9b483c35d6d --- /dev/null +++ b/src/components/views/context_menus/DeveloperToolsOption.tsx @@ -0,0 +1,47 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import Modal from "../../../Modal"; +import DevtoolsDialog from "../dialogs/DevtoolsDialog"; +import { IconizedContextMenuOption } from "./IconizedContextMenu"; +import { _t } from "../../../languageHandler"; + +interface Props { + onFinished: () => void; + roomId: string; +} + +export const DeveloperToolsOption: React.FC = ({ onFinished, roomId }) => { + return ( + { + Modal.createDialog( + DevtoolsDialog, + { + onFinished: () => {}, + roomId: roomId, + }, + "mx_DevtoolsDialog_wrapper", + ); + onFinished(); + }} + label={_t("Developer tools")} + iconClassName="mx_IconizedContextMenu_developerTools" + /> + ); +}; diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 5c2b0c3884e..4be907c161d 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 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. @@ -48,15 +48,18 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import SettingsStore from "../../../settings/SettingsStore"; -import DevtoolsDialog from "../dialogs/DevtoolsDialog"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; +import { DeveloperToolsOption } from "./DeveloperToolsOption"; interface IProps extends IContextMenuProps { room: Room; } +/** + * Room context menu accessible via the room header. + */ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { const cli = useContext(MatrixClientContext); const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => @@ -393,23 +396,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { {exportChatOption} {SettingsStore.getValue("developerMode") && ( - { - ev.preventDefault(); - ev.stopPropagation(); - - Modal.createDialog( - DevtoolsDialog, - { - roomId: room.roomId, - }, - "mx_DevtoolsDialog_wrapper", - ); - onFinished(); - }} - label={_t("Developer tools")} - iconClassName="mx_RoomTile_iconDeveloperTools" - /> + )} {leaveOption} diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index 711b5fb910c..901ed519b69 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 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. @@ -40,6 +40,8 @@ import IconizedContextMenu, { import { ButtonEvent } from "../elements/AccessibleButton"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; +import { DeveloperToolsOption } from "./DeveloperToolsOption"; +import { useSettingValue } from "../../../hooks/useSettings"; export interface RoomGeneralContextMenuProps extends IContextMenuProps { room: Room; @@ -52,6 +54,9 @@ export interface RoomGeneralContextMenuProps extends IContextMenuProps { onPostLeaveClick?: (event: ButtonEvent) => void; } +/** + * Room context menu accessible via the room list. + */ export const RoomGeneralContextMenu: React.FC = ({ room, onFinished, @@ -221,6 +226,11 @@ export const RoomGeneralContextMenu: React.FC = ({ /> ) : null; + const developerModeEnabled = useSettingValue("developerMode"); + const developerToolsOption = developerModeEnabled ? ( + + ) : null; + return ( @@ -234,6 +244,7 @@ export const RoomGeneralContextMenu: React.FC = ({ {settingsOption} )} + {developerToolsOption} {leaveOption} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index cb15fdfe1ab..b5c1e808693 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2019 - 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. diff --git a/test/components/views/context_menus/RoomContextMenu-test.tsx b/test/components/views/context_menus/RoomContextMenu-test.tsx index 1d1dffce5f9..a1e9f58b702 100644 --- a/test/components/views/context_menus/RoomContextMenu-test.tsx +++ b/test/components/views/context_menus/RoomContextMenu-test.tsx @@ -1,6 +1,7 @@ /* Copyright 2023 Mikhail Aheichyk Copyright 2023 Nordeck IT + Consulting GmbH. +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. @@ -27,6 +28,7 @@ import { shouldShowComponent } from "../../../../src/customisations/helpers/UICo import { stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import SettingsStore from "../../../../src/settings/SettingsStore"; jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({ shouldShowComponent: jest.fn(), @@ -58,8 +60,8 @@ describe("RoomContextMenu", () => { onFinished = jest.fn(); }); - function getComponent(props: Partial> = {}) { - return render( + function renderComponent(props: Partial> = {}) { + render( , @@ -70,7 +72,7 @@ describe("RoomContextMenu", () => { jest.spyOn(room, "canInvite").mockReturnValue(true); mocked(shouldShowComponent).mockReturnValue(false); - getComponent(); + renderComponent(); expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument(); }); @@ -79,8 +81,24 @@ describe("RoomContextMenu", () => { jest.spyOn(room, "canInvite").mockReturnValue(true); mocked(shouldShowComponent).mockReturnValue(true); - getComponent(); + renderComponent(); expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument(); }); + + it("when developer mode is disabled, it should not render the developer tools option", () => { + renderComponent(); + expect(screen.queryByText("Developer tools")).not.toBeInTheDocument(); + }); + + describe("when developer mode is enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode"); + }); + + it("should render the developer tools option", () => { + renderComponent(); + expect(screen.getByText("Developer tools")).toBeInTheDocument(); + }); + }); }); diff --git a/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx b/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx index cadf067ef55..df04e1c054f 100644 --- a/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx +++ b/test/components/views/context_menus/RoomGeneralContextMenu-test.tsx @@ -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. @@ -20,6 +20,7 @@ import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import React from "react"; +import userEvent from "@testing-library/user-event"; import { ChevronFace } from "../../../../src/components/structures/ContextMenu"; import { @@ -34,6 +35,8 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { mkMessage, stubClient } from "../../../test-utils/test-utils"; import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../src/settings/UIFeature"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import Modal from "../../../../src/Modal"; jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({ shouldShowComponent: jest.fn(), @@ -87,6 +90,10 @@ describe("RoomGeneralContextMenu", () => { onFinished = jest.fn(); }); + afterEach(() => { + Modal.closeCurrentModal("force"); + }); + it("renders an empty context menu for archived rooms", async () => { jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([DefaultTagID.Archived]); @@ -138,4 +145,28 @@ describe("RoomGeneralContextMenu", () => { expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(event, ReceiptType.Read, true); expect(onFinished).toHaveBeenCalled(); }); + + it("when developer mode is disabled, it should not render the developer tools option", () => { + getComponent(); + expect(screen.queryByText("Developer tools")).not.toBeInTheDocument(); + }); + + describe("when developer mode is enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode"); + getComponent(); + }); + + it("should render the developer tools option", async () => { + const developerToolsItem = screen.getByRole("menuitem", { name: "Developer tools" }); + expect(developerToolsItem).toBeInTheDocument(); + + // click open developer tools dialog + await userEvent.click(developerToolsItem); + + // assert that the dialog is displayed by searching some if its contents + expect(await screen.findByText("Toolbox")).toBeInTheDocument(); + expect(await screen.findByText(`Room ID: ${ROOM_ID}`)).toBeInTheDocument(); + }); + }); }); From 8867f1801e3bf2d74f3d486f380d79f31f14a1ac Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 20 Apr 2023 08:21:06 +0000 Subject: [PATCH 013/105] Remove obsolete CSS declarations from `_RoomHeader.pcss` (#10496) * Remove obsolete mx_RoomHeader_textButton and mx_RoomHeader_cancelButton These declarations have been obsolete since GroupView.js was deleted by fce36ec8266e08b3456673da1177eff105bca563. Signed-off-by: Suguru Hirahara * Remove obsolete mx_RoomHeader_spinner As of 79d3cca6 it was used as follows: ```` if (this.props.saving) { const Spinner = sdk.getComponent("elements.Spinner"); spinner =
; } ```` Signed-off-by: Suguru Hirahara * Remove obsolete mx_RoomHeader_info This was deprecated by 18fedb23ecfae873793498eb2acab09d69030557 Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- res/css/views/rooms/_RoomHeader.pcss | 29 ---------------------------- 1 file changed, 29 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 38e8124b9f1..ced702e5d3e 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -61,35 +61,6 @@ limitations under the License. } } -.mx_RoomHeader_spinner { - flex: 1; - height: 36px; - padding-left: 12px; - padding-right: 12px; -} - -.mx_RoomHeader_textButton { - @mixin mx_DialogButton; - margin-right: 8px; - margin-top: -5px; -} - -.mx_RoomHeader_textButton_danger { - background-color: $alert; -} - -.mx_RoomHeader_cancelButton { - cursor: pointer; - padding-left: 12px; - padding-right: 12px; -} - -.mx_RoomHeader_info { - display: flex; - flex: 1; - align-items: center; -} - .mx_RoomHeader_name { flex: 0 1 auto; overflow: hidden; From 93b4ee654b500b4c98c2694fe31d5161f7eb9d4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 09:49:10 +0100 Subject: [PATCH 014/105] Conform more of the codebase to `strictNullChecks` (#10666) * Conform more of the codebase to `strictNullChecks` * Iterate * Iterate * Iterate * Iterate --- src/AddThreepid.ts | 4 +- src/DateUtils.ts | 7 +- src/LegacyCallHandler.tsx | 20 ++--- src/components/structures/FilePanel.tsx | 4 +- src/components/structures/LeftPanel.tsx | 1 + .../structures/LegacyCallEventGrouper.ts | 4 +- src/components/structures/MatrixChat.tsx | 6 +- src/components/structures/MessagePanel.tsx | 19 +++-- src/components/structures/PipContainer.tsx | 8 +- src/components/structures/RightPanel.tsx | 74 ++++++++++--------- src/components/structures/RoomView.tsx | 35 ++++----- src/components/structures/ScrollPanel.tsx | 27 +++---- src/components/structures/TimelinePanel.tsx | 6 +- .../views/rooms/MessageComposerFormatBar.tsx | 2 +- src/components/views/rooms/RoomPreviewBar.tsx | 2 +- src/components/views/rooms/RoomSublist.tsx | 8 +- .../views/rooms/WhoIsTypingTile.tsx | 13 ++-- 17 files changed, 126 insertions(+), 114 deletions(-) diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 5121fdb51a8..fdf0fc65f11 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -293,7 +293,7 @@ export default class AddThreepid { const authClient = new IdentityAuthClient(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); - let result; + let result: { success: boolean } | MatrixError; if (this.submitUrl) { result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl( this.submitUrl, @@ -311,7 +311,7 @@ export default class AddThreepid { } else { throw new UserFriendlyError("The add / bind with MSISDN flow is misconfigured"); } - if (result.errcode) { + if (result instanceof Error) { throw result; } diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 0e392059169..e743b3feead 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -16,6 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Optional } from "matrix-events-sdk"; + import { _t } from "./languageHandler"; function getDaysArray(): string[] { @@ -194,10 +196,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { return prevDate.getFullYear() === nextDate.getFullYear(); } -export function wantsDateSeparator( - prevEventDate: Date | null | undefined, - nextEventDate: Date | null | undefined, -): boolean { +export function wantsDateSeparator(prevEventDate: Optional, nextEventDate: Optional): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index e6a4388e653..f84509238b7 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -288,8 +288,8 @@ export default class LegacyCallHandler extends EventEmitter { this.play(AudioID.Ring); } - public isCallSilenced(callId: string): boolean { - return this.isForcedSilent() || this.silencedCalls.has(callId); + public isCallSilenced(callId?: string): boolean { + return this.isForcedSilent() || (!!callId && this.silencedCalls.has(callId)); } /** @@ -395,6 +395,7 @@ export default class LegacyCallHandler extends EventEmitter { } const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call); + if (!mappedRoomId) return; if (this.getCallForRoom(mappedRoomId)) { logger.log( "Got incoming call for room " + mappedRoomId + " but there's already a call for this room: ignoring", @@ -411,7 +412,8 @@ export default class LegacyCallHandler extends EventEmitter { // the call, we'll be ready to send. NB. This is the protocol-level room ID not // the mapped one: that's where we'll send the events. const cli = MatrixClientPeg.get(); - cli.prepareToEncrypt(cli.getRoom(call.roomId)); + const room = cli.getRoom(call.roomId); + if (room) cli.prepareToEncrypt(room); }; public getCallById(callId: string): MatrixCall | null { @@ -505,7 +507,7 @@ export default class LegacyCallHandler extends EventEmitter { if (this.audioPromises.has(audioId)) { this.audioPromises.set( audioId, - this.audioPromises.get(audioId).then(() => { + this.audioPromises.get(audioId)!.then(() => { audio.load(); return playAudio(); }), @@ -531,7 +533,7 @@ export default class LegacyCallHandler extends EventEmitter { }; if (audio) { if (this.audioPromises.has(audioId)) { - this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(pauseAudio)); + this.audioPromises.set(audioId, this.audioPromises.get(audioId)!.then(pauseAudio)); } else { pauseAudio(); } @@ -546,7 +548,7 @@ export default class LegacyCallHandler extends EventEmitter { // is the call we consider 'the' call for its room. const mappedRoomId = this.roomIdForCall(call); - const callForThisRoom = this.getCallForRoom(mappedRoomId); + const callForThisRoom = mappedRoomId ? this.getCallForRoom(mappedRoomId) : null; return !!callForThisRoom && call.callId === callForThisRoom.callId; } @@ -840,7 +842,7 @@ export default class LegacyCallHandler extends EventEmitter { cancelButton: _t("OK"), onFinished: (allow) => { SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - cli.setFallbackICEServerAllowed(allow); + cli.setFallbackICEServerAllowed(!!allow); }, }, undefined, @@ -898,7 +900,7 @@ export default class LegacyCallHandler extends EventEmitter { // previous calls that are probably stale by now, so just cancel them. if (mappedRoomId !== roomId) { const mappedRoom = MatrixClientPeg.get().getRoom(mappedRoomId); - if (mappedRoom.getPendingEvents().length > 0) { + if (mappedRoom?.getPendingEvents().length) { Resend.cancelUnsentEvents(mappedRoom); } } @@ -933,7 +935,7 @@ export default class LegacyCallHandler extends EventEmitter { } } - public async placeCall(roomId: string, type?: CallType, transferee?: MatrixCall): Promise { + public async placeCall(roomId: string, type: CallType, transferee?: MatrixCall): Promise { // Pause current broadcast, if any SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index a6cce06c354..ba5a68d333d 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -273,7 +273,9 @@ class FilePanel extends React.Component { withoutScrollContainer ref={this.card} > - + {this.card.current && ( + + )} { } private doStickyHeaders(list: HTMLDivElement): void { + if (!list.parentElement) return; const topEdge = list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 2d51b4ee187..d28224729af 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -123,8 +123,8 @@ export default class LegacyCallEventGrouper extends EventEmitter { } public get duration(): number | null { - if (!this.hangup || !this.selectAnswer) return null; - return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime(); + if (!this.hangup?.getDate() || !this.selectAnswer?.getDate()) return null; + return this.hangup.getDate()!.getTime() - this.selectAnswer.getDate()!.getTime(); } /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a503895058b..07590446a3a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -169,7 +169,7 @@ interface IProps { // the initial queryParams extracted from the hash-fragment of the URI startingFragmentQueryParams?: QueryDict; // called when we have completed a token login - onTokenLoginCompleted?: () => void; + onTokenLoginCompleted: () => void; // Represents the screen to display as a result of parsing the initial window.location initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. @@ -1200,7 +1200,7 @@ export default class MatrixChat extends React.PureComponent { // We have to manually update the room list because the forgotten room will not // be notified to us, therefore the room list will have no other way of knowing // the room is forgotten. - RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); + if (room) RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); }) .catch((err) => { const errCode = err.errcode || _td("unknown error code"); @@ -2124,7 +2124,7 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} - defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string} + defaultUsername={this.props.startingFragmentQueryParams?.defaultUsername as string | undefined} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index ab01fad9397..ed6f778bd59 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -24,6 +24,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; +import { Optional } from "matrix-events-sdk"; import shouldHideEvent from "../../shouldHideEvent"; import { wantsDateSeparator } from "../../DateUtils"; @@ -436,10 +437,8 @@ export default class MessagePanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); - } + public scrollToEvent(eventId: string, pixelOffset?: number, offsetBase?: number): void { + this.scrollPanel.current?.scrollToToken(eventId, pixelOffset, offsetBase); } public scrollToEventIfNeeded(eventId: string): void { @@ -590,16 +589,16 @@ export default class MessagePanel extends React.Component { return { nextEventAndShouldShow, nextTile }; } - private get pendingEditItem(): string | undefined { + private get pendingEditItem(): string | null { if (!this.props.room) { - return undefined; + return null; } try { return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); } catch (err) { logger.error(err); - return undefined; + return null; } } @@ -815,7 +814,7 @@ export default class MessagePanel extends React.Component { return ret; } - public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Date): boolean { + public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Optional): boolean { if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { return false; } @@ -1174,7 +1173,7 @@ class CreationGrouper extends BaseGrouper { const ts = createEvent.event.getTs(); ret.push(
  • - +
  • , ); } @@ -1326,7 +1325,7 @@ class MainGrouper extends BaseGrouper { const ts = this.events[0].getTs(); ret.push(
  • - +
  • , ); } diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 5c2d6ef2c68..8ef9b1b8166 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -71,8 +71,8 @@ interface IState { secondaryCall: MatrixCall; // widget candidate to be displayed in the pip view. - persistentWidgetId: string; - persistentRoomId: string; + persistentWidgetId: string | null; + persistentRoomId: string | null; showWidgetInPip: boolean; } @@ -225,7 +225,7 @@ class PipContainerInner extends React.Component { if (callRoomId ?? this.state.persistentRoomId) { dis.dispatch({ action: Action.ViewRoom, - room_id: callRoomId ?? this.state.persistentRoomId, + room_id: callRoomId ?? this.state.persistentRoomId ?? undefined, metricsTrigger: "WebFloatingCallWindow", }); } @@ -318,7 +318,7 @@ class PipContainerInner extends React.Component { pipContent.push(({ onStartMoving }) => ( { } break; case RightPanelPhases.Timeline: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.FilePanel: card = ; break; case RightPanelPhases.ThreadView: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.ThreadPanel: @@ -262,18 +266,22 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.RoomSummary: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.Widget: - card = ; + if (this.props.room) { + card = ; + } break; } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1eaa2cf16e3..4e14733d04b 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -654,7 +654,7 @@ export class RoomView extends React.Component { // and the root event. // The rest will be lost for now, until the aggregation API on the server // becomes available to fetch a whole thread - if (!initialEvent && this.context.client) { + if (!initialEvent && this.context.client && roomId) { initialEvent = (await fetchInitialEvent(this.context.client, roomId, initialEventId)) ?? undefined; } @@ -741,7 +741,7 @@ export class RoomView extends React.Component { // If an event ID wasn't specified, default to the one saved for this room // in the scroll state store. Assume initialEventPixelOffset should be set. - if (!newState.initialEventId) { + if (!newState.initialEventId && newState.roomId) { const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId); if (roomScrollState) { newState.initialEventId = roomScrollState.focussedEvent; @@ -770,7 +770,7 @@ export class RoomView extends React.Component { // callback because this would prevent the setStates from being batched, // ie. cause it to render RoomView twice rather than the once that is necessary. if (initial) { - this.setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek); + this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); } }; @@ -794,13 +794,13 @@ export class RoomView extends React.Component { this.setState({ activeCall }); }; - private getRoomId = (): string => { + private getRoomId = (): string | undefined => { // According to `onRoomViewStoreUpdate`, `state.roomId` can be null // if we have a room alias we haven't resolved yet. To work around this, // first we'll try the room object if it's there, and then fallback to // the bare room ID. (We may want to update `state.roomId` after // resolving aliases, so we could always trust it.) - return this.state.room ? this.state.room.roomId : this.state.roomId; + return this.state.room?.roomId ?? this.state.roomId; }; private getPermalinkCreatorForRoom(room: Room): RoomPermalinkCreator { @@ -1008,7 +1008,7 @@ export class RoomView extends React.Component { SettingsStore.unwatchSetting(watcher); } - if (this.viewsLocalRoom) { + if (this.viewsLocalRoom && this.state.room) { // clean up if this was a local room this.context.client?.store.removeRoom(this.state.room.roomId); } @@ -1188,16 +1188,16 @@ export class RoomView extends React.Component { }; private onLocalRoomEvent(roomId: string): void { - if (roomId !== this.state.room.roomId) return; + if (!this.context.client || !this.state.room || roomId !== this.state.room.roomId) return; createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } private onRoomTimeline = ( ev: MatrixEvent, room: Room | undefined, - toStartOfTimeline: boolean, + toStartOfTimeline: boolean | undefined, removed: boolean, - data?: IRoomTimelineData, + data: IRoomTimelineData, ): void => { if (this.unmounted) return; @@ -1249,6 +1249,7 @@ export class RoomView extends React.Component { }; private handleEffects = (ev: MatrixEvent): void => { + if (!this.state.room) return; const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room); if (!notifState.isUnread) return; @@ -1362,7 +1363,7 @@ export class RoomView extends React.Component { private updatePreviewUrlVisibility({ roomId }: Room): void { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = this.context.client.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; + const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; this.setState({ showUrlPreview: SettingsStore.getValue(key, roomId), }); @@ -1661,7 +1662,7 @@ export class RoomView extends React.Component { this.setState({ rejecting: true, }); - this.context.client.leave(this.state.roomId).then( + this.context.client?.leave(this.state.roomId).then( () => { dis.dispatch({ action: Action.ViewHomePage }); this.setState({ @@ -1691,13 +1692,13 @@ export class RoomView extends React.Component { }); try { - const myMember = this.state.room.getMember(this.context.client.getUserId()); - const inviteEvent = myMember.events.member; - const ignoredUsers = this.context.client.getIgnoredUsers(); - ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk - await this.context.client.setIgnoredUsers(ignoredUsers); + const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); + const inviteEvent = myMember!.events.member; + const ignoredUsers = this.context.client!.getIgnoredUsers(); + ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk + await this.context.client!.setIgnoredUsers(ignoredUsers); - await this.context.client.leave(this.state.roomId); + await this.context.client!.leave(this.state.roomId!); dis.dispatch({ action: Action.ViewHomePage }); this.setState({ rejecting: false, diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index c732f07d3ac..3b380c1d193 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -382,13 +382,12 @@ export default class ScrollPanel extends React.Component { } const itemlist = this.itemlist.current; - const firstTile = itemlist && (itemlist.firstElementChild as HTMLElement); - const contentTop = firstTile && firstTile.offsetTop; + const firstTile = itemlist?.firstElementChild as HTMLElement | undefined; const fillPromises: Promise[] = []; // if scrollTop gets to 1 screen from the top of the first tile, // try backward filling - if (!firstTile || sn.scrollTop - contentTop < sn.clientHeight) { + if (!firstTile || sn.scrollTop - firstTile.offsetTop < sn.clientHeight) { // need to back-fill fillPromises.push(this.maybeFill(depth, true)); } @@ -424,7 +423,7 @@ export default class ScrollPanel extends React.Component { // check if unfilling is possible and send an unfill request if necessary private checkUnfillState(backwards: boolean): void { let excessHeight = this.getExcessHeight(backwards); - if (excessHeight <= 0) { + if (excessHeight <= 0 || !this.itemlist.current) { return; } @@ -617,10 +616,7 @@ export default class ScrollPanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => { - pixelOffset = pixelOffset || 0; - offsetBase = offsetBase || 0; - + public scrollToToken = (scrollToken: string, pixelOffset = 0, offsetBase = 0): void => { // set the trackedScrollToken, so we can get the node through getTrackedNode this.scrollState = { stuckAtBottom: false, @@ -652,6 +648,7 @@ export default class ScrollPanel extends React.Component { const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight); const itemlist = this.itemlist.current; + if (!itemlist) return; const messages = itemlist.children; let node: HTMLElement | null = null; @@ -705,7 +702,7 @@ export default class ScrollPanel extends React.Component { this.bottomGrowth += bottomDiff; scrollState.bottomOffset = newBottomOffset; const newHeight = `${this.getListHeight()}px`; - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } debuglog("balancing height because messages below viewport grew by", bottomDiff); @@ -755,7 +752,7 @@ export default class ScrollPanel extends React.Component { const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } if (sn.scrollTop !== sn.scrollHeight) { @@ -770,7 +767,7 @@ export default class ScrollPanel extends React.Component { // the currently filled piece of the timeline if (trackedNode) { const oldTop = trackedNode.offsetTop; - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } const newTop = trackedNode.offsetTop; @@ -823,9 +820,9 @@ export default class ScrollPanel extends React.Component { private getMessagesHeight(): number { const itemlist = this.itemlist.current; - const lastNode = itemlist.lastElementChild as HTMLElement; + const lastNode = itemlist?.lastElementChild as HTMLElement; const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; - const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0; + const firstNodeTop = (itemlist?.firstElementChild as HTMLElement)?.offsetTop ?? 0; // 18 is itemlist padding return lastNodeBottom - firstNodeTop + 18 * 2; } @@ -865,8 +862,8 @@ export default class ScrollPanel extends React.Component { */ public preventShrinking = (): void => { const messageList = this.itemlist.current; - const tiles = messageList && messageList.children; - if (!messageList) { + const tiles = messageList?.children; + if (!tiles) { return; } let lastTileNode; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 3a28a2a4cd4..6172d6228a2 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -777,7 +777,7 @@ class TimelinePanel extends React.Component { } }; - public canResetTimeline = (): boolean => this.messagePanel?.current?.isAtBottom(); + public canResetTimeline = (): boolean => this.messagePanel?.current?.isAtBottom() === true; private onRoomRedaction = (ev: MatrixEvent, room: Room): void => { if (this.unmounted) return; @@ -1044,7 +1044,7 @@ class TimelinePanel extends React.Component { const sendRRs = SettingsStore.getValue("sendReadReceipts", roomId); debuglog( - `Sending Read Markers for ${this.props.timelineSet.room.roomId}: `, + `Sending Read Markers for ${roomId}: `, `rm=${this.state.readMarkerEventId} `, `rr=${sendRRs ? lastReadEvent?.getId() : null} `, `prr=${lastReadEvent?.getId()}`, @@ -1092,7 +1092,7 @@ class TimelinePanel extends React.Component { // we only do this if we're right at the end, because we're just assuming // that sending an RR for the latest message will set our notif counter // to zero: it may not do this if we send an RR for somewhere before the end. - if (this.isAtEndOfLiveTimeline()) { + if (this.isAtEndOfLiveTimeline() && this.props.timelineSet.room) { this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0); this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); dis.dispatch({ diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index e1a2f9d4bd1..9730806e3e6 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -99,7 +99,7 @@ export default class MessageComposerFormatBar extends React.PureComponent { return {}; } const kickerMember = this.props.room?.currentState.getMember(myMember.events.member.getSender()); - const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender(); + const memberName = kickerMember?.name ?? myMember.events.member?.getSender(); const reason = myMember.events.member?.getContent().reason; return { memberName, reason }; } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 6e471dde428..bca5388d1e3 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -433,9 +433,11 @@ export default class RoomSublist extends React.Component { }; private onHeaderClick = (): void => { - const possibleSticky = this.headerButton.current.parentElement; - const sublist = possibleSticky.parentElement.parentElement; - const list = sublist.parentElement.parentElement; + const possibleSticky = this.headerButton.current?.parentElement; + const sublist = possibleSticky?.parentElement?.parentElement; + const list = sublist?.parentElement?.parentElement; + if (!possibleSticky || !list) return; + // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky const listScrollTop = Math.round(list.scrollTop); const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT); diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 3c23e76482c..2ff8d1d9b46 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -91,7 +91,7 @@ export default class WhoIsTypingTile extends React.Component { private onRoomTimeline = (event: MatrixEvent, room?: Room): void => { if (room?.roomId === this.props.room.roomId) { - const userId = event.getSender(); + const userId = event.getSender()!; // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); if (usersTyping.length !== this.state.usersTyping.length) { @@ -200,14 +200,15 @@ export default class WhoIsTypingTile extends React.Component { } public render(): React.ReactNode { - let usersTyping = this.state.usersTyping; - const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers).map((userId) => - this.props.room.getMember(userId), - ); + const usersTyping = this.state.usersTyping; // append the users that have been reported not typing anymore // but have a timeout timer running so they can disappear // when a message comes in - usersTyping = usersTyping.concat(stoppedUsersOnTimer); + for (const userId in this.state.delayedStopTypingTimers) { + const member = this.props.room.getMember(userId); + if (member) usersTyping.push(member); + } + // sort them so the typing members don't change order when // moved to delayedStopTypingTimers usersTyping.sort((a, b) => compare(a.name, b.name)); From 1efa82917a0cf581db5bb30f202e7c3efde92f00 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 09:25:53 +0100 Subject: [PATCH 015/105] Fix wrong room topic tooltip position (#10667) * Fix wrong room topic tooltip position * Update snapshots * Fix tests --- src/components/views/elements/RoomTopic.tsx | 17 +++- .../views/elements/TooltipTarget.tsx | 92 ++++++++++--------- test/PosthogAnalytics-test.ts | 31 ++++--- .../__snapshots__/RoomView-test.tsx.snap | 44 ++++----- test/utils/MegolmExportEncryption-test.ts | 14 +-- 5 files changed, 101 insertions(+), 97 deletions(-) diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx index eb9ae028a48..02a9bfe2d77 100644 --- a/src/components/views/elements/RoomTopic.tsx +++ b/src/components/views/elements/RoomTopic.tsx @@ -103,10 +103,17 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element { const className = classNames(props.className, "mx_RoomTopic"); return ( -
    - - {body} - -
    + + {body} + ); } diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx index a8c773d1f60..cbd0555d381 100644 --- a/src/components/views/elements/TooltipTarget.tsx +++ b/src/components/views/elements/TooltipTarget.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { HTMLAttributes } from "react"; +import React, { forwardRef, HTMLAttributes } from "react"; import useFocus from "../../../hooks/useFocus"; import useHover from "../../../hooks/useHover"; @@ -29,49 +29,55 @@ interface IProps extends HTMLAttributes, Omit = ({ - children, - tooltipTargetClassName, - // tooltip pass through props - className, - id, - label, - alignment, - tooltipClassName, - maxParentWidth, - ignoreHover, - ...rest -}) => { - const [isFocused, focusProps] = useFocus(); - const [isHovering, hoverProps] = useHover(ignoreHover || (() => false)); +const TooltipTarget = forwardRef( + ( + { + children, + tooltipTargetClassName, + // tooltip pass through props + className, + id, + label, + alignment, + tooltipClassName, + maxParentWidth, + ignoreHover, + ...rest + }, + ref, + ) => { + const [isFocused, focusProps] = useFocus(); + const [isHovering, hoverProps] = useHover(ignoreHover || (() => false)); - // No need to fill up the DOM with hidden tooltip elements. Only add the - // tooltip when we're hovering over the item (performance) - const tooltip = (isFocused || isHovering) && ( - - ); + // No need to fill up the DOM with hidden tooltip elements. Only add the + // tooltip when we're hovering over the item (performance) + const tooltip = (isFocused || isHovering) && ( + + ); - return ( -
    - {children} - {tooltip} -
    - ); -}; + return ( +
    + {children} + {tooltip} +
    + ); + }, +); export default TooltipTarget; diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index e0b47e028ed..61a46a54058 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -52,24 +52,27 @@ describe("PosthogAnalytics", () => { beforeEach(() => { fakePosthog = getFakePosthog(); - window.crypto = { - subtle: { - digest: async (_: AlgorithmIdentifier, encodedMessage: BufferSource) => { - const message = new TextDecoder().decode(encodedMessage); - const hexHash = shaHashes[message]; - const bytes: number[] = []; - for (let c = 0; c < hexHash.length; c += 2) { - bytes.push(parseInt(hexHash.slice(c, c + 2), 16)); - } - return bytes as unknown as ArrayBuffer; + Object.defineProperty(window, "crypto", { + value: { + subtle: { + digest: async (_: AlgorithmIdentifier, encodedMessage: BufferSource) => { + const message = new TextDecoder().decode(encodedMessage); + const hexHash = shaHashes[message]; + const bytes: number[] = []; + for (let c = 0; c < hexHash.length; c += 2) { + bytes.push(parseInt(hexHash.slice(c, c + 2), 16)); + } + return bytes; + }, }, - } as unknown as SubtleCrypto, - } as unknown as Crypto; + }, + }); }); afterEach(() => { - // @ts-ignore - window.crypto = null; + Object.defineProperty(window, "crypto", { + value: null, + }); SdkConfig.unset(); // we touch the config, so clean up }); diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index d32c9a75d7d..b1a6c4ea5d0 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -55,14 +55,11 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
    -
    - -
    +
    @@ -149,14 +146,11 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
    -
    - -
    +
    @@ -339,14 +333,11 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
    -
    - -
    +
    @@ -606,14 +597,11 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
    -
    - -
    +
    diff --git a/test/utils/MegolmExportEncryption-test.ts b/test/utils/MegolmExportEncryption-test.ts index 69d803073f2..e3e6d8ca24c 100644 --- a/test/utils/MegolmExportEncryption-test.ts +++ b/test/utils/MegolmExportEncryption-test.ts @@ -75,13 +75,13 @@ describe("MegolmExportEncryption", function () { let MegolmExportEncryption: typeof MegolmExportEncryptionExport; beforeEach(() => { - window.crypto = { - getRandomValues, - randomUUID: jest.fn().mockReturnValue("not-random-uuid"), - subtle: webCrypto.subtle, - }; - // @ts-ignore for some reason including it in the object above gets ignored - window.crypto.subtle = webCrypto.subtle; + Object.defineProperty(window, "crypto", { + value: { + getRandomValues, + randomUUID: jest.fn().mockReturnValue("not-random-uuid"), + subtle: webCrypto.subtle, + }, + }); MegolmExportEncryption = require("../../src/utils/MegolmExportEncryption"); }); From 6b211dd602ff4eac197f1cb3147bc1faadc6933c Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 20 Apr 2023 09:33:31 +0000 Subject: [PATCH 016/105] Update events.spec.ts - use Cypress Testing Library (#10590) Signed-off-by: Suguru Hirahara --- cypress/e2e/widgets/events.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/widgets/events.spec.ts b/cypress/e2e/widgets/events.spec.ts index d9aeb460625..a3c98b4f6fd 100644 --- a/cypress/e2e/widgets/events.spec.ts +++ b/cypress/e2e/widgets/events.spec.ts @@ -159,7 +159,9 @@ describe("Widget Events", () => { cy.viewRoomByName(ROOM_NAME); // approve capabilities - cy.contains(".mx_WidgetCapabilitiesPromptDialog button", "Approve").click(); + cy.get(".mx_WidgetCapabilitiesPromptDialog").within(() => { + cy.findByRole("button", { name: "Approve" }).click(); + }); cy.all([cy.get("@widgetEventSent"), cy.get("@layoutEventSent")]).then(async () => { // bot creates a new room with 'm.room.topic' From 2cec8b5a4436fc250acf3e0e60cf591d2d43613d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 20 Apr 2023 12:04:51 +0100 Subject: [PATCH 017/105] Add Cypress tests for the "complete security" phase (#10668) * Cypress tests for the "complete security" phase * fix imports --- cypress/e2e/crypto/complete-security.spec.ts | 101 +++++++++++++++++++ cypress/e2e/crypto/crypto.spec.ts | 35 +------ cypress/e2e/crypto/utils.ts | 63 ++++++++++++ 3 files changed, 166 insertions(+), 33 deletions(-) create mode 100644 cypress/e2e/crypto/complete-security.spec.ts create mode 100644 cypress/e2e/crypto/utils.ts diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts new file mode 100644 index 00000000000..0838abd4590 --- /dev/null +++ b/cypress/e2e/crypto/complete-security.spec.ts @@ -0,0 +1,101 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { handleVerificationRequest, waitForVerificationRequest } from "./utils"; +import { CypressBot } from "../../support/bot"; + +describe("Complete security", () => { + let homeserver: HomeserverInstance; + + beforeEach(() => { + cy.startHomeserver("default").then((data) => { + homeserver = data; + }); + // visit the login page of the app, to load the matrix sdk + cy.visit("/#/login"); + + // wait for the page to load + cy.window({ log: false }).should("have.property", "matrixcs"); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + it("should go straight to the welcome screen if we have no signed device", () => { + const username = Cypress._.uniqueId("user_"); + const password = "supersecret"; + cy.registerUser(homeserver, username, password, "Jeff"); + logIntoElement(homeserver.baseUrl, username, password); + cy.findByText("Welcome Jeff"); + }); + + it("should walk through device verification if we have a signed device", () => { + // create a new user, and have it bootstrap cross-signing + let botClient: CypressBot; + cy.getBot(homeserver, { displayName: "Jeff" }) + .then(async (bot) => { + botClient = bot; + await bot.bootstrapCrossSigning({}); + }) + .then(() => { + // now log in, in Element. We go in through the login page because otherwise the device setup flow + // doesn't get triggered + console.log("%cAccount set up; logging in user", "font-weight: bold; font-size:x-large"); + logIntoElement(homeserver.baseUrl, botClient.getSafeUserId(), botClient.__cypress_password); + + // we should see a prompt for a device verification + cy.findByRole("heading", { name: "Verify this device" }); + const botVerificationRequestPromise = waitForVerificationRequest(botClient); + cy.findByRole("button", { name: "Verify with another device" }).click(); + + // accept the verification request on the "bot" side + cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => { + await verificationRequest.accept(); + await handleVerificationRequest(verificationRequest); + }); + + // confirm that the emojis match + cy.findByRole("button", { name: "They match" }).click(); + + // we should get the confirmation box + cy.findByText(/You've successfully verified/); + + cy.findByRole("button", { name: "Got it" }).click(); + }); + }); +}); + +/** + * Fill in the login form in element with the given creds + */ +function logIntoElement(homeserverUrl: string, username: string, password: string) { + cy.visit("/#/login"); + + // select homeserver + cy.findByRole("button", { name: "Edit" }).click(); + cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl); + cy.findByRole("button", { name: "Continue" }).click(); + + // wait for the dialog to go away + cy.get(".mx_ServerPickerDialog").should("not.exist"); + + cy.findByRole("textbox", { name: "Username" }).type(username); + cy.findByPlaceholderText("Password").type(password); + cy.findByRole("button", { name: "Sign in" }).click(); +} diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 99e83da26a4..a415db48bcd 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -16,30 +16,16 @@ limitations under the License. import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; import type { CypressBot } from "../../support/bot"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import Chainable = Cypress.Chainable; import { UserCredentials } from "../../support/login"; +import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils"; -type EmojiMapping = [emoji: string, name: string]; interface CryptoTestContext extends Mocha.Context { homeserver: HomeserverInstance; bob: CypressBot; } -const waitForVerificationRequest = (cli: MatrixClient): Promise => { - return new Promise((resolve) => { - const onVerificationRequestEvent = (request: VerificationRequest) => { - // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here - cli.off("crypto.verification.request", onVerificationRequestEvent); - resolve(request); - }; - // @ts-ignore - cli.on("crypto.verification.request", onVerificationRequestEvent); - }); -}; - const openRoomInfo = () => { cy.get(".mx_RightPanel_roomSummaryButton").click(); return cy.get(".mx_RightPanel"); @@ -117,23 +103,6 @@ function autoJoin(client: MatrixClient) { }); } -const handleVerificationRequest = (request: VerificationRequest): Chainable => { - return cy.wrap( - new Promise((resolve) => { - const onShowSas = (event: ISasEvent) => { - verifier.off("show_sas", onShowSas); - event.confirm(); - verifier.done(); - resolve(event.sas.emoji); - }; - - const verifier = request.beginKeyVerification("m.sas.v1"); - verifier.on("show_sas", onShowSas); - verifier.verify(); - }), - ); -}; - const verify = function (this: CryptoTestContext) { const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob); @@ -150,7 +119,7 @@ const verify = function (this: CryptoTestContext) { .as("bobsVerificationRequest"); cy.findByRole("button", { name: "Verify by emoji" }).click(); cy.get("@bobsVerificationRequest").then((request: VerificationRequest) => { - return handleVerificationRequest(request).then((emojis: EmojiMapping[]) => { + return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => { cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => { emojis.forEach((emoji: EmojiMapping, index: number) => { expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]); diff --git a/cypress/e2e/crypto/utils.ts b/cypress/e2e/crypto/utils.ts new file mode 100644 index 00000000000..6f99a23d0fd --- /dev/null +++ b/cypress/e2e/crypto/utils.ts @@ -0,0 +1,63 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; + +export type EmojiMapping = [emoji: string, name: string]; + +/** + * wait for the given client to receive an incoming verification request + * + * @param cli - matrix client we expect to receive a request + */ +export function waitForVerificationRequest(cli: MatrixClient): Promise { + return new Promise((resolve) => { + const onVerificationRequestEvent = (request: VerificationRequest) => { + // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here + cli.off("crypto.verification.request", onVerificationRequestEvent); + resolve(request); + }; + // @ts-ignore + cli.on("crypto.verification.request", onVerificationRequestEvent); + }); +} + +/** + * Handle an incoming verification request + * + * Starts the key verification process, and, once it is accepted on the other side, confirms that the + * emojis match. + * + * Returns a promise that resolves, with the emoji list, once we confirm the emojis + * + * @param request - incoming verification request + */ +export function handleVerificationRequest(request: VerificationRequest) { + return new Promise((resolve) => { + const onShowSas = (event: ISasEvent) => { + verifier.off("show_sas", onShowSas); + event.confirm(); + verifier.done(); + resolve(event.sas.emoji); + }; + + const verifier = request.beginKeyVerification("m.sas.v1"); + verifier.on("show_sas", onShowSas); + verifier.verify(); + }); +} From 483b53c148ef2a1886b2b2a0830b06c89a05ee4a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 13:18:13 +0100 Subject: [PATCH 018/105] Translate credits in help about section (#10676) --- .../tabs/user/HelpUserSettingsTab.tsx | 134 ++++++++++++------ src/i18n/strings/en_EN.json | 3 + 2 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index b5c1e808693..f939cba64cc 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -137,57 +137,97 @@ export default class HelpUserSettingsTab extends React.Component {_t("Credits")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c64a49abd14..a391253494a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1568,6 +1568,9 @@ "Olm version:": "Olm version:", "Legal": "Legal", "Credits": "Credits", + "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.", + "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.", + "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "For help with using %(brand)s, click here or start a chat with our bot using the button below.", "Chat with %(brand)s Bot": "Chat with %(brand)s Bot", From 467c52a2ae0e47534f0c477d1568618fd4fe54b4 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 20 Apr 2023 13:18:19 +0000 Subject: [PATCH 019/105] Add E2E test - `general-user-settings-tab.spec.ts` (#10658) * Add E2E test: `general-user-settings-tab.spec.ts` Initial implementation Signed-off-by: Suguru Hirahara * lint Signed-off-by: Suguru Hirahara * Check an input area for a new email address too Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- .../general-user-settings-tab.spec.ts | 224 ++++++++++++++++++ .../tabs/user/GeneralUserSettingsTab.tsx | 8 +- 2 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/settings/general-user-settings-tab.spec.ts diff --git a/cypress/e2e/settings/general-user-settings-tab.spec.ts b/cypress/e2e/settings/general-user-settings-tab.spec.ts new file mode 100644 index 00000000000..837ae5aaaa5 --- /dev/null +++ b/cypress/e2e/settings/general-user-settings-tab.spec.ts @@ -0,0 +1,224 @@ +/* +Copyright 2023 Suguru Hirahara + +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 { HomeserverInstance } from "../../plugins/utils/homeserver"; + +const USER_NAME = "Bob"; +const USER_NAME_NEW = "Alice"; +const IntegrationManager = "scalar.vector.im"; + +describe("General user settings tab", () => { + let homeserver: HomeserverInstance; + let userId: string; + + beforeEach(() => { + cy.startHomeserver("default").then((data) => { + homeserver = data; + cy.initTestUser(homeserver, USER_NAME).then((user) => (userId = user.userId)); + cy.tweakConfig({ default_country_code: "US" }); // For checking the international country calling code + }); + cy.openUserSettings("General"); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + it("should be rendered properly", () => { + // Exclude userId from snapshots + const percyCSS = ".mx_ProfileSettings_profile_controls_userId { visibility: hidden !important; }"; + + cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").percySnapshotElement("User settings tab - General", { + percyCSS, + }); + + cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").within(() => { + // Assert that the top heading is rendered + cy.findByTestId("general").should("have.text", "General").should("be.visible"); + + cy.get(".mx_ProfileSettings_profile") + .scrollIntoView() + .within(() => { + // Assert USER_NAME is rendered + cy.findByRole("textbox", { name: "Display Name" }) + .get(`input[value='${USER_NAME}']`) + .should("be.visible"); + + // Assert that a userId is rendered + cy.get(".mx_ProfileSettings_profile_controls_userId").within(() => { + cy.findByText(userId).should("exist"); + }); + + // Check avatar setting + cy.get(".mx_AvatarSetting_avatar") + .should("exist") + .realHover() + .get(".mx_AvatarSetting_avatar_hovering") + .within(() => { + // Hover effect + cy.get(".mx_AvatarSetting_hoverBg").should("exist"); + cy.get(".mx_AvatarSetting_hover span").within(() => { + cy.findByText("Upload").should("exist"); + }); + }); + }); + + // Wait until spinners disappear + cy.get(".mx_GeneralUserSettingsTab_accountSection .mx_Spinner").should("not.exist"); + cy.get(".mx_GeneralUserSettingsTab_discovery .mx_Spinner").should("not.exist"); + + cy.get(".mx_GeneralUserSettingsTab_accountSection").within(() => { + // Assert that input areas for changing a password exists + cy.get("form.mx_GeneralUserSettingsTab_changePassword") + .scrollIntoView() + .within(() => { + cy.findByLabelText("Current password").should("be.visible"); + cy.findByLabelText("New Password").should("be.visible"); + cy.findByLabelText("Confirm password").should("be.visible"); + }); + + // Check email addresses area + cy.get(".mx_EmailAddresses") + .scrollIntoView() + .within(() => { + // Assert that an input area for a new email address is rendered + cy.findByRole("textbox", { name: "Email Address" }).should("be.visible"); + + // Assert the add button is visible + cy.findByRole("button", { name: "Add" }).should("be.visible"); + }); + + // Check phone numbers area + cy.get(".mx_PhoneNumbers") + .scrollIntoView() + .within(() => { + // Assert that an input area for a new phone number is rendered + cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible"); + + // Assert that the add button is rendered + cy.findByRole("button", { name: "Add" }).should("be.visible"); + }); + }); + + // Check language and region setting dropdown + cy.get(".mx_GeneralUserSettingsTab_languageInput") + .scrollIntoView() + .within(() => { + // Check the default value + cy.findByText("English").should("be.visible"); + + // Click the button to display the dropdown menu + cy.findByRole("button", { name: "Language Dropdown" }).click(); + + // Assert that the default option is rendered and highlighted + cy.findByRole("option", { name: /Bahasa Indonesia/ }) + .should("be.visible") + .should("have.class", "mx_Dropdown_option_highlight"); + + // Click again to close the dropdown + cy.findByRole("button", { name: "Language Dropdown" }).click(); + + // Assert that the default value is rendered again + cy.findByText("English").should("be.visible"); + }); + + cy.get("form.mx_SetIdServer") + .scrollIntoView() + .within(() => { + // Assert that an input area for identity server exists + cy.findByRole("textbox", { name: "Enter a new identity server" }).should("be.visible"); + }); + + cy.get(".mx_SetIntegrationManager") + .scrollIntoView() + .within(() => { + cy.contains(".mx_SetIntegrationManager_heading_manager", IntegrationManager).should("be.visible"); + + // Make sure integration manager's toggle switch is enabled + cy.get(".mx_ToggleSwitch_enabled").should("be.visible"); + }); + + // Assert the account deactivation button is displayed + cy.findByTestId("account-management-section") + .scrollIntoView() + .findByRole("button", { name: "Deactivate Account" }) + .should("be.visible") + .should("have.class", "mx_AccessibleButton_kind_danger"); + }); + }); + + it("should support adding and removing a profile picture", () => { + cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => { + // Upload a picture + cy.get(".mx_ProfileSettings_avatarUpload").selectFile("cypress/fixtures/riot.png", { force: true }); + + // Find and click "Remove" link button + cy.get(".mx_ProfileSettings_profile").within(() => { + cy.findByRole("button", { name: "Remove" }).click(); + }); + + // Assert that the link button disappeared + cy.get(".mx_AvatarSetting_avatar .mx_AccessibleButton_kind_link_sm").should("not.exist"); + }); + }); + + it("should set a country calling code based on default_country_code", () => { + // Check phone numbers area + cy.get(".mx_PhoneNumbers") + .scrollIntoView() + .within(() => { + // Assert that an input area for a new phone number is rendered + cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible"); + + // Check a new phone number dropdown menu + cy.get(".mx_PhoneNumbers_country") + .scrollIntoView() + .within(() => { + // Assert that the country calling code of United States is visible + cy.findByText(/\+1/).should("be.visible"); + + // Click the button to display the dropdown menu + cy.findByRole("button", { name: "Country Dropdown" }).click(); + + // Assert that the option for calling code of United Kingdom is visible + cy.findByRole("option", { name: /United Kingdom/ }).should("be.visible"); + + // Click again to close the dropdown + cy.findByRole("button", { name: "Country Dropdown" }).click(); + + // Assert that the default value is rendered again + cy.findByText(/\+1/).should("be.visible"); + }); + + cy.findByRole("button", { name: "Add" }).should("be.visible"); + }); + }); + + it("should support changing a display name", () => { + cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => { + // Change the diaplay name to USER_NAME_NEW + cy.findByRole("textbox", { name: "Display Name" }).type(`{selectAll}{del}${USER_NAME_NEW}{enter}`); + }); + + cy.closeDialog(); + + // Assert the avatar's initial characters are set + cy.get(".mx_UserMenu .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice + cy.get(".mx_RoomView_wrapper .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice + }); +}); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index dc3bf9f408b..233e999afb8 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -468,7 +468,7 @@ export default class GeneralUserSettingsTab extends React.Component +
    {_t("Account management")} {_t("Deactivating your account is a permanent action — be careful!")} @@ -528,8 +528,10 @@ export default class GeneralUserSettingsTab extends React.Component -
    {_t("General")}
    +
    +
    + {_t("General")} +
    {this.renderProfileSection()} {this.renderAccountSection()} {this.renderLanguageSection()} From 0d9fa0515df077d5068cb3dc8d0a4cbbcc4a9191 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 15:44:54 +0100 Subject: [PATCH 020/105] Fix typing tile duplicating users (#10678) --- src/components/views/rooms/WhoIsTypingTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 2ff8d1d9b46..5552fc5c691 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -200,7 +200,7 @@ export default class WhoIsTypingTile extends React.Component { } public render(): React.ReactNode { - const usersTyping = this.state.usersTyping; + const usersTyping = [...this.state.usersTyping]; // append the users that have been reported not typing anymore // but have a timeout timer running so they can disappear // when a message comes in From 2da52372d49cda1d1e932e584d39291d483c7d18 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 15:56:21 +0100 Subject: [PATCH 021/105] Add arrow key controls to emoji and reaction pickers (#10637) * Add arrow key controls to emoji and reaction pickers * Iterate types * Switch to using aria-activedescendant * Add tests * Fix tests * Iterate * Update test * Tweak header keyboard navigation behaviour * Also handle scrolling on left/right arrow keys * Iterate --- cypress/e2e/threads/threads.spec.ts | 2 +- res/css/views/emojipicker/_EmojiPicker.pcss | 8 + src/accessibility/RovingTabIndex.tsx | 8 +- .../roving/RovingAccessibleButton.tsx | 13 +- src/components/structures/ContextMenu.tsx | 2 +- .../views/elements/LazyRenderList.tsx | 2 + src/components/views/emojipicker/Category.tsx | 22 ++- src/components/views/emojipicker/Emoji.tsx | 20 +- .../views/emojipicker/EmojiPicker.tsx | 177 ++++++++++++++---- src/components/views/emojipicker/Header.tsx | 10 +- .../views/emojipicker/QuickReactions.tsx | 8 +- .../views/emojipicker/ReactionPicker.tsx | 1 + src/components/views/emojipicker/Search.tsx | 14 +- src/components/views/rooms/EmojiButton.tsx | 15 +- .../views/emojipicker/EmojiPicker-test.tsx | 49 ++++- 15 files changed, 277 insertions(+), 74 deletions(-) diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 1d36cbdd850..f93d9990c84 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -174,7 +174,7 @@ describe("Threads", () => { .click({ force: true }); // Cypress has no ability to hover cy.get(".mx_EmojiPicker").within(() => { cy.get('input[type="text"]').type("wave"); - cy.contains('[role="menuitem"]', "👋").click(); + cy.contains('[role="gridcell"]', "👋").click(); }); cy.get(".mx_ThreadView").within(() => { diff --git a/res/css/views/emojipicker/_EmojiPicker.pcss b/res/css/views/emojipicker/_EmojiPicker.pcss index c9169dbe7d8..8e78061a11b 100644 --- a/res/css/views/emojipicker/_EmojiPicker.pcss +++ b/res/css/views/emojipicker/_EmojiPicker.pcss @@ -179,6 +179,14 @@ limitations under the License. list-style: none; width: 38px; cursor: pointer; + + &:focus-within { + background-color: $focus-bg-color; + } +} + +.mx_EmojiPicker_body .mx_EmojiPicker_item_wrapper[tabindex="0"] .mx_EmojiPicker_item { + background-color: $focus-bg-color; } .mx_EmojiPicker_item { diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index b449b10710f..7b8cb7ede58 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -61,7 +61,7 @@ export interface IState { refs: Ref[]; } -interface IContext { +export interface IContext { state: IState; dispatch: Dispatch; } @@ -80,7 +80,7 @@ export enum Type { SetFocus = "SET_FOCUS", } -interface IAction { +export interface IAction { type: Type; payload: { ref: Ref; @@ -160,7 +160,7 @@ interface IProps { handleUpDown?: boolean; handleLeftRight?: boolean; children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent): void }): ReactNode; - onKeyDown?(ev: React.KeyboardEvent, state: IState): void; + onKeyDown?(ev: React.KeyboardEvent, state: IState, dispatch: Dispatch): void; } export const findSiblingElement = ( @@ -199,7 +199,7 @@ export const RovingTabIndexProvider: React.FC = ({ const onKeyDownHandler = useCallback( (ev: React.KeyboardEvent) => { if (onKeyDown) { - onKeyDown(ev, context.state); + onKeyDown(ev, context.state, context.dispatch); if (ev.defaultPrevented) { return; } diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 71818c6cda1..28748de73fb 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -22,10 +22,17 @@ import { Ref } from "./types"; interface IProps extends Omit, "inputRef" | "tabIndex"> { inputRef?: Ref; + focusOnMouseOver?: boolean; } // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton: React.FC = ({ inputRef, onFocus, ...props }) => { +export const RovingAccessibleButton: React.FC = ({ + inputRef, + onFocus, + onMouseOver, + focusOnMouseOver, + ...props +}) => { const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); return ( = ({ inputRef, onFocus, .. onFocusInternal(); onFocus?.(event); }} + onMouseOver={(event: React.MouseEvent) => { + if (focusOnMouseOver) onFocusInternal(); + onMouseOver?.(event); + }} inputRef={ref} tabIndex={isActive ? 0 : -1} /> diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 270a0b0a072..8691c6c25d0 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -148,7 +148,7 @@ export default class ContextMenu extends React.PureComponent('[role^="menuitem"]') || - element.querySelector("[tab-index]"); + element.querySelector("[tabindex]"); if (first) { first.focus(); diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx index 0a041730339..802e60ca196 100644 --- a/src/components/views/elements/LazyRenderList.tsx +++ b/src/components/views/elements/LazyRenderList.tsx @@ -73,6 +73,7 @@ interface IProps { element?: string; className?: string; + role?: string; } interface IState { @@ -128,6 +129,7 @@ export default class LazyRenderList extends React.Component, const elementProps = { style: { paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px` }, className: this.props.className, + role: this.props.role, }; return React.createElement(element, elementProps, renderedItems.map(renderItem)); } diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index f4ffce911b5..cf662feea39 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -21,6 +21,7 @@ import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPic import LazyRenderList from "../elements/LazyRenderList"; import { DATA_BY_CATEGORY, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; +import { ButtonEvent } from "../elements/AccessibleButton"; const OVERFLOW_ROWS = 3; @@ -42,18 +43,31 @@ interface IProps { heightBefore: number; viewportHeight: number; scrollTop: number; - onClick(emoji: IEmoji): void; + onClick(ev: ButtonEvent, emoji: IEmoji): void; onMouseEnter(emoji: IEmoji): void; onMouseLeave(emoji: IEmoji): void; isEmojiDisabled?: (unicode: string) => boolean; } +function hexEncode(str: string): string { + let hex: string; + let i: number; + + let result = ""; + for (i = 0; i < str.length; i++) { + hex = str.charCodeAt(i).toString(16); + result += ("000" + hex).slice(-4); + } + + return result; +} + class Category extends React.PureComponent { private renderEmojiRow = (rowIndex: number): JSX.Element => { const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props; const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8); return ( -
    +
    {emojisForRow.map((emoji) => ( { onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} disabled={this.props.isEmojiDisabled?.(emoji.unicode)} + id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`} + role="gridcell" /> ))}
    @@ -101,7 +117,6 @@ class Category extends React.PureComponent { >

    {name}

    { overflowItems={OVERFLOW_ROWS} overflowMargin={0} renderItem={this.renderEmojiRow} + role="grid" /> ); diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 022c29a94a6..62798873034 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -17,36 +17,40 @@ limitations under the License. import React from "react"; -import { MenuItem } from "../../structures/ContextMenu"; import { IEmoji } from "../../../emoji"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; interface IProps { emoji: IEmoji; selectedEmojis?: Set; - onClick(emoji: IEmoji): void; + onClick(ev: ButtonEvent, emoji: IEmoji): void; onMouseEnter(emoji: IEmoji): void; onMouseLeave(emoji: IEmoji): void; disabled?: boolean; + id?: string; + role?: string; } class Emoji extends React.PureComponent { public render(): React.ReactNode { const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; - const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); + const isSelected = selectedEmojis?.has(emoji.unicode); return ( - onClick(emoji)} + onClick(ev, emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" - label={emoji.unicode} disabled={this.props.disabled} + role={this.props.role} + focusOnMouseOver >
    {emoji.unicode}
    -
    + ); } } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index b4a868f474d..7a62c4dd079 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { Dispatch } from "react"; import { _t } from "../../../languageHandler"; import * as recent from "../../../emojipicker/recent"; @@ -25,8 +25,18 @@ import Header from "./Header"; import Search from "./Search"; import Preview from "./Preview"; import QuickReactions from "./QuickReactions"; -import Category, { ICategory, CategoryKey } from "./Category"; +import Category, { CategoryKey, ICategory } from "./Category"; import { filterBoolean } from "../../../utils/arrays"; +import { + IAction as RovingAction, + IState as RovingState, + RovingTabIndexProvider, + Type, +} from "../../../accessibility/RovingTabIndex"; +import { Key } from "../../../Keyboard"; +import { clamp } from "../../../utils/numbers"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import { Ref } from "../../../accessibility/roving/types"; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -37,6 +47,7 @@ const ZERO_WIDTH_JOINER = "\u200D"; interface IProps { selectedEmojis?: Set; onChoose(unicode: string): boolean; + onFinished(): void; isEmojiDisabled?: (unicode: string) => boolean; } @@ -150,6 +161,68 @@ class EmojiPicker extends React.Component { this.updateVisibility(); }; + private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch): void { + const node = state.activeRef.current; + const parent = node.parentElement; + if (!parent) return; + const rowIndex = Array.from(parent.children).indexOf(node); + const refIndex = state.refs.indexOf(state.activeRef); + + let focusRef: Ref | undefined; + let newParent: HTMLElement | undefined; + switch (ev.key) { + case Key.ARROW_LEFT: + focusRef = state.refs[refIndex - 1]; + newParent = focusRef?.current?.parentElement; + break; + + case Key.ARROW_RIGHT: + focusRef = state.refs[refIndex + 1]; + newParent = focusRef?.current?.parentElement; + break; + + case Key.ARROW_UP: + case Key.ARROW_DOWN: { + // For up/down we find the prev/next parent by inspecting the refs either side of our row + const ref = + ev.key === Key.ARROW_UP + ? state.refs[refIndex - rowIndex - 1] + : state.refs[refIndex - rowIndex + EMOJIS_PER_ROW]; + newParent = ref?.current?.parentElement; + const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)]; + focusRef = state.refs.find((r) => r.current === newTarget); + break; + } + } + + if (focusRef) { + dispatch({ + type: Type.SetFocus, + payload: { ref: focusRef }, + }); + + if (parent !== newParent) { + focusRef.current?.scrollIntoView({ + behavior: "auto", + block: "center", + inline: "center", + }); + } + } + + ev.preventDefault(); + ev.stopPropagation(); + } + + private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch): void => { + if ( + state.activeRef?.current && + [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key) + ) { + this.keyboardNavigation(ev, state, dispatch); + } + }; + private updateVisibility = (): void => { const body = this.scrollRef.current?.containerRef.current; if (!body) return; @@ -239,11 +312,11 @@ class EmojiPicker extends React.Component { }; private onEnterFilter = (): void => { - const btn = - this.scrollRef.current?.containerRef.current?.querySelector(".mx_EmojiPicker_item"); - if (btn) { - btn.click(); - } + const btn = this.scrollRef.current?.containerRef.current?.querySelector( + '.mx_EmojiPicker_item_wrapper[tabindex="0"]', + ); + btn?.click(); + this.props.onFinished(); }; private onHoverEmoji = (emoji: IEmoji): void => { @@ -258,10 +331,13 @@ class EmojiPicker extends React.Component { }); }; - private onClickEmoji = (emoji: IEmoji): void => { + private onClickEmoji = (ev: ButtonEvent, emoji: IEmoji): void => { if (this.props.onChoose(emoji.unicode) !== false) { recent.add(emoji.unicode); } + if ((ev as React.KeyboardEvent).key === Key.ENTER) { + this.props.onFinished(); + } }; private static categoryHeightForEmojiCount(count: number): number { @@ -272,41 +348,60 @@ class EmojiPicker extends React.Component { } public render(): React.ReactNode { - let heightBefore = 0; return ( -
    -
    - - - {this.categories.map((category) => { - const emojis = this.memoizedDataByCategory[category.id]; - const categoryElement = ( - + {({ onKeyDownHandler }) => { + let heightBefore = 0; + return ( +
    +
    + - ); - const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length); - heightBefore += height; - return categoryElement; - })} - - {this.state.previewEmoji ? ( - - ) : ( - - )} -
    + + {this.categories.map((category) => { + const emojis = this.memoizedDataByCategory[category.id]; + const categoryElement = ( + + ); + const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length); + heightBefore += height; + return categoryElement; + })} + + {this.state.previewEmoji ? ( + + ) : ( + + )} +
    + ); + }} + ); } } diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx index 9a7005d6324..c3643f6e2a9 100644 --- a/src/components/views/emojipicker/Header.tsx +++ b/src/components/views/emojipicker/Header.tsx @@ -17,6 +17,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; +import { findLastIndex } from "lodash"; import { _t } from "../../../languageHandler"; import { CategoryKey, ICategory } from "./Category"; @@ -40,7 +41,14 @@ class Header extends React.PureComponent { } private changeCategoryRelative(delta: number): void { - const current = this.props.categories.findIndex((c) => c.visible); + let current: number; + // As multiple categories may be visible at once, we want to find the one closest to the relative direction + if (delta < 0) { + current = this.props.categories.findIndex((c) => c.visible); + } else { + // XXX: Switch to Array::findLastIndex once we enable ES2023 + current = findLastIndex(this.props.categories, (c) => c.visible); + } this.changeCategoryAbsolute(current + delta, delta); } diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index 6b149069481..a58c6b875fd 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -20,6 +20,8 @@ import React from "react"; import { _t } from "../../../languageHandler"; import { getEmojiFromUnicode, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import Toolbar from "../../../accessibility/Toolbar"; // We use the variation-selector Heart in Quick Reactions for some reason const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map((emoji) => { @@ -32,7 +34,7 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀 interface IProps { selectedEmojis?: Set; - onClick(emoji: IEmoji): void; + onClick(ev: ButtonEvent, emoji: IEmoji): void; } interface IState { @@ -70,7 +72,7 @@ class QuickReactions extends React.Component { )} -
      + {QUICK_REACTIONS.map((emoji) => ( { selectedEmojis={this.props.selectedEmojis} /> ))} -
    + ); } diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 6b13c768231..97222740f88 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -135,6 +135,7 @@ class ReactionPicker extends React.Component { ); diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index edd6b2c4fca..a34a14cbafd 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -20,14 +20,19 @@ import React from "react"; import { _t } from "../../../languageHandler"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; +import { RovingTabIndexContext } from "../../../accessibility/RovingTabIndex"; interface IProps { query: string; onChange(value: string): void; onEnter(): void; + onKeyDown(event: React.KeyboardEvent): void; } class Search extends React.PureComponent { + public static contextType = RovingTabIndexContext; + public context!: React.ContextType; + private inputRef = React.createRef(); public componentDidMount(): void { @@ -43,11 +48,14 @@ class Search extends React.PureComponent { ev.stopPropagation(); ev.preventDefault(); break; + + default: + this.props.onKeyDown(ev); } }; public render(): React.ReactNode { - let rightButton; + let rightButton: JSX.Element; if (this.props.query) { rightButton = (
    diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index db7accb62c5..b35aa2aef55 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -36,17 +36,14 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP let contextMenu: React.ReactElement | null = null; if (menuDisplayed && button.current) { const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect()); + const onFinished = (): void => { + closeMenu(); + overflowMenuCloser?.(); + }; contextMenu = ( - { - closeMenu(); - overflowMenuCloser?.(); - }} - managed={false} - > - + + ); } diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx index efd09825a9c..4f8c091bb7c 100644 --- a/test/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/components/views/emojipicker/EmojiPicker-test.tsx @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + import EmojiPicker from "../../../../src/components/views/emojipicker/EmojiPicker"; import { stubClient } from "../../../test-utils"; @@ -21,7 +25,7 @@ describe("EmojiPicker", function () { stubClient(); it("sort emojis by shortcode and size", function () { - const ep = new EmojiPicker({ onChoose: (str: String) => false }); + const ep = new EmojiPicker({ onChoose: (str: string) => false, onFinished: jest.fn() }); //@ts-ignore private access ep.onChangeFilter("heart"); @@ -31,4 +35,47 @@ describe("EmojiPicker", function () { //@ts-ignore private access expect(ep.memoizedDataByCategory["people"][1].shortcodes[0]).toEqual("heartbeat"); }); + + it("should allow keyboard navigation using arrow keys", async () => { + // mock offsetParent + Object.defineProperty(HTMLElement.prototype, "offsetParent", { + get() { + return this.parentNode; + }, + }); + + const onChoose = jest.fn(); + const onFinished = jest.fn(); + const { container } = render(); + + const input = container.querySelector("input")!; + expect(input).toHaveFocus(); + + function getEmoji(): string { + const activeDescendant = input.getAttribute("aria-activedescendant"); + return container.querySelector("#" + activeDescendant)!.textContent!; + } + + expect(getEmoji()).toEqual("😀"); + await userEvent.keyboard("[ArrowDown]"); + expect(getEmoji()).toEqual("🙂"); + await userEvent.keyboard("[ArrowUp]"); + expect(getEmoji()).toEqual("😀"); + await userEvent.keyboard("Flag"); + await userEvent.keyboard("[ArrowRight]"); + await userEvent.keyboard("[ArrowRight]"); + expect(getEmoji()).toEqual("📫️"); + await userEvent.keyboard("[ArrowDown]"); + expect(getEmoji()).toEqual("🇦🇨"); + await userEvent.keyboard("[ArrowLeft]"); + expect(getEmoji()).toEqual("📭️"); + await userEvent.keyboard("[ArrowUp]"); + expect(getEmoji()).toEqual("⛳️"); + await userEvent.keyboard("[ArrowRight]"); + expect(getEmoji()).toEqual("📫️"); + await userEvent.keyboard("[Enter]"); + + expect(onChoose).toHaveBeenCalledWith("📫️"); + expect(onFinished).toHaveBeenCalled(); + }); }); From 782060a26e381c8ca5138b140e51989ae9bf314c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 Apr 2023 18:13:30 +0100 Subject: [PATCH 022/105] ARIA Accessibility improvements (#10674) * Add missing aria-expanded attributes * Improve autoComplete for phone numbers & email addresses * Fix room summary card heading order * Fix missing label on timeline search field * Use appropriate semantic elements for dropdown listbox * Use semantic list elements in keyboard settings tab * Use semantic list elements in spotlight * Fix types and i18n * Improve types * Update tests * Add snapshot test --- res/css/views/dialogs/_SpotlightDialog.pcss | 5 + res/css/views/right_panel/_BaseCard.pcss | 3 +- .../views/right_panel/_RoomSummaryCard.pcss | 5 +- .../tabs/user/_KeyboardUserSettingsTab.pcss | 5 + src/components/views/auth/CountryDropdown.tsx | 1 + .../views/dialogs/spotlight/Option.tsx | 14 +- src/components/views/elements/Dropdown.tsx | 20 +- src/components/views/right_panel/BaseCard.tsx | 2 +- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/rooms/SearchBar.tsx | 3 + .../views/settings/account/EmailAddresses.tsx | 2 +- .../views/settings/account/PhoneNumbers.tsx | 2 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 1 + .../tabs/user/AppearanceUserSettingsTab.tsx | 6 +- .../tabs/user/KeyboardUserSettingsTab.tsx | 8 +- .../views/spaces/QuickSettingsButton.tsx | 1 + .../spaces/SpaceSettingsVisibilityTab.tsx | 1 + src/i18n/strings/en_EN.json | 2 + .../views/dialogs/SpotlightDialog-test.tsx | 14 +- .../FilterDropdown-test.tsx.snap | 12 +- .../RoomSummaryCard-test.tsx.snap | 12 +- .../user/AppearanceUserSettingsTab-test.tsx | 37 ++ .../AppearanceUserSettingsTab-test.tsx.snap | 386 ++++++++++++++++++ .../KeyboardUserSettingsTab-test.tsx.snap | 224 +++++----- 24 files changed, 611 insertions(+), 157 deletions(-) create mode 100644 test/components/views/settings/tabs/user/AppearanceUserSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index b9fa6348254..c85d94bf451 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -155,6 +155,11 @@ limitations under the License. overflow-y: auto; padding: $spacing-16; + ul { + padding: 0; + margin: 0; + } + .mx_SpotlightDialog_section { > h4, > .mx_SpotlightDialog_sectionHeader > h4 { diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 22720a99e03..5f700dfbf38 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -151,10 +151,11 @@ limitations under the License. margin-right: $spacing-12; } - > h1 { + > h2 { color: $tertiary-content; font-size: $font-12px; font-weight: 500; + margin: $spacing-12; } .mx_BaseCard_Button { diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index f75743037b0..a138e332ce1 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -19,8 +19,9 @@ limitations under the License. text-align: center; margin-top: $spacing-20; - h2 { + h1 { margin: $spacing-12 0 $spacing-4; + font-weight: $font-semi-bold; } .mx_RoomSummaryCard_alias { @@ -30,7 +31,7 @@ limitations under the License. text-overflow: ellipsis; } - h2, + h1, .mx_RoomSummaryCard_alias { display: -webkit-box; -webkit-line-clamp: 2; diff --git a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss index aa65e6d4943..6f387380f24 100644 --- a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss @@ -16,6 +16,11 @@ limitations under the License. */ .mx_KeyboardUserSettingsTab .mx_SettingsTab_section { + ul { + margin: 0; + padding: 0; + } + .mx_KeyboardShortcut_shortcutRow, .mx_KeyboardShortcut { display: flex; diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index 83f6eca71a4..a9061f6e5af 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -164,6 +164,7 @@ export default class CountryDropdown extends React.Component { searchEnabled={true} disabled={this.props.disabled} label={_t("Country Dropdown")} + autoComplete="tel-country-code" > {options} diff --git a/src/components/views/dialogs/spotlight/Option.tsx b/src/components/views/dialogs/spotlight/Option.tsx index 2bfd2e17a1c..a1dc41a8524 100644 --- a/src/components/views/dialogs/spotlight/Option.tsx +++ b/src/components/views/dialogs/spotlight/Option.tsx @@ -15,18 +15,21 @@ limitations under the License. */ import classNames from "classnames"; -import React, { ComponentProps, ReactNode } from "react"; +import React, { ReactNode, RefObject } from "react"; -import { RovingAccessibleButton } from "../../../../accessibility/roving/RovingAccessibleButton"; import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; -interface OptionProps extends ComponentProps { +interface OptionProps { + inputRef?: RefObject; endAdornment?: ReactNode; + id?: string; + className?: string; + onClick: ((ev: ButtonEvent) => void) | null; } export const Option: React.FC = ({ inputRef, children, endAdornment, className, ...props }) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); return ( = ({ inputRef, children, endAdornment tabIndex={-1} aria-selected={isActive} role="option" + element="li" > {children}
    diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index f603a4b9570..305cee51967 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -30,7 +30,7 @@ interface IMenuOptionProps { highlighted?: boolean; dropdownKey: string; id?: string; - inputRef?: Ref; + inputRef?: Ref; onClick(dropdownKey: string): void; onMouseEnter(dropdownKey: string): void; } @@ -57,7 +57,7 @@ class MenuOption extends React.Component { }); return ( -
    { ref={this.props.inputRef} > {this.props.children} -
    + ); } } @@ -78,6 +78,7 @@ export interface DropdownProps { label: string; value?: string; className?: string; + autoComplete?: string; children: NonEmptyArray; // negative for consistency with HTML disabled?: boolean; @@ -318,21 +319,21 @@ export default class Dropdown extends React.Component { }); if (!options?.length) { return [ -
    +
  • {_t("No results")} -
  • , + , ]; } return options; } public render(): React.ReactNode { - let currentValue; + let currentValue: JSX.Element | undefined; const menuStyle: CSSProperties = {}; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; - let menu; + let menu: JSX.Element | undefined; if (this.state.expanded) { if (this.props.searchEnabled) { currentValue = ( @@ -340,6 +341,7 @@ export default class Dropdown extends React.Component { id={`${this.props.id}_input`} type="text" autoFocus={true} + autoComplete={this.props.autoComplete} className="mx_Dropdown_option" onChange={this.onInputChange} value={this.state.searchQuery} @@ -355,9 +357,9 @@ export default class Dropdown extends React.Component { ); } menu = ( -
    +
      {this.getMenuOptions()} -
    + ); } diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 615792057dd..80e538c0487 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -47,7 +47,7 @@ interface IGroupProps { export const Group: React.FC = ({ className, title, children }) => { return (
    -

    {title}

    +

    {title}

    {children}
    ); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index a32d8da047d..860a58df024 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -318,7 +318,7 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose }) />
    - {(name) =>

    {name}

    }
    + {(name) =>

    {name}

    }
    {alias}
    diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx index c75b340c29a..a4dbfe60ce6 100644 --- a/src/components/views/rooms/SearchBar.tsx +++ b/src/components/views/rooms/SearchBar.tsx @@ -121,6 +121,9 @@ export default class SearchBar extends React.Component { type="text" autoFocus={true} placeholder={_t("Search…")} + aria-label={ + this.state.scope === SearchScope.Room ? _t("Search this room") : _t("Search all rooms") + } onKeyDown={this.onSearchChange} /> { { {this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 7a8157e38c7..f4b05b3631d 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -88,7 +88,11 @@ export default class AppearanceUserSettingsTab extends React.Component this.setState({ showAdvanced: !this.state.showAdvanced })}> + this.setState({ showAdvanced: !this.state.showAdvanced })} + aria-expanded={this.state.showAdvanced} + > {this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")} ); diff --git a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx index cf9a41a5540..ef79b98d483 100644 --- a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx @@ -41,10 +41,10 @@ const KeyboardShortcutRow: React.FC = ({ name }) => { if (!displayName || !value) return null; return ( -
    +
  • {displayName} -
  • + ); }; @@ -59,12 +59,12 @@ const KeyboardShortcutSection: React.FC = ({ cate return (
    {_t(category.categoryLabel)}
    -
    +
      {" "} {category.settingNames.map((shortcutName) => { return ; })}{" "} -
    +
    ); }; diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 1adcd419600..4f9529466b0 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -134,6 +134,7 @@ const QuickSettingsButton: React.FC<{ title={_t("Quick settings")} inputRef={handle} forceHide={!isPanelCollapsed} + aria-expanded={!isPanelCollapsed} > {!isPanelCollapsed ? _t("Settings") : null} diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index 7993a2af642..368d6c96fc0 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -97,6 +97,7 @@ const SpaceSettingsVisibilityTab: React.FC = ({ matrixClient: cli, space onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced" + aria-expanded={showAdvancedSection} > {showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a391253494a..dd9afaf5d75 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2141,6 +2141,8 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", + "Search this room": "Search this room", + "Search all rooms": "Search all rooms", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", diff --git a/test/components/views/dialogs/SpotlightDialog-test.tsx b/test/components/views/dialogs/SpotlightDialog-test.tsx index e587ffbe5cd..fb1fc1e65ff 100644 --- a/test/components/views/dialogs/SpotlightDialog-test.tsx +++ b/test/components/views/dialogs/SpotlightDialog-test.tsx @@ -174,7 +174,7 @@ describe("Spotlight Dialog", () => { expect(filterChip.innerHTML).toContain("Public rooms"); const content = document.querySelector("#mx_SpotlightDialog_content")!; - const options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = content.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBe(1); expect(options[0].innerHTML).toContain(testPublicRoom.name); }); @@ -196,7 +196,7 @@ describe("Spotlight Dialog", () => { expect(filterChip.innerHTML).toContain("People"); const content = document.querySelector("#mx_SpotlightDialog_content")!; - const options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = content.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBeGreaterThanOrEqual(1); expect(options[0]!.innerHTML).toContain(testPerson.display_name); }); @@ -242,7 +242,7 @@ describe("Spotlight Dialog", () => { expect(filterChip.innerHTML).toContain("Public rooms"); const content = document.querySelector("#mx_SpotlightDialog_content")!; - const options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = content.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBe(1); expect(options[0]!.innerHTML).toContain(testPublicRoom.name); @@ -265,7 +265,7 @@ describe("Spotlight Dialog", () => { expect(filterChip.innerHTML).toContain("People"); const content = document.querySelector("#mx_SpotlightDialog_content")!; - const options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = content.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBeGreaterThanOrEqual(1); expect(options[0]!.innerHTML).toContain(testPerson.display_name); }); @@ -324,7 +324,7 @@ describe("Spotlight Dialog", () => { await flushPromisesWithFakeTimers(); const content = document.querySelector("#mx_SpotlightDialog_content")!; - options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + options = content.querySelectorAll("li.mx_SpotlightDialog_option"); }); it("should find Rooms", () => { @@ -350,7 +350,7 @@ describe("Spotlight Dialog", () => { jest.advanceTimersByTime(200); await flushPromisesWithFakeTimers(); - const options = document.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = document.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBeGreaterThanOrEqual(1); expect(options[0]!.innerHTML).toContain(testPerson.display_name); @@ -372,7 +372,7 @@ describe("Spotlight Dialog", () => { await flushPromisesWithFakeTimers(); const content = document.querySelector("#mx_SpotlightDialog_content")!; - const options = content.querySelectorAll("div.mx_SpotlightDialog_option"); + const options = content.querySelectorAll("li.mx_SpotlightDialog_option"); expect(options.length).toBe(1); expect(options[0].innerHTML).toContain(testPublicRoom.name); diff --git a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap index d82ee64cefa..1f1c5dff73c 100644 --- a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders dropdown options in menu 1`] = ` -
    -
    renders dropdown options in menu 1`] = ` Option one
    -
    -
    +
  • renders dropdown options in menu 1`] = ` with description
  • -
    -
    + + `; exports[` renders selected option 1`] = ` diff --git a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index 23f6e600874..ee66c73a689 100644 --- a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -44,11 +44,11 @@ exports[` renders the room summary 1`] = ` tabindex="0" /> -

    !room:domain.org -

    +
    renders the room summary 1`] = `
    -

    +

    About -

    +
    renders the room summary 1`] = `
    -

    +

    Widgets -

    +
    Room
    -
    +
      -
      Search (must be enabled) @@ -445,8 +445,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
      -
    -
    +
  • Upload a file @@ -471,8 +471,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • -
    -
    +
  • Dismiss read marker and jump to bottom @@ -485,8 +485,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • -
    -
    +
  • Jump to oldest unread message @@ -505,8 +505,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • -
    -
    +
  • Scroll up in the timeline @@ -519,8 +519,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • -
    -
    +
  • Scroll down in the timeline @@ -533,8 +533,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Jump to first message @@ -553,8 +553,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Jump to last message @@ -573,9 +573,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - + - +
    Room List
    -
    +
      -
      Select room from the room list @@ -600,8 +600,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
      -
    -
    +
  • Collapse room list section @@ -614,8 +614,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Expand room list section @@ -628,8 +628,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Navigate down in the room list @@ -642,8 +642,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Navigate up in the room list @@ -656,9 +656,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - + - +
    Accessibility
    -
    +
      -
      Close dialog or context menu @@ -683,8 +683,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
      -
    -
    +
  • Activate selected button @@ -697,9 +697,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - + - +
    Navigation
    -
    +
      -
      Toggle the top left menu @@ -730,8 +730,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
      -
    -
    +
  • Toggle right panel @@ -750,8 +750,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Toggle space panel @@ -776,8 +776,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Open this settings tab @@ -796,8 +796,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Go to Home View @@ -822,8 +822,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Jump to room search @@ -842,8 +842,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Next unread room or DM @@ -868,8 +868,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Previous unread room or DM @@ -894,8 +894,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Next room or DM @@ -914,8 +914,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Previous room or DM @@ -934,9 +934,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - + - +
    Autocomplete
    -
    +
      -
      Cancel autocomplete @@ -961,8 +961,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
      -
    -
    +
  • Next autocomplete suggestion @@ -975,8 +975,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Previous autocomplete suggestion @@ -989,8 +989,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Complete @@ -1003,8 +1003,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - -
    +
  • Force complete @@ -1017,9 +1017,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
  • - + - + From 5445ee85cd0e5e81ed3cc48c08b6e4c0b2820c6e Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 20 Apr 2023 21:36:44 +0000 Subject: [PATCH 023/105] Update spaces.spec.ts - use Cypress Testing Library (#10620) Signed-off-by: Suguru Hirahara --- cypress/e2e/spaces/spaces.spec.ts | 146 ++++++++++++++++++------------ 1 file changed, 89 insertions(+), 57 deletions(-) diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts index f89fa297d01..9b1fb241d0d 100644 --- a/cypress/e2e/spaces/spaces.spec.ts +++ b/cypress/e2e/spaces/spaces.spec.ts @@ -24,7 +24,7 @@ import Chainable = Cypress.Chainable; import { UserCredentials } from "../../support/login"; function openSpaceCreateMenu(): Chainable { - cy.get(".mx_SpaceButton_new").click(); + cy.findByRole("button", { name: "Create a space" }).click(); return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu"); } @@ -83,64 +83,72 @@ describe("Spaces", () => { openSpaceCreateMenu(); cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu"); cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => { - cy.get(".mx_SpaceCreateMenuType_public").click(); + // Regex pattern due to strings of "mx_SpaceCreateMenuType_public" + cy.findByRole("button", { name: /Public/ }).click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile( "cypress/fixtures/riot.png", { force: true }, ); - cy.get('input[label="Name"]').type("Let's have a Riot"); - cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot"); - cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!"); - cy.contains(".mx_AccessibleButton", "Create").click(); + cy.findByRole("textbox", { name: "Name" }).type("Let's have a Riot"); + cy.findByRole("textbox", { name: "Address" }).should("have.value", "lets-have-a-riot"); + cy.findByRole("textbox", { name: "Description" }).type("This is a space to reminisce Riot.im!"); + cy.findByRole("button", { name: "Create" }).click(); }); // Create the default General & Random rooms, as well as a custom "Jokes" room - cy.get('input[label="Room name"][value="General"]').should("exist"); - cy.get('input[label="Room name"][value="Random"]').should("exist"); - cy.get('input[placeholder="Support"]').type("Jokes"); - cy.contains(".mx_AccessibleButton", "Continue").click(); + cy.findByPlaceholderText("General").should("exist"); + cy.findByPlaceholderText("Random").should("exist"); + cy.findByPlaceholderText("Support").type("Jokes"); + cy.findByRole("button", { name: "Continue" }).click(); // Copy matrix.to link - cy.get(".mx_SpacePublicShare_shareButton").focus().realClick(); + // Regex pattern due to strings of "mx_SpacePublicShare_shareButton" + cy.findByRole("button", { name: /Share invite link/ }).realClick(); cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost"); // Go to space home - cy.contains(".mx_AccessibleButton", "Go to my first room").click(); + cy.findByRole("button", { name: "Go to my first room" }).click(); // Assert rooms exist in the room list - cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist"); - cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist"); - cy.contains(".mx_RoomList .mx_RoomTile", "Jokes").should("exist"); + cy.findByRole("treeitem", { name: "General" }).should("exist"); + cy.findByRole("treeitem", { name: "Random" }).should("exist"); + cy.findByRole("treeitem", { name: "Jokes" }).should("exist"); }); it("should allow user to create private space", () => { openSpaceCreateMenu().within(() => { - cy.get(".mx_SpaceCreateMenuType_private").click(); + // Regex pattern due to strings of "mx_SpaceCreateMenuType_private" + cy.findByRole("button", { name: /Private/ }).click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile( "cypress/fixtures/riot.png", { force: true }, ); - cy.get('input[label="Name"]').type("This is not a Riot"); - cy.get('input[label="Address"]').should("not.exist"); - cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im..."); - cy.contains(".mx_AccessibleButton", "Create").click(); + cy.findByRole("textbox", { name: "Name" }).type("This is not a Riot"); + cy.findByRole("textbox", { name: "Address" }).should("not.exist"); + cy.findByRole("textbox", { name: "Description" }).type("This is a private space of mourning Riot.im..."); + cy.findByRole("button", { name: "Create" }).click(); }); - cy.get(".mx_SpaceRoomView_privateScope_meAndMyTeammatesButton").click(); + // Regex pattern due to strings of "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton" + cy.findByRole("button", { name: /Me and my teammates/ }).click(); // Create the default General & Random rooms, as well as a custom "Projects" room - cy.get('input[label="Room name"][value="General"]').should("exist"); - cy.get('input[label="Room name"][value="Random"]').should("exist"); - cy.get('input[placeholder="Support"]').type("Projects"); - cy.contains(".mx_AccessibleButton", "Continue").click(); - - cy.get(".mx_SpaceRoomView").should("contain", "Invite your teammates"); - cy.contains(".mx_AccessibleButton", "Skip for now").click(); + cy.findByPlaceholderText("General").should("exist"); + cy.findByPlaceholderText("Random").should("exist"); + cy.findByPlaceholderText("Support").type("Projects"); + cy.findByRole("button", { name: "Continue" }).click(); + + cy.get(".mx_SpaceRoomView").within(() => { + cy.get("h1").findByText("Invite your teammates"); + cy.findByRole("button", { name: "Skip for now" }).click(); + }); // Assert rooms exist in the room list - cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist"); - cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist"); - cy.contains(".mx_RoomList .mx_RoomTile", "Projects").should("exist"); + cy.findByRole("treeitem", { name: "General" }).should("exist"); + cy.findByRole("treeitem", { name: "Random" }).should("exist"); + cy.findByRole("treeitem", { name: "Projects" }).should("exist"); // Assert rooms exist in the space explorer cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "General").should("exist"); @@ -154,23 +162,32 @@ describe("Spaces", () => { }); openSpaceCreateMenu().within(() => { - cy.get(".mx_SpaceCreateMenuType_private").click(); + // Regex pattern due to strings of "mx_SpaceCreateMenuType_private" + cy.findByRole("button", { name: /Private/ }).click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile( "cypress/fixtures/riot.png", { force: true }, ); - cy.get('input[label="Address"]').should("not.exist"); - cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im..."); - cy.get('input[label="Name"]').type("This is my Riot{enter}"); + cy.findByRole("textbox", { name: "Address" }).should("not.exist"); + cy.findByRole("textbox", { name: "Description" }).type("This is a personal space to mourn Riot.im..."); + cy.findByRole("textbox", { name: "Name" }).type("This is my Riot{enter}"); }); - cy.get(".mx_SpaceRoomView_privateScope_justMeButton").click(); + // Regex pattern due to of strings of "mx_SpaceRoomView_privateScope_justMeButton" + cy.findByRole("button", { name: /Just me/ }).click(); + + cy.findByText("Sample Room").click({ force: true }); // force click as checkbox size is zero - cy.get(".mx_AddExistingToSpace_entry").click(); - cy.contains(".mx_AccessibleButton", "Add").click(); + // Temporal implementation as multiple elements with the role "button" and name "Add" are found + cy.get(".mx_AddExistingToSpace_footer").within(() => { + cy.findByRole("button", { name: "Add" }).click(); + }); - cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist"); - cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist"); + cy.get(".mx_SpaceHierarchy_list").within(() => { + // Regex pattern due to the strings of "mx_SpaceHierarchy_roomTile_joined" + cy.findByRole("treeitem", { name: /Sample Room/ }).should("exist"); + }); }); it("should allow user to invite another to a space", () => { @@ -185,20 +202,24 @@ describe("Spaces", () => { }).as("spaceId"); openSpaceContextMenu("#space:localhost").within(() => { - cy.get('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]').click(); + cy.findByRole("menuitem", { name: "Invite" }).click(); }); cy.get(".mx_SpacePublicShare").within(() => { // Copy link first - cy.get(".mx_SpacePublicShare_shareButton").focus().realClick(); + // Regex pattern due to strings of "mx_SpacePublicShare_shareButton" + cy.findByRole("button", { name: /Share invite link/ }) + .focus() + .realClick(); cy.getClipboardText().should("eq", "https://matrix.to/#/#space:localhost"); // Start Matrix invite flow - cy.get(".mx_SpacePublicShare_inviteButton").click(); + // Regex pattern due to strings of "mx_SpacePublicShare_inviteButton" + cy.findByRole("button", { name: /Invite people/ }).click(); }); cy.get(".mx_InviteDialog_other").within(() => { - cy.get('input[type="text"]').type(bot.getUserId()); - cy.contains(".mx_AccessibleButton", "Invite").click(); + cy.findByRole("textbox").type(bot.getUserId()); + cy.findByRole("button", { name: "Invite" }).click(); }); cy.get(".mx_InviteDialog_other").should("not.exist"); @@ -219,7 +240,7 @@ describe("Spaces", () => { .should("exist") .parent() .next() - .find('.mx_SpaceButton[aria-label="My Space"]') + .findByRole("button", { name: "My Space" }) .should("exist"); }); @@ -243,8 +264,11 @@ describe("Spaces", () => { cy.viewSpaceHomeByName(spaceName); }); cy.get(".mx_SpaceRoomView .mx_SpaceHierarchy_list").within(() => { - cy.contains(".mx_SpaceHierarchy_roomTile", "Music").should("exist"); - cy.contains(".mx_SpaceHierarchy_roomTile", "Gaming").should("exist"); + // Regex pattern due to strings in "mx_SpaceHierarchy_roomTile_name" + cy.findByRole("treeitem", { name: /Music/ }).findByRole("button").should("exist"); + cy.findByRole("treeitem", { name: /Gaming/ }) + .findByRole("button") + .should("exist"); }); }); @@ -260,8 +284,12 @@ describe("Spaces", () => { initial_state: [spaceChildInitialState(spaceId)], }).as("spaceId"); }); - cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist"); - cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Child Space"]').should("not.exist"); + + // Find collapsed Space panel + cy.findByRole("tree", { name: "Spaces" }).within(() => { + cy.findByRole("button", { name: "Root Space" }).should("exist"); + cy.findByRole("button", { name: "Child Space" }).should("not.exist"); + }); const axeOptions = { rules: { @@ -274,8 +302,12 @@ describe("Spaces", () => { cy.checkA11y(undefined, axeOptions); cy.get(".mx_SpacePanel").percySnapshotElement("Space panel collapsed", { widths: [68] }); - cy.get(".mx_SpaceButton_toggleCollapse").click({ force: true }); - cy.get(".mx_SpacePanel:not(.collapsed)").should("exist"); + cy.findByRole("tree", { name: "Spaces" }).within(() => { + // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another + // button with the same name with different class name "mx_SpacePanel_toggleCollapse". + cy.findByRole("button", { name: "Expand" }).realHover().click(); + }); + cy.get(".mx_SpacePanel:not(.collapsed)").should("exist"); // TODO: replace :not() selector cy.contains(".mx_SpaceItem", "Root Space") .should("exist") @@ -300,12 +332,12 @@ describe("Spaces", () => { cy.getSpacePanelButton("Test Space").should("exist"); cy.wait(500); // without this we can end up clicking too quickly and it ends up having no effect cy.viewSpaceByName("Test Space"); - cy.contains(".mx_AccessibleButton", "Accept").click(); + cy.findByRole("button", { name: "Accept" }).click(); - cy.contains(".mx_SpaceHierarchy_roomTile.mx_AccessibleButton", "Test Room").within(() => { - cy.contains("Join").should("exist").realHover().click(); - cy.contains("View", { timeout: 5000 }).should("exist").click(); - }); + // Regex pattern due to strings in "mx_SpaceHierarchy_roomTile_item" + cy.findByRole("button", { name: /Test Room/ }).realHover(); + cy.findByRole("button", { name: "Join" }).should("exist").realHover().click(); + cy.findByRole("button", { name: "View", timeout: 5000 }).should("exist").realHover().click(); // Assert we get shown the new room intro, and thus not the soft crash screen cy.get(".mx_NewRoomIntro").should("exist"); From 7a914e73f89d1afafe674896b94143f0148d31a2 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 21 Apr 2023 10:35:44 +1200 Subject: [PATCH 024/105] hack to fix console noise from unfaked timers and clearAllModals (#10660) --- test/test-utils/utilities.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 420e5d3bca2..6590ef52306 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -211,6 +211,17 @@ export const clearAllModals = async (): Promise => { // of removing the same modal because the promises don't flush otherwise. // // XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead. - await flushPromisesWithFakeTimers(); + + // this is called in some places where timers are not faked + // which causes a lot of noise in the console + // to make a hack even hackier check if timers are faked using a weird trick from github + // then call the appropriate promise flusher + // https://github.com/facebook/jest/issues/10555#issuecomment-1136466942 + const jestTimersFaked = setTimeout.name === "setTimeout"; + if (jestTimersFaked) { + await flushPromisesWithFakeTimers(); + } else { + await flushPromises(); + } } }; From 05ef1d5560ccc5f8095564c7d9ada4641150a566 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 21 Apr 2023 09:32:26 +0000 Subject: [PATCH 025/105] Update `sliding-sync.spec.ts` - use Cypress Testing Library (#10618) * Update sliding-sync.spec.ts - use Cypress Testing Library Signed-off-by: Suguru Hirahara * Use findByRole() - group, treeitem The elements with ARIA "treeitem" role resides in ones with ARIA "group" role such as Favourites, People, and Invites. The elements with the "treeitem" role correspond to rooms on the room list. Signed-off-by: Suguru Hirahara * Use the library more - 'should render the Rooms list in reverse chronological order by default and allowing sorting A-Z' Signed-off-by: Suguru Hirahara * Use cy.findAllByRole("treeitem") Note the Test room is excluded from being counted thanks to within(). Signed-off-by: Suguru Hirahara * Rename the rooms to avoid confusion Since it has been unclear "Join" etc. is a verb or a room name, the room names are changed as below: - Join -> Room to Join - Reject -> Room to Reject (the invite) - Rescind -> Room to Rescind (the invite) Signed-off-by: Suguru Hirahara * Specify ARIA label for the room sublist headers Have different ARIA labels specified for "mx_RoomSublist" and "mx_RoomSublist_headerContainer" to clarify the structure. Change the test to check the new ARIA label. Signed-off-by: Suguru Hirahara * lint Signed-off-by: Suguru Hirahara * Fix a race condition Signed-off-by: Suguru Hirahara * Revert "Specify ARIA label for the room sublist headers" This reverts commit 193a47de4c0fac4139d7c689fa020d6f0acc6819. Signed-off-by: Suguru Hirahara * Fix realHover() target Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- cypress/e2e/sliding-sync/sliding-sync.ts | 187 +++++++++++++++-------- 1 file changed, 122 insertions(+), 65 deletions(-) diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index 10bd2467970..6caa01a9903 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -62,7 +62,7 @@ describe("Sliding Sync", () => { // assert order const checkOrder = (wantOrder: string[]) => { - cy.contains(".mx_RoomSublist", "Rooms") + cy.findByRole("group", { name: "Rooms" }) .find(".mx_RoomTile_title") .should((elements) => { expect( @@ -102,16 +102,31 @@ describe("Sliding Sync", () => { it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => { // create rooms and check room names are correct - cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple")); - cy.createRoom({ name: "Pineapple" }).then(() => cy.contains(".mx_RoomSublist", "Pineapple")); - cy.createRoom({ name: "Orange" }).then(() => cy.contains(".mx_RoomSublist", "Orange")); - // check the rooms are in the right order - cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach + cy.createRoom({ name: "Apple" }).then(() => cy.findByRole("treeitem", { name: "Apple" })); + cy.createRoom({ name: "Pineapple" }).then(() => cy.findByRole("treeitem", { name: "Pineapple" })); + cy.createRoom({ name: "Orange" }).then(() => cy.findByRole("treeitem", { name: "Orange" })); + + cy.get(".mx_RoomSublist_tiles").within(() => { + cy.findAllByRole("treeitem").should("have.length", 4); // due to the Test Room in beforeEach + }); + checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]); - cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomSublist_menuButton").click({ force: true }); - cy.contains("A-Z").click(); - cy.get(".mx_StyledRadioButton_checked").should("contain.text", "A-Z"); + cy.findByRole("group", { name: "Rooms" }).within(() => { + cy.get(".mx_RoomSublist_headerContainer") + .realHover() + .findByRole("button", { name: "List options" }) + .click(); + }); + + // force click as the radio button's size is zero + cy.findByRole("menuitemradio", { name: "A-Z" }).click({ force: true }); + + // Assert that the radio button is checked + cy.get(".mx_StyledRadioButton_checked").within(() => { + cy.findByText("A-Z").should("exist"); + }); + checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]); }); @@ -119,16 +134,16 @@ describe("Sliding Sync", () => { // create rooms and check room names are correct cy.createRoom({ name: "Apple" }) .as("roomA") - .then(() => cy.contains(".mx_RoomSublist", "Apple")); + .then(() => cy.findByRole("treeitem", { name: "Apple" })); cy.createRoom({ name: "Pineapple" }) .as("roomP") - .then(() => cy.contains(".mx_RoomSublist", "Pineapple")); + .then(() => cy.findByRole("treeitem", { name: "Pineapple" })); cy.createRoom({ name: "Orange" }) .as("roomO") - .then(() => cy.contains(".mx_RoomSublist", "Orange")); + .then(() => cy.findByRole("treeitem", { name: "Orange" })); // Select the Test Room - cy.contains(".mx_RoomTile", "Test Room").click(); + cy.findByRole("treeitem", { name: "Test Room" }).click(); checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]); bumpRoom("@roomA"); @@ -145,20 +160,20 @@ describe("Sliding Sync", () => { // create rooms and check room names are correct cy.createRoom({ name: "Apple" }) .as("roomA") - .then(() => cy.contains(".mx_RoomSublist", "Apple")); + .then(() => cy.findByRole("treeitem", { name: "Apple" })); cy.createRoom({ name: "Pineapple" }) .as("roomP") - .then(() => cy.contains(".mx_RoomSublist", "Pineapple")); + .then(() => cy.findByRole("treeitem", { name: "Pineapple" })); cy.createRoom({ name: "Orange" }) .as("roomO") - .then(() => cy.contains(".mx_RoomSublist", "Orange")); + .then(() => cy.findByRole("treeitem", { name: "Orange" })); // Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should // turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically // be Apple, Orange Pineapple - only when you click on a different room do things reshuffle. // Select the Pineapple room - cy.contains(".mx_RoomTile", "Pineapple").click(); + cy.findByRole("treeitem", { name: "Pineapple" }).click(); checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]); // Move Apple @@ -166,7 +181,7 @@ describe("Sliding Sync", () => { checkOrder(["Apple", "Pineapple", "Orange", "Test Room"]); // Select the Test Room - cy.contains(".mx_RoomTile", "Test Room").click(); + cy.findByRole("treeitem", { name: "Test Room" }).click(); // the rooms reshuffle to match reality checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]); @@ -181,19 +196,22 @@ describe("Sliding Sync", () => { }); // check that there is an unread notification (grey) as 1 - cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "1"); + cy.findByRole("treeitem", { name: "Test Room 1 unread message." }).contains(".mx_NotificationBadge_count", "1"); cy.get(".mx_NotificationBadge").should("not.have.class", "mx_NotificationBadge_highlighted"); // send an @mention: highlight count (red) should be 2. cy.all([cy.get("@roomId"), cy.get("@bob")]).then(([roomId, bob]) => { return bob.sendTextMessage(roomId, "Hello Sloth"); }); - cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "2"); + cy.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).contains( + ".mx_NotificationBadge_count", + "2", + ); cy.get(".mx_NotificationBadge").should("have.class", "mx_NotificationBadge_highlighted"); // click on the room, the notif counts should disappear - cy.contains(".mx_RoomTile", "Test Room").click(); - cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count"); + cy.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).click(); + cy.findByRole("treeitem", { name: "Test Room" }).should("not.have.class", "mx_NotificationBadge_count"); }); it("should not show unread indicators", () => { @@ -201,8 +219,11 @@ describe("Sliding Sync", () => { createAndJoinBob(); // disable notifs in this room (TODO: CS API call?) - cy.contains(".mx_RoomTile", "Test Room").find(".mx_RoomTile_notificationsButton").click({ force: true }); - cy.contains("Mute room").click(); + cy.findByRole("treeitem", { name: "Test Room" }) + .realHover() + .findByRole("button", { name: "Notification options" }) + .click(); + cy.findByRole("menuitemradio", { name: "Mute room" }).click(); // create a new room so we know when the message has been received as it'll re-shuffle the room list cy.createRoom({ @@ -216,13 +237,13 @@ describe("Sliding Sync", () => { // wait for this message to arrive, tell by the room list resorting checkOrder(["Test Room", "Dummy"]); - cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist"); + cy.findByRole("treeitem", { name: "Test Room" }).get(".mx_NotificationBadge").should("not.exist"); }); it("should update user settings promptly", () => { - cy.get(".mx_UserMenu_userAvatar").click(); - cy.contains("All settings").click(); - cy.contains("Preferences").click(); + cy.findByRole("button", { name: "User menu" }).click(); + cy.findByRole("menuitem", { name: "All settings" }).click(); + cy.findByRole("button", { name: "Preferences" }).click(); cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format") .should("exist") .find(".mx_ToggleSwitch_on") @@ -257,9 +278,9 @@ describe("Sliding Sync", () => { .then((bob) => { bobClient = bob; return Promise.all([ - bob.createRoom({ name: "Join" }), - bob.createRoom({ name: "Reject" }), - bob.createRoom({ name: "Rescind" }), + bob.createRoom({ name: "Room to Join" }), + bob.createRoom({ name: "Room to Reject" }), + bob.createRoom({ name: "Room to Rescind" }), ]); }) .then(([join, reject, rescind]) => { @@ -273,23 +294,44 @@ describe("Sliding Sync", () => { ]); }); - // wait for them all to be on the UI - cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach + cy.findByRole("group", { name: "Invites" }).within(() => { + // Exclude headerText + cy.get(".mx_RoomSublist_tiles").within(() => { + // Wait for them all to be on the UI + cy.findAllByRole("treeitem").should("have.length", 3); + }); + }); + + // Select the room to join + cy.findByRole("treeitem", { name: "Room to Join" }).click(); - cy.contains(".mx_RoomTile", "Join").click(); - cy.contains(".mx_AccessibleButton", "Accept").click(); + cy.get(".mx_RoomView").within(() => { + // Accept the invite + cy.findByRole("button", { name: "Accept" }).click(); + }); - checkOrder(["Join", "Test Room"]); + checkOrder(["Room to Join", "Test Room"]); - cy.contains(".mx_RoomTile", "Reject").click(); - cy.contains(".mx_RoomView .mx_AccessibleButton", "Reject").click(); + // Select the room to reject + cy.findByRole("treeitem", { name: "Room to Reject" }).click(); - // wait for the rejected room to disappear - cy.get(".mx_RoomTile").should("have.length", 3); + cy.get(".mx_RoomView").within(() => { + // Reject the invite + cy.findByRole("button", { name: "Reject" }).click(); + }); + + cy.findByRole("group", { name: "Invites" }).within(() => { + // Exclude headerText + cy.get(".mx_RoomSublist_tiles").within(() => { + // Wait for the rejected room to disappear + cy.findAllByRole("treeitem").should("have.length", 2); + }); + }); // check the lists are correct - checkOrder(["Join", "Test Room"]); - cy.contains(".mx_RoomSublist", "Invites") + checkOrder(["Room to Join", "Test Room"]); + + cy.findByRole("group", { name: "Invites" }) .find(".mx_RoomTile_title") .should((elements) => { expect( @@ -297,7 +339,7 @@ describe("Sliding Sync", () => { return e.textContent; }), "rooms are sorted", - ).to.deep.equal(["Rescind"]); + ).to.deep.equal(["Room to Rescind"]); }); // now rescind the invite @@ -305,9 +347,15 @@ describe("Sliding Sync", () => { return bob.kick(roomRescind, clientUserId); }); - // wait for the rescind to take effect and check the joined list once more - cy.get(".mx_RoomTile").should("have.length", 2); - checkOrder(["Join", "Test Room"]); + cy.findByRole("group", { name: "Rooms" }).within(() => { + // Exclude headerText + cy.get(".mx_RoomSublist_tiles").within(() => { + // Wait for the rescind to take effect and check the joined list once more + cy.findAllByRole("treeitem").should("have.length", 2); + }); + }); + + checkOrder(["Room to Join", "Test Room"]); }); it("should show a favourite DM only in the favourite sublist", () => { @@ -320,8 +368,8 @@ describe("Sliding Sync", () => { cy.getClient().then((cli) => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 })); }); - cy.contains('.mx_RoomSublist[aria-label="Favourites"] .mx_RoomTile', "Favourite DM").should("exist"); - cy.contains('.mx_RoomSublist[aria-label="People"] .mx_RoomTile', "Favourite DM").should("not.exist"); + cy.findByRole("group", { name: "Favourites" }).findByText("Favourite DM").should("exist"); + cy.findByRole("group", { name: "People" }).findByText("Favourite DM").should("not.exist"); }); // Regression test for a bug in SS mode, but would be useful to have in non-SS mode too. @@ -329,7 +377,7 @@ describe("Sliding Sync", () => { it("should clear the reply to field when swapping rooms", () => { cy.createRoom({ name: "Other Room" }) .as("roomA") - .then(() => cy.contains(".mx_RoomSublist", "Other Room")); + .then(() => cy.findByRole("treeitem", { name: "Other Room" })); cy.get("@roomId").then((roomId) => { return cy.sendEvent(roomId, null, "m.room.message", { body: "Hello world", @@ -337,20 +385,24 @@ describe("Sliding Sync", () => { }); }); // select the room - cy.contains(".mx_RoomTile", "Test Room").click(); + cy.findByRole("treeitem", { name: "Test Room" }).click(); cy.get(".mx_ReplyPreview").should("not.exist"); // click reply-to on the Hello World message - cy.contains(".mx_EventTile", "Hello world") - .find('.mx_AccessibleButton[aria-label="Reply"]') - .click({ force: true }); + cy.get(".mx_EventTile_last") + .within(() => { + cy.findByText("Hello world", { timeout: 1000 }); + }) + .realHover() + .findByRole("button", { name: "Reply" }) + .click(); // check it's visible cy.get(".mx_ReplyPreview").should("exist"); // now click Other Room - cy.contains(".mx_RoomTile", "Other Room").click(); + cy.findByRole("treeitem", { name: "Other Room" }).click(); // ensure the reply-to disappears cy.get(".mx_ReplyPreview").should("not.exist"); // click back - cy.contains(".mx_RoomTile", "Test Room").click(); + cy.findByRole("treeitem", { name: "Test Room" }).click(); // ensure the reply-to reappears cy.get(".mx_ReplyPreview").should("exist"); }); @@ -378,12 +430,17 @@ describe("Sliding Sync", () => { }); }); // select the room - cy.contains(".mx_RoomTile", "Test Room").click(); + cy.findByRole("treeitem", { name: "Test Room" }).click(); cy.get(".mx_ReplyPreview").should("not.exist"); // click reply-to on the Reply to me message - cy.contains(".mx_EventTile", "Reply to me") - .find('.mx_AccessibleButton[aria-label="Reply"]') - .click({ force: true }); + cy.get(".mx_EventTile") + .last() + .within(() => { + cy.findByText("Reply to me"); + }) + .realHover() + .findByRole("button", { name: "Reply" }) + .click(); // check it's visible cy.get(".mx_ReplyPreview").should("exist"); // now click on the permalink for Permalink me @@ -401,15 +458,15 @@ describe("Sliding Sync", () => { cy.createRoom({ name: "Apple" }) .as("roomA") .then((roomId) => (roomAId = roomId)) - .then(() => cy.contains(".mx_RoomSublist", "Apple")); + .then(() => cy.findByRole("treeitem", { name: "Apple" })); cy.createRoom({ name: "Pineapple" }) .as("roomP") .then((roomId) => (roomPId = roomId)) - .then(() => cy.contains(".mx_RoomSublist", "Pineapple")); + .then(() => cy.findByRole("treeitem", { name: "Pineapple" })); cy.createRoom({ name: "Orange" }) .as("roomO") - .then(() => cy.contains(".mx_RoomSublist", "Orange")); + .then(() => cy.findByRole("treeitem", { name: "Orange" })); // Intercept all calls to /sync cy.intercept({ method: "POST", url: "**/sync*" }).as("syncRequest"); @@ -426,7 +483,7 @@ describe("Sliding Sync", () => { }; // Select the Test Room - cy.contains(".mx_RoomTile", "Apple").click(); + cy.findByRole("treeitem", { name: "Apple" }).click(); // and wait for cypress to get the result as alias cy.wait("@syncRequest").then((interception) => { @@ -435,11 +492,11 @@ describe("Sliding Sync", () => { }); // Switch to another room - cy.contains(".mx_RoomTile", "Pineapple").click(); + cy.findByRole("treeitem", { name: "Pineapple" }).click(); cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId)); // And switch to even another room - cy.contains(".mx_RoomTile", "Apple").click(); + cy.findByRole("treeitem", { name: "Apple" }).click(); cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId)); // TODO: Add tests for encrypted rooms From 259b5fe253b75e5dec754d27128a68ff5519539d Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 21 Apr 2023 09:45:10 +0000 Subject: [PATCH 026/105] Update `threads.spec.ts` - use Cypress Testing Library (#10680) * Set within() Signed-off-by: Suguru Hirahara * Use findByRole("textbox") Signed-off-by: Suguru Hirahara * Use findByText() Signed-off-by: Suguru Hirahara * Apply findByRole() to reaction and emoji picker Signed-off-by: Suguru Hirahara * Use findByText() and findByRole() for removing a message Signed-off-by: Suguru Hirahara * Apply findByRole() to the close button Signed-off-by: Suguru Hirahara * Replace other commands Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- cypress/e2e/threads/threads.spec.ts | 252 ++++++++++++++++------------ 1 file changed, 141 insertions(+), 111 deletions(-) diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index f93d9990c84..63546bd4174 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -71,20 +71,22 @@ describe("Threads", () => { // Exclude timestamp and read marker from snapshots const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; - // User sends message - cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + cy.get(".mx_RoomView_body").within(() => { + // User sends message + cy.findByRole("textbox", { name: "Send a message…" }).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, - ); + // Check the colour of timestamp on the main timeline + cy.get(".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") - .as("threadId"); + // Wait for message to send, get its ID and save as @threadId + cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") + .invoke("attr", "data-scroll-tokens") + .as("threadId"); + }); // Bot starts thread cy.get("@threadId").then((threadId) => { @@ -96,15 +98,16 @@ describe("Threads", () => { }); // User asserts timeline thread summary visible & clicks it - cy.get(".mx_RoomView_body").within(() => { - cy.get(".mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); - cy.get(".mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there"); - cy.get(".mx_ThreadSummary").click(); - }); + cy.get(".mx_RoomView_body .mx_ThreadSummary") + .within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText(MessageLong).should("exist"); + }) + .click(); // Wait until the both messages are read cy.get(".mx_ThreadView .mx_EventTile_last[data-layout=group]").within(() => { - cy.get(".mx_EventTile_line .mx_MTextBody").should("have.text", MessageLong); + cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); // Make sure the CSS style for spacing is applied to mx_EventTile_line on group/modern layout @@ -122,7 +125,7 @@ describe("Threads", () => { cy.get(".mx_ThreadView .mx_EventTile[data-layout='group'].mx_EventTile_last").within(() => { // Wait until the messages are rendered - cy.get(".mx_EventTile_line .mx_MTextBody").should("have.text", MessageLong); + cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); // Make sure the avatar inside ReadReceiptGroup is visible on the group layout cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); @@ -145,20 +148,22 @@ describe("Threads", () => { // Re-enable the group layout cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // User responds in thread - cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}"); + cy.get(".mx_ThreadView").within(() => { + // User responds in thread + cy.findByRole("textbox", { name: "Send a message…" }).type("Test{enter}"); - // Check the colour of timestamp on EventTile in a thread (mx_ThreadView) - cy.get(".mx_ThreadView .mx_EventTile_last[data-layout='group'] .mx_EventTile_line .mx_MessageTimestamp").should( - "have.css", - "color", - MessageTimestampColor, - ); + // Check the colour of timestamp on EventTile in a thread (mx_ThreadView) + cy.get(".mx_EventTile_last[data-layout='group'] .mx_EventTile_line .mx_MessageTimestamp").should( + "have.css", + "color", + MessageTimestampColor, + ); + }); // User asserts summary was updated correctly cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").should("contain", "Tom"); - cy.get(".mx_ThreadSummary_content").should("contain", "Test"); + cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("Test").should("exist"); }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -169,12 +174,17 @@ describe("Threads", () => { cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); // User reacts to message instead - cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there") - .find('[aria-label="React"]') - .click({ force: true }); // Cypress has no ability to hover + cy.get(".mx_ThreadView").within(() => { + cy.contains(".mx_EventTile .mx_EventTile_line", "Hello there") + .realHover() + .findByRole("toolbar", { name: "Message Actions" }) + .findByRole("button", { name: "React" }) + .click(); + }); + cy.get(".mx_EmojiPicker").within(() => { - cy.get('input[type="text"]').type("wave"); - cy.contains('[role="gridcell"]', "👋").click(); + cy.findByRole("textbox").type("wave"); + cy.findByRole("gridcell", { name: "👋" }).click(); }); cy.get(".mx_ThreadView").within(() => { @@ -231,17 +241,20 @@ describe("Threads", () => { // User redacts their prior response cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test") - .find('[aria-label="Options"]') - .click({ force: true }); // Cypress has no ability to hover + .realHover() + .findByRole("button", { name: "Options" }) + .click(); cy.get(".mx_IconizedContextMenu").within(() => { - cy.contains('[role="menuitem"]', "Remove").click(); + cy.findByRole("menuitem", { name: "Remove" }).click(); }); cy.get(".mx_TextInputDialog").within(() => { - cy.contains(".mx_Dialog_primary", "Remove").click(); + cy.findByRole("button", { name: "Remove" }).should("have.class", "mx_Dialog_primary").click(); }); - // Wait until the response is redacted - cy.get(".mx_ThreadView .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); + cy.get(".mx_ThreadView").within(() => { + // Wait until the response is redacted + cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); + }); // Take Percy snapshots in group layout and bubble layout (IRC layout is not available on ThreadView) cy.get(".mx_ThreadView .mx_EventTile[data-layout='group']").should("be.visible"); @@ -258,12 +271,16 @@ describe("Threads", () => { cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); // User asserts summary was updated correctly - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there"); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText(MessageLong).should("exist"); + }); // User closes right panel after clicking back to thread list - cy.get(".mx_ThreadView .mx_BaseCard_back").click(); - cy.get(".mx_ThreadPanel .mx_BaseCard_close").click(); + cy.get(".mx_ThreadPanel").within(() => { + cy.findByRole("button", { name: "Threads" }).click(); + cy.findByRole("button", { name: "Close" }).click(); + }); // Bot responds to thread cy.get("@threadId").then((threadId) => { @@ -273,21 +290,22 @@ describe("Threads", () => { }); }); - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "How are things?"); - // User asserts thread list unread indicator - cy.get('.mx_HeaderButtons [aria-label="Threads"]').should("have.class", "mx_RightPanel_headerButton_unread"); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("How are things?").should("exist"); + }); - // User opens thread list - cy.get('.mx_HeaderButtons [aria-label="Threads"]').click(); + cy.findByRole("tab", { name: "Threads" }) + .should("have.class", "mx_RightPanel_headerButton_unread") // User asserts thread list unread indicator + .click(); // User opens thread list // User asserts thread with correct root & latest events & unread dot 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?"); + cy.get(".mx_EventTile_body").findByText("Hello Mr. Bot").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("How are things?").should("exist"); // Check the number of the replies - cy.get(".mx_ThreadPanel_replies_amount").should("have.text", "2"); + cy.get(".mx_ThreadPanel_replies_amount").findByText("2").should("exist"); // Check the colour of timestamp on thread list cy.get(".mx_EventTile_details .mx_MessageTimestamp").should("have.css", "color", MessageTimestampColor); @@ -300,23 +318,29 @@ describe("Threads", () => { }); // User responds & asserts - cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Great!{enter}"); - 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", "Great!"); + cy.get(".mx_ThreadView").within(() => { + cy.findByRole("textbox", { name: "Send a message…" }).type("Great!{enter}"); + }); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("Great!").should("exist"); + }); // User edits & asserts - cy.contains(".mx_ThreadView .mx_EventTile_last .mx_EventTile_line", "Great!").within(() => { - cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover - cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}"); + cy.get(".mx_ThreadView .mx_EventTile_last").within(() => { + cy.findByText("Great!").should("exist"); + cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Edit" }).click(); + cy.findByRole("textbox").type(" How about yourself?{enter}"); + }); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("Great! How about yourself?").should("exist"); }); - 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", - "Great! How about yourself?", - ); // User closes right panel - cy.get(".mx_ThreadView .mx_BaseCard_close").click(); + cy.get(".mx_ThreadPanel").within(() => { + cy.findByRole("button", { name: "Close" }).click(); + }); // Bot responds to thread and saves the id of their message to @eventId cy.get("@threadId").then((threadId) => { @@ -331,11 +355,10 @@ describe("Threads", () => { }); // User asserts - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should( - "contain", - "I'm very good thanks", - ); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("I'm very good thanks").should("exist"); + }); // Bot edits their latest event cy.get("@eventId").then((eventId) => { @@ -354,11 +377,10 @@ describe("Threads", () => { }); // User asserts - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); - cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should( - "contain", - "I'm very good thanks :)", - ); + cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { + cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); + cy.get(".mx_ThreadSummary_content").findByText("I'm very good thanks :)").should("exist"); + }); }); it("can send voice messages", () => { @@ -375,18 +397,20 @@ describe("Threads", () => { }); // Send message - cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + cy.get(".mx_RoomView_body").within(() => { + cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - // Create thread - cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .realHover() - .find(".mx_MessageActionBar_threadButton") - .click(); + // Create thread + cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") + .realHover() + .findByRole("button", { name: "Reply in thread" }) + .click(); + }); cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click(); + cy.openMessageComposerOptions(true).findByRole("menuitem", { name: "Voice Message" }).click(); cy.wait(3000); - cy.getComposer(true).find(".mx_MessageComposer_sendMessage").click(); + cy.getComposer(true).findByRole("button", { name: "Send voice message" }).click(); cy.get(".mx_ThreadView .mx_MVoiceMessageBody").should("have.length", 1); }); @@ -394,10 +418,10 @@ describe("Threads", () => { it("should send location and reply to the location on ThreadView", () => { // See: location.spec.ts const selectLocationShareTypeOption = (shareType: string): Chainable => { - return cy.get(`[data-testid="share-location-option-${shareType}"]`); + return cy.findByTestId(`share-location-option-${shareType}`); }; const submitShareLocation = (): void => { - cy.get('[data-testid="location-picker-submit-button"]').click(); + cy.findByRole("button", { name: "Share location" }).click(); }; let bot: MatrixClient; @@ -423,13 +447,15 @@ describe("Threads", () => { const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker, .mapboxgl-map { visibility: hidden !important; }"; - // User sends message - cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + cy.get(".mx_RoomView_body").within(() => { + // User sends message + cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - // 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") - .as("threadId"); + // Wait for message to send, get its ID and save as @threadId + cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") + .invoke("attr", "data-scroll-tokens") + .as("threadId"); + }); // Bot starts thread cy.get("@threadId").then((threadId) => { @@ -444,7 +470,7 @@ describe("Threads", () => { // User sends location on ThreadView cy.get(".mx_ThreadView").should("exist"); - cy.openMessageComposerOptions(true).find("[aria-label='Location']").click(); + cy.openMessageComposerOptions(true).findByRole("menuitem", { name: "Location" }).click(); selectLocationShareTypeOption("Pin").click(); cy.get("#mx_LocationPicker_map").click("center"); submitShareLocation(); @@ -452,13 +478,9 @@ describe("Threads", () => { // User replies to the location cy.get(".mx_ThreadView").within(() => { - cy.get(".mx_EventTile_last") - .realHover() - .within(() => { - cy.get("[aria-label='Reply']").click({ force: false }); - }); + cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click(); - cy.get(".mx_BasicMessageComposer_input").type("Please come here.{enter}"); + cy.findByRole("textbox", { name: "Reply to thread…" }).type("Please come here.{enter}"); // Wait until the reply is sent cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); @@ -475,30 +497,38 @@ describe("Threads", () => { roomId = _roomId; cy.visit("/#/room/" + roomId); }); + // Send message - cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); + cy.get(".mx_RoomView_body").within(() => { + cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - // Create thread - cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .realHover() - .find(".mx_MessageActionBar_threadButton") - .click(); + // Create thread + cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") + .realHover() + .findByRole("button", { name: "Reply in thread" }) + .click(); + }); cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); // Send message to thread - cy.get(".mx_BaseCard .mx_BasicMessageComposer_input").type("Hello Mr. User{enter}"); - cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User"); + cy.get(".mx_ThreadPanel").within(() => { + cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. User{enter}"); + cy.get(".mx_EventTile_last").findByText("Hello Mr. User").should("exist"); - // Close thread - cy.get(".mx_BaseCard_close").click(); + // Close thread + cy.findByRole("button", { name: "Close" }).click(); + }); // Open existing thread cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") .realHover() - .find(".mx_MessageActionBar_threadButton") + .findByRole("button", { name: "Reply in thread" }) .click(); cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. Bot"); - cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User"); + + cy.get(".mx_BaseCard").within(() => { + cy.get(".mx_EventTile").first().findByText("Hello Mr. Bot").should("exist"); + cy.get(".mx_EventTile").last().findByText("Hello Mr. User").should("exist"); + }); }); }); From 792a39a39b20a5ce2872cd020e1e7783120aff21 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2023 10:48:48 +0100 Subject: [PATCH 027/105] ARIA Accessibility improvements (#10675) * Fix confusing tab indexes in EventTilePreview * Stop using headings inside buttons * Prefer labelledby and describedby over duplicated aria-labels * Improve semantics of tables used in settings * Fix types * Update tests * Fix timestamps --- res/css/structures/_SpaceRoomView.pcss | 13 ++--- .../views/settings/_CrossSigningPanel.pcss | 7 ++- .../views/settings/_CryptographyPanel.pcss | 30 ++++++++-- .../views/settings/_SecureBackupPanel.pcss | 7 ++- src/components/structures/SpaceRoomView.tsx | 4 +- .../views/elements/EventTilePreview.tsx | 4 +- .../views/elements/LabelledToggleSwitch.tsx | 19 +++--- .../views/elements/ToggleSwitch.tsx | 4 +- src/components/views/rooms/EventTile.tsx | 19 ++++-- .../views/settings/CrossSigningPanel.tsx | 58 +++++++++---------- .../views/settings/CryptographyPanel.tsx | 30 +++++----- .../views/settings/SecureBackupPanel.tsx | 58 +++++++++---------- .../views/spaces/SpaceCreateMenu.tsx | 4 +- .../views/spaces/SpacePublicShare.tsx | 6 +- .../structures/SpaceHierarchy-test.tsx | 8 +-- .../views/location/LocationShareMenu-test.tsx | 5 ++ .../LocationShareMenu-test.tsx.snap | 8 ++- .../views/settings/Notifications-test.tsx | 12 ++++ .../__snapshots__/Notifications-test.tsx.snap | 11 +++- .../SpaceSettingsVisibilityTab-test.tsx | 17 ++++-- .../SpaceSettingsVisibilityTab-test.tsx.snap | 10 +++- 21 files changed, 197 insertions(+), 137 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index 433febae48d..6a71a75d954 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -23,15 +23,14 @@ $SpaceRoomViewInnerWidth: 428px; box-sizing: border-box; border-radius: 8px; border: 1px solid $input-border-color; - font-size: $font-15px; + font-size: $font-17px; + font-weight: $font-semi-bold; margin: 20px 0; - > h3 { - font-weight: $font-semi-bold; - margin: 0 0 4px; - } - - > span { + > div { + margin-top: 4px; + font-weight: normal; + font-size: $font-15px; color: $secondary-content; } diff --git a/res/css/views/settings/_CrossSigningPanel.pcss b/res/css/views/settings/_CrossSigningPanel.pcss index 12a0e36835f..1b5f7d1f74c 100644 --- a/res/css/views/settings/_CrossSigningPanel.pcss +++ b/res/css/views/settings/_CrossSigningPanel.pcss @@ -17,7 +17,12 @@ limitations under the License. .mx_CrossSigningPanel_statusList { border-spacing: 0; - td { + th { + text-align: start; + } + + td, + th { padding: 0; &:first-of-type { diff --git a/res/css/views/settings/_CryptographyPanel.pcss b/res/css/views/settings/_CryptographyPanel.pcss index 98dab47c592..855949d013d 100644 --- a/res/css/views/settings/_CryptographyPanel.pcss +++ b/res/css/views/settings/_CryptographyPanel.pcss @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .mx_CryptographyPanel_sessionInfo { padding: 0em; border-spacing: 0px; @@ -5,13 +21,15 @@ .mx_CryptographyPanel_sessionInfo > tr { vertical-align: baseline; padding: 0em; -} -.mx_CryptographyPanel_sessionInfo > tr > td { - padding-bottom: 0em; - padding-left: 0em; - padding-right: 1em; - padding-top: 0em; + th { + text-align: start; + } + + td, + th { + padding: 0 1em 0 0; + } } .mx_CryptographyPanel_importExportButtons .mx_AccessibleButton { diff --git a/res/css/views/settings/_SecureBackupPanel.pcss b/res/css/views/settings/_SecureBackupPanel.pcss index 86f7b2036d0..6dcc8321fd7 100644 --- a/res/css/views/settings/_SecureBackupPanel.pcss +++ b/res/css/views/settings/_SecureBackupPanel.pcss @@ -50,7 +50,12 @@ limitations under the License. .mx_SecureBackupPanel_statusList { border-spacing: 0; - td { + th { + text-align: start; + } + + td, + th { padding: 0; &:first-of-type { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index b8b020f039c..85806913110 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -476,7 +476,7 @@ const SpaceSetupPrivateScope: React.FC<{ onFinished(false); }} > -

    {_t("Just me")}

    + {_t("Just me")}
    {_t("A private space to organise your rooms")}
    -

    {_t("Me and my teammates")}

    + {_t("Me and my teammates")}
    {_t("A private space for you and your teammates")}
    diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index eaa41903f70..2648aac6a70 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -128,8 +128,8 @@ export default class EventTilePreview extends React.Component { const event = this.fakeEvent(this.state); return ( -
    - +
    +
    ); } diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx index 4455b16a9f3..2a1540920b2 100644 --- a/src/components/views/elements/LabelledToggleSwitch.tsx +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; +import { randomString } from "matrix-js-sdk/src/randomstring"; import ToggleSwitch from "./ToggleSwitch"; import { Caption } from "../typography/Caption"; @@ -43,18 +44,15 @@ interface IProps { } export default class LabelledToggleSwitch extends React.PureComponent { + private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`; + public render(): React.ReactNode { // This is a minimal version of a SettingsFlag const { label, caption } = this.props; let firstPart = ( - {label} - {caption && ( - <> -
    - {caption} - - )} +
    {label}
    + {caption && {caption}}
    ); let secondPart = ( @@ -62,15 +60,14 @@ export default class LabelledToggleSwitch extends React.PureComponent { checked={this.props.value} disabled={this.props.disabled} onChange={this.props.onChange} - title={this.props.label} tooltip={this.props.tooltip} + aria-labelledby={this.id} + aria-describedby={caption ? `${this.id}_caption` : undefined} /> ); if (this.props.toggleInFront) { - const temp = firstPart; - firstPart = secondPart; - secondPart = temp; + [firstPart, secondPart] = [secondPart, firstPart]; } const classes = classNames("mx_SettingsFlag", this.props.className, { diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index f29405ba8d5..588374d17b6 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -41,7 +41,7 @@ interface IProps { } // Controlled Toggle Switch element, written with Accessibility in mind -export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps): JSX.Element => { +export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => { const _onClick = (): void => { if (disabled) return; onChange(!checked); @@ -61,8 +61,6 @@ export default ({ checked, disabled = false, title, tooltip, onChange, ...props role="switch" aria-checked={checked} aria-disabled={disabled} - title={title} - tooltip={tooltip} >
    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index ab3a4493daf..fb44993553a 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -218,6 +218,10 @@ export interface EventTileProps { // displayed to the current user either because they're // the author or they are a moderator isSeeingThroughMessageHiddenForModeration?: boolean; + + // The following properties are used by EventTilePreview to disable tab indexes within the event tile + hideTimestamp?: boolean; + inhibitInteraction?: boolean; } interface IState { @@ -1006,7 +1010,7 @@ export class UnwrappedEventTile extends React.Component } if (this.props.mxEvent.sender && avatarSize) { - let member; + let member: RoomMember | null = null; // set member to receiver (target) if it is a 3PID invite // so that the correct avatar is shown as the text is // `$target accepted the invitation for $email` @@ -1016,9 +1020,11 @@ export class UnwrappedEventTile extends React.Component member = this.props.mxEvent.sender; } // In the ThreadsList view we use the entire EventTile as a click target to open the thread instead - const viewUserOnClick = ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes( - this.context.timelineRenderingType, - ); + const viewUserOnClick = + !this.props.inhibitInteraction && + ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes( + this.context.timelineRenderingType, + ); avatar = (
    const showTimestamp = this.props.mxEvent.getTs() && + !this.props.hideTimestamp && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || @@ -1101,7 +1108,7 @@ export class UnwrappedEventTile extends React.Component ); } - const linkedTimestamp = ( + const linkedTimestamp = !this.props.hideTimestamp ? ( > {timestamp} - ); + ) : null; const useIRCLayout = this.props.layout === Layout.IRC; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx index e3f62d4ba26..d3926d954fa 100644 --- a/src/components/views/settings/CrossSigningPanel.tsx +++ b/src/components/views/settings/CrossSigningPanel.tsx @@ -243,36 +243,34 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
    {_t("Advanced")} - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
    {_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}
    {_t("Cross-signing private keys:")} - {crossSigningPrivateKeysInStorage - ? _t("in secret storage") - : _t("not found in storage")} -
    {_t("Master private key:")}{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("Self signing private key:")}{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("User signing private key:")}{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("Homeserver feature support:")}{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
    {_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}
    {_t("Cross-signing private keys:")} + {crossSigningPrivateKeysInStorage + ? _t("in secret storage") + : _t("not found in storage")} +
    {_t("Master private key:")}{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("Self signing private key:")}{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("User signing private key:")}{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
    {_t("Homeserver feature support:")}{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
    {errorSection} diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 34b52e405ee..79ddad2544e 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -75,22 +75,20 @@ export default class CryptographyPanel extends React.Component {
    {_t("Cryptography")} - - - - - - - - - - + + + + + + + +
    {_t("Session ID:")} - {deviceId} -
    {_t("Session key:")} - - {identityKey} - -
    {_t("Session ID:")} + {deviceId} +
    {_t("Session key:")} + + {identityKey} + +
    {importExportButtons} {noSendUnverifiedSetting} diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 747378684c0..2b19a8af583 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -15,7 +15,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 { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; @@ -231,9 +231,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { sessionsRemaining, } = this.state; - let statusDescription; - let extraDetailsTableRows; - let extraDetails; + let statusDescription: JSX.Element; + let extraDetailsTableRows: JSX.Element | undefined; + let extraDetails: JSX.Element | undefined; const actions: JSX.Element[] = []; if (error) { statusDescription =
    {_t("Unable to load key backup status")}
    ; @@ -267,7 +267,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { restoreButtonCaption = _t("Connect this session to Key Backup"); } - let uploadStatus; + let uploadStatus: ReactNode; if (!MatrixClientPeg.get().getKeyBackupEnabled()) { // No upload status to show when backup disabled. uploadStatus = ""; @@ -391,11 +391,11 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { extraDetailsTableRows = ( <> - {_t("Backup version:")} + {_t("Backup version:")} {backupInfo.version} - {_t("Algorithm:")} + {_t("Algorithm:")} {backupInfo.algorithm} @@ -460,7 +460,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } } - let actionRow; + let actionRow: JSX.Element | undefined; if (actions.length) { actionRow =
    {actions}
    ; } @@ -478,28 +478,26 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
    {_t("Advanced")} - - - - - - - - - - - - - - - - - - {extraDetailsTableRows} - + + + + + + + + + + + + + + + + + {extraDetailsTableRows}
    {_t("Backup key stored:")}{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}
    {_t("Backup key cached:")} - {backupKeyCached ? _t("cached locally") : _t("not found locally")} - {backupKeyWellFormedText} -
    {_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
    {_t("Secret storage:")}{secretStorageReady ? _t("ready") : _t("not ready")}
    {_t("Backup key stored:")}{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}
    {_t("Backup key cached:")} + {backupKeyCached ? _t("cached locally") : _t("not found locally")} + {backupKeyWellFormedText} +
    {_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
    {_t("Secret storage:")}{secretStorageReady ? _t("ready") : _t("not ready")}
    {extraDetails}
    diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 64fc408b774..ded069778d5 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -89,8 +89,8 @@ const SpaceCreateMenuType: React.FC<{ }> = ({ title, description, className, onClick }) => { return ( -

    {title}

    - {description} + {title} +
    {description}
    ); }; diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index 85446ab2517..68bf940831a 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -52,7 +52,7 @@ const SpacePublicShare: React.FC = ({ space, onFinished }) => { } }} > -

    {_t("Share invite link")}

    + {_t("Share invite link")} {copiedText} {space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers) ? ( @@ -63,8 +63,8 @@ const SpacePublicShare: React.FC = ({ space, onFinished }) => { showRoomInviteDialog(space.roomId); }} > -

    {_t("Invite people")}

    - {_t("Invite with email or username")} + {_t("Invite people")} +
    {_t("Invite with email or username")}
    ) : null}
    diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx index 5f248140c89..b81a84facaa 100644 --- a/test/components/structures/SpaceHierarchy-test.tsx +++ b/test/components/structures/SpaceHierarchy-test.tsx @@ -32,11 +32,9 @@ import DMRoomMap from "../../../src/utils/DMRoomMap"; import SettingsStore from "../../../src/settings/SettingsStore"; // Fake random strings to give a predictable snapshot for checkbox IDs -jest.mock("matrix-js-sdk/src/randomstring", () => { - return { - randomString: () => "abdefghi", - }; -}); +jest.mock("matrix-js-sdk/src/randomstring", () => ({ + randomString: () => "abdefghi", +})); describe("SpaceHierarchy", () => { describe("showRoom", () => { diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index 9ee667f319e..8ab7b46cbd0 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -72,6 +72,11 @@ jest.mock("../../../../src/Modal", () => ({ ModalManagerEvent: { Opened: "opened" }, })); +// Fake random strings to give a predictable snapshot for IDs +jest.mock("matrix-js-sdk/src/randomstring", () => ({ + randomString: () => "abdefghi", +})); + describe("", () => { const userId = "@ernie:server.org"; const mockClient = getMockClientWithEventEmitter({ diff --git a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap index cd492db7a2e..e83d959d5cd 100644 --- a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap +++ b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap @@ -25,12 +25,16 @@ exports[` with live location disabled goes to labs flag scr - Enable live location sharing +
    + Enable live location sharing +
    ({ + randomString: jest.fn(), +})); + const masterRule: IPushRule = { actions: [PushRuleActionName.DontNotify], conditions: [], @@ -271,6 +278,11 @@ describe("", () => { mockClient.getPushRules.mockResolvedValue(pushRules); beforeEach(() => { + let i = 0; + mocked(randomString).mockImplementation(() => { + return "testid_" + i++; + }); + mockClient.getPushRules.mockClear().mockResolvedValue(pushRules); mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] }); mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] }); diff --git a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap index 23fec442b25..6d9c127b5f8 100644 --- a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap @@ -12,18 +12,23 @@ exports[` main notification switches renders only enable notifi - Enable notifications for this account -
    +
    + Enable notifications for this account +
    Turn off to disable notifications on all your devices and sessions
    ({ + randomString: jest.fn(), +})); + jest.useFakeTimers(); describe("", () => { @@ -89,13 +95,16 @@ describe("", () => { const toggleButton = getByTestId("toggle-guest-access-btn")!; fireEvent.click(toggleButton); }; - const getGuestAccessToggle = ({ container }: RenderResult) => - container.querySelector('[aria-label="Enable guest access"]'); - const getHistoryVisibilityToggle = ({ container }: RenderResult) => - container.querySelector('[aria-label="Preview Space"]'); + const getGuestAccessToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Enable guest access"); + const getHistoryVisibilityToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Preview Space"); const getErrorMessage = ({ getByTestId }: RenderResult) => getByTestId("space-settings-error")?.textContent; beforeEach(() => { + let i = 0; + mocked(randomString).mockImplementation(() => { + return "testid_" + i++; + }); + (mockMatrixClient.sendStateEvent as jest.Mock).mockClear().mockResolvedValue({}); MatrixClientPeg.get = jest.fn().mockReturnValue(mockMatrixClient); }); diff --git a/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap b/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap index 8de0ae2c153..a93fda9d6a5 100644 --- a/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap +++ b/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap @@ -4,7 +4,7 @@ exports[` for a public space Access renders guest
    renders container 1`] = ` - Preview Space +
    + Preview Space +
    Date: Fri, 21 Apr 2023 11:50:42 +0100 Subject: [PATCH 028/105] Conform more of the codebase to strictNullChecks (#10672) * Conform more of the codebase to `strictNullChecks` * Iterate * Iterate * Iterate * Iterate * Conform more of the codebase to `strictNullChecks` * Iterate * Update record key --- src/Avatar.ts | 2 +- src/Searching.ts | 23 +++++++++----- src/actions/RoomListActions.ts | 11 ++----- src/components/structures/MessagePanel.tsx | 3 +- src/components/structures/RoomView.tsx | 11 ++++--- src/components/structures/ScrollPanel.tsx | 8 ++--- src/components/structures/UserView.tsx | 3 +- .../views/context_menus/RoomContextMenu.tsx | 6 ++-- .../context_menus/RoomGeneralContextMenu.tsx | 2 +- .../views/dialogs/ReportEventDialog.tsx | 6 ++-- .../security/AccessSecretStorageDialog.tsx | 2 +- .../security/RestoreKeyBackupDialog.tsx | 5 ++- .../views/rooms/MessageComposerButtons.tsx | 21 +++++-------- src/components/views/rooms/RoomPreviewBar.tsx | 10 +++--- .../views/rooms/SearchResultTile.tsx | 2 +- .../views/rooms/SendMessageComposer.tsx | 12 ++++--- .../tabs/room/AdvancedRoomSettingsTab.tsx | 2 +- .../tabs/user/GeneralUserSettingsTab.tsx | 31 ++++++++++++------- src/components/views/voip/AudioFeed.tsx | 4 +-- src/components/views/voip/LegacyCallView.tsx | 20 ++++++------ src/components/views/voip/VideoFeed.tsx | 2 +- src/indexing/EventIndex.ts | 15 +++++---- src/models/Call.ts | 4 +-- src/resizer/sizer.ts | 4 +-- src/stores/BreadcrumbsStore.ts | 2 +- src/stores/OwnBeaconStore.ts | 9 +++--- src/stores/local-echo/RoomEchoChamber.ts | 7 +++-- src/stores/spaces/SpaceStore.ts | 4 +-- src/stores/widgets/StopGapWidgetDriver.ts | 9 ++++-- src/stores/widgets/WidgetLayoutStore.ts | 2 +- src/stores/widgets/WidgetPermissionStore.ts | 2 +- src/utils/SortMembers.ts | 10 +++--- src/utils/notifications.ts | 2 +- .../dialogs/MessageEditHistoryDialog-test.tsx | 2 +- 34 files changed, 143 insertions(+), 115 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index a023ba0ee75..79254ef1b59 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -26,7 +26,7 @@ import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( - member: RoomMember, + member: RoomMember | undefined, width: number, height: number, resizeMethod: ResizeMethod, diff --git a/src/Searching.ts b/src/Searching.ts index 85efeea8c80..25800c8e06e 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -176,7 +176,10 @@ async function localSearch( searchArgs.room_id = roomId; } - const localResult = await eventIndex.search(searchArgs); + const localResult = await eventIndex!.search(searchArgs); + if (!localResult) { + throw new Error("Local search failed"); + } searchArgs.next_batch = localResult.next_batch; @@ -225,7 +228,11 @@ async function localPagination(searchResult: ISeshatSearchResults): Promise[2] | null = null; @@ -63,12 +62,8 @@ export default class RoomListActions { newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); - // If the room was moved "down" (increasing index) in the same list we - // need to use the orders of the tiles with indices shifted by +1 - const offset = newTag === oldTag && oldIndex < newIndex ? 1 : 0; - - const indexBefore = offset + newIndex - 1; - const indexAfter = offset + newIndex; + const indexBefore = newIndex - 1; + const indexAfter = newIndex; const prevOrder = indexBefore <= 0 ? 0 : newList[indexBefore].tags[newTag].order; const nextOrder = indexAfter >= newList.length ? 1 : newList[indexAfter].tags[newTag].order; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index ed6f778bd59..3d48c925fe2 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -883,6 +883,7 @@ export default class MessagePanel extends React.Component { const existingReceipts = receiptsByEvent.get(lastShownEventId) || []; const newReceipts = this.getReadReceiptsForEvent(event); + if (!newReceipts) continue; receiptsByEvent.set(lastShownEventId, existingReceipts.concat(newReceipts)); // Record these receipts along with their last shown event ID for @@ -1218,7 +1219,7 @@ class CreationGrouper extends BaseGrouper { key="roomcreationsummary" events={this.events} onToggle={panel.onHeightChanged} // Update scroll state - summaryMembers={[ev.sender]} + summaryMembers={ev.sender ? [ev.sender] : undefined} summaryText={summaryText} layout={this.panel.props.layout} > diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 4e14733d04b..d1d2a5807ba 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -627,7 +627,7 @@ export class RoomView extends React.Component { mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined, initialEventId: undefined, // default to clearing this, will get set later in the method if needed showRightPanel: this.context.rightPanelStore.isOpenForRoom(roomId), - activeCall: CallStore.instance.getActiveCall(roomId), + activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null, }; if ( @@ -1071,6 +1071,7 @@ export class RoomView extends React.Component { }; private onAction = async (payload: ActionPayload): Promise => { + if (!this.context.client) return; switch (payload.action) { case "message_sent": this.checkDesktopNotifications(); @@ -1228,7 +1229,7 @@ export class RoomView extends React.Component { this.handleEffects(ev); } - if (ev.getSender() !== this.context.client.getSafeUserId()) { + if (this.context.client && ev.getSender() !== this.context.client.getSafeUserId()) { // update unread count when scrolled up if (!this.state.search && this.state.atEndOfLiveTimeline) { // no change @@ -1469,7 +1470,7 @@ export class RoomView extends React.Component { }; private updatePermissions(room: Room): void { - if (room) { + if (room && this.context.client) { const me = this.context.client.getSafeUserId(); const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, me); @@ -1956,6 +1957,8 @@ export class RoomView extends React.Component { } public render(): React.ReactNode { + if (!this.context.client) return null; + if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { return this.renderLocalRoomCreateLoader(this.state.room); @@ -2064,7 +2067,7 @@ export class RoomView extends React.Component { const inviteEvent = myMember ? myMember.events.member : null; let inviterName = _t("Unknown"); if (inviteEvent) { - inviterName = inviteEvent.sender?.name ?? inviteEvent.getSender(); + inviterName = inviteEvent.sender?.name ?? inviteEvent.getSender()!; } // We deliberately don't try to peek into invites, even if we have permission to peek diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 3b380c1d193..5c2edef0ea5 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -786,7 +786,7 @@ export default class ScrollPanel extends React.Component { const scrollState = this.scrollState; const trackedNode = scrollState.trackedNode; - if (!trackedNode?.parentElement) { + if (!trackedNode?.parentElement && this.itemlist.current) { let node: HTMLElement | undefined = undefined; const messages = this.itemlist.current.children; const scrollToken = scrollState.trackedScrollToken; @@ -890,7 +890,7 @@ export default class ScrollPanel extends React.Component { public clearPreventShrinking = (): void => { const messageList = this.itemlist.current; const balanceElement = messageList && messageList.parentElement; - if (balanceElement) balanceElement.style.paddingBottom = null; + if (balanceElement) balanceElement.style.removeProperty("paddingBottom"); this.preventShrinkingState = null; debuglog("prevent shrinking cleared"); }; @@ -904,7 +904,7 @@ export default class ScrollPanel extends React.Component { what it was when marking. */ public updatePreventShrinking = (): void => { - if (this.preventShrinkingState) { + if (this.preventShrinkingState && this.itemlist.current) { const sn = this.getScrollNode(); const scrollState = this.scrollState; const messageList = this.itemlist.current; @@ -922,7 +922,7 @@ export default class ScrollPanel extends React.Component { if (!shouldClear) { const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight); const offsetDiff = offsetFromBottom - currentOffset; - if (offsetDiff > 0) { + if (offsetDiff > 0 && balanceElement) { balanceElement.style.paddingBottom = `${offsetDiff}px`; debuglog("update prevent shrinking ", offsetDiff, "px from bottom"); } else if (offsetDiff < 0) { diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index a5cdb0b584c..4cff508dfba 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -79,7 +79,8 @@ export default class UserView extends React.Component { return; } const fakeEvent = new MatrixEvent({ type: "m.room.member", content: profileInfo }); - const member = new RoomMember(null, this.props.userId); + // We pass an empty string room ID here, this is slight abuse of the class to simplify code + const member = new RoomMember("", this.props.userId); member.setMembershipEvent(fakeEvent); this.setState({ member, loading: false }); } diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 4be907c161d..4a01503496f 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -169,8 +169,8 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { ); const echoChamber = EchoChamber.forRoom(room); - let notificationLabel: string; - let iconClassName: string; + let notificationLabel: string | undefined; + let iconClassName: string | undefined; switch (echoChamber.notificationVolume) { case RoomNotifState.AllMessages: notificationLabel = _t("Default"); @@ -337,7 +337,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId); const removeTag = isApplied ? tagId : inverseTag; const addTag = isApplied ? null : tagId; - dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, undefined, 0)); + dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, 0)); } else { logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`); } diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index 901ed519b69..0401b20b51c 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -100,7 +100,7 @@ export const RoomGeneralContextMenu: React.FC = ({ const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId); const removeTag = isApplied ? tagId : inverseTag; const addTag = isApplied ? null : tagId; - dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, undefined, 0)); + dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, 0)); } else { logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`); } diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 8eaa64bc34b..8ce08208c0d 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -260,7 +260,7 @@ export default class ReportEventDialog extends React.Component { // if the user should also be ignored, do that if (this.state.ignoreUserToo) { - await client.setIgnoredUsers([...client.getIgnoredUsers(), ev.getSender()]); + await client.setIgnoredUsers([...client.getIgnoredUsers(), ev.getSender()!]); } this.props.onFinished(true); @@ -309,8 +309,8 @@ export default class ReportEventDialog extends React.Component { // Display report-to-moderator dialog. // We let the user pick a nature. const client = MatrixClientPeg.get(); - const homeServerName = SdkConfig.get("validated_server_config").hsName; - let subtitle; + const homeServerName = SdkConfig.get("validated_server_config")!.hsName; + let subtitle: string; switch (this.state.nature) { case Nature.Disagreement: subtitle = _t( diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index a261e441041..a4ab5995631 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -130,7 +130,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent(null const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); - const { room, roomId, narrow } = useContext(RoomContext); + const { room, narrow } = useContext(RoomContext); const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); - if (props.haveRecording) { + if (!matrixClient || !room || props.haveRecording) { return null; } @@ -93,7 +93,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { voiceRecordingButton(props, narrow), startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, - showLocationButton(props, room, roomId, matrixClient), + showLocationButton(props, room, matrixClient), ]; } else { mainButtons = [ @@ -113,7 +113,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { voiceRecordingButton(props, narrow), startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, - showLocationButton(props, room, roomId, matrixClient), + showLocationButton(props, room, matrixClient), ]; } @@ -127,7 +127,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { }); return ( - + {mainButtons} {moreButtons.length > 0 && ( { } } -function showLocationButton( - props: IProps, - room: Room, - roomId: string, - matrixClient: MatrixClient, -): ReactElement | null { - const sender = room.getMember(matrixClient.getUserId()!); +function showLocationButton(props: IProps, room: Room, matrixClient: MatrixClient): ReactElement | null { + const sender = room.getMember(matrixClient.getSafeUserId()); return props.showLocationButton && sender ? ( { const result = await MatrixClientPeg.get().lookupThreePid( "email", this.props.invitedEmail, - identityAccessToken, + identityAccessToken!, ); this.setState({ invitedEmailMxid: result.mxid }); } catch (err) { @@ -243,8 +243,8 @@ export default class RoomPreviewBar extends React.Component { if (!inviteEvent) { return null; } - const inviterUserId = inviteEvent.events.member.getSender(); - return room.currentState.getMember(inviterUserId); + const inviterUserId = inviteEvent.events.member?.getSender(); + return inviterUserId ? room.currentState.getMember(inviterUserId) : null; } private isDMInvite(): boolean { @@ -252,8 +252,8 @@ export default class RoomPreviewBar extends React.Component { if (!myMember) { return false; } - const memberContent = myMember.events.member.getContent(); - return memberContent.membership === "invite" && memberContent.is_direct; + const memberContent = myMember.events.member?.getContent(); + return memberContent?.membership === "invite" && memberContent.is_direct; } private makeScreenAfterLogin(): { screen: string; params: Record } { diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index be15ea96948..437b13b899c 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -64,7 +64,7 @@ export default class SearchResultTile extends React.Component { const eventId = resultEvent.getId(); const ts1 = resultEvent.getTs(); - const ret = []; + const ret = []; const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 24fbf5ccad1..700776d54cd 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -372,7 +372,10 @@ export class SendMessageComposer extends React.Component= 0; i--) { @@ -443,8 +447,8 @@ export class SendMessageComposer extends React.Component(posthogEvent); @@ -480,7 +484,7 @@ export class SendMessageComposer extends React.Component { const room = MatrixClientPeg.get().getRoom(this.props.roomId); - Modal.createDialog(RoomUpgradeDialog, { room }); + if (room) Modal.createDialog(RoomUpgradeDialog, { room }); }; private onOldRoomClicked = (e: ButtonEvent): void => { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 233e999afb8..0827065fac3 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -66,13 +66,20 @@ interface IState { haveIdServer: boolean; serverSupportsSeparateAddAndBind?: boolean; idServerHasUnsignedTerms: boolean; - requiredPolicyInfo: { - // This object is passed along to a component for handling - hasTerms: boolean; - policiesAndServices: ServicePolicyPair[] | null; // From the startTermsFlow callback - agreedUrls: string[] | null; // From the startTermsFlow callback - resolve: ((values: string[]) => void) | null; // Promise resolve function for startTermsFlow callback - }; + requiredPolicyInfo: + | { + // This object is passed along to a component for handling + hasTerms: false; + policiesAndServices: null; // From the startTermsFlow callback + agreedUrls: null; // From the startTermsFlow callback + resolve: null; // Promise resolve function for startTermsFlow callback + } + | { + hasTerms: boolean; + policiesAndServices: ServicePolicyPair[]; + agreedUrls: string[]; + resolve: (values: string[]) => void; + }; emails: IThreepid[]; msisdns: IThreepid[]; loading3pids: boolean; // whether or not the emails and msisdns have been loaded @@ -191,19 +198,19 @@ export default class GeneralUserSettingsTab extends React.Component { - if (!this.state.haveIdServer) { + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); + if (!this.state.haveIdServer || !idServerUrl) { this.setState({ idServerHasUnsignedTerms: false }); return; } - // By starting the terms flow we get the logic for checking which terms the user has signed - // for free. So we might as well use that for our own purposes. - const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); const authClient = new IdentityAuthClient(); try { const idAccessToken = await authClient.getAccessToken({ check: false }); await startTermsFlow( - [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken)], + [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)], (policiesAndServices, agreedUrls, extraClassNames) => { return new Promise((resolve, reject) => { this.setState({ diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index d9a36af6ccf..e079360b473 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -62,7 +62,7 @@ export default class AudioFeed extends React.Component { // it fails. // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID // back to the default after the call is over - Dave - element.setSinkId(audioOutput); + element!.setSinkId(audioOutput); } catch (e) { logger.error("Couldn't set requested audio output device: using default", e); logger.warn("Couldn't set requested audio output device: using default", e); @@ -103,7 +103,7 @@ export default class AudioFeed extends React.Component { if (!element) return; element.pause(); - element.src = null; + element.removeAttribute("src"); // As per comment in componentDidMount, setting the sink ID back to the // default once the call is over makes setSinkId work reliably. - Dave diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx index 86be87608d5..5978acb316f 100644 --- a/src/components/views/voip/LegacyCallView.tsx +++ b/src/components/views/voip/LegacyCallView.tsx @@ -339,16 +339,17 @@ export default class LegacyCallView extends React.Component { private onCallResumeClick = (): void => { const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); - LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId); + if (userFacingRoomId) LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId); }; private onTransferClick = (): void => { const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(this.props.call.callId); - this.props.call.transferToCall(transfereeCall); + if (transfereeCall) this.props.call.transferToCall(transfereeCall); }; private onHangupClick = (): void => { - LegacyCallHandler.instance.hangupOrReject(LegacyCallHandler.instance.roomIdForCall(this.props.call)); + const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); + if (roomId) LegacyCallHandler.instance.hangupOrReject(roomId); }; private onToggleSidebar = (): void => { @@ -451,13 +452,12 @@ export default class LegacyCallView extends React.Component { let holdTransferContent: React.ReactNode; if (transfereeCall) { - const transferTargetRoom = MatrixClientPeg.get().getRoom( - LegacyCallHandler.instance.roomIdForCall(call), - ); + const cli = MatrixClientPeg.get(); + const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); + const transferTargetRoom = callRoomId ? cli.getRoom(callRoomId) : null; const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); - const transfereeRoom = MatrixClientPeg.get().getRoom( - LegacyCallHandler.instance.roomIdForCall(transfereeCall), - ); + const transfereeCallRoomId = LegacyCallHandler.instance.roomIdForCall(transfereeCall); + const transfereeRoom = transfereeCallRoomId ? cli.getRoom(transfereeCallRoomId) : null; const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); holdTransferContent = ( @@ -579,6 +579,8 @@ export default class LegacyCallView extends React.Component { const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall); const callRoom = callRoomId ? client.getRoom(callRoomId) : null; + if (!callRoom) return null; + const secCallRoom = secondaryCallRoomId ? client.getRoom(secondaryCallRoomId) : null; const callViewClasses = classNames({ diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 503c53ec66e..c02154936f4 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -150,7 +150,7 @@ export default class VideoFeed extends React.PureComponent { if (!element) return; element.pause(); - element.src = null; + element.removeAttribute("src"); // As per comment in componentDidMount, setting the sink ID back to the // default once the call is over makes setSinkId work reliably. - Dave diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index bca7547b6ce..84c0a3ec204 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -235,8 +235,11 @@ export default class EventIndex extends EventEmitter { const indexManager = PlatformPeg.get()?.getEventIndexingManager(); if (!indexManager) return; + const associatedId = ev.getAssociatedId(); + if (!associatedId) return; + try { - await indexManager.deleteEvent(ev.getAssociatedId()); + await indexManager.deleteEvent(associatedId); } catch (e) { logger.log("EventIndex: Error deleting event from index", e); } @@ -519,10 +522,10 @@ export default class EventIndex extends EventEmitter { const profiles: Record = {}; stateEvents.forEach((ev) => { - if (ev.event.content && ev.event.content.membership === "join") { - profiles[ev.event.sender] = { - displayname: ev.event.content.displayname, - avatar_url: ev.event.content.avatar_url, + if (ev.getContent().membership === "join") { + profiles[ev.getSender()!] = { + displayname: ev.getContent().displayname, + avatar_url: ev.getContent().avatar_url, }; } }); @@ -733,7 +736,7 @@ export default class EventIndex extends EventEmitter { const matrixEvents = events.map((e) => { const matrixEvent = eventMapper(e.event); - const member = new RoomMember(room.roomId, matrixEvent.getSender()); + const member = new RoomMember(room.roomId, matrixEvent.getSender()!); // We can't really reconstruct the whole room state from our // EventIndex to calculate the correct display name. Use the diff --git a/src/models/Call.ts b/src/models/Call.ts index 6f96e9d887c..d3b99db2843 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -213,7 +213,7 @@ export abstract class Call extends TypedEventEmitter { await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) }); } } else if (payload.action === Action.ViewRoom) { - if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) { + if (payload.auto_join && payload.room_id && !this.matrixClient.getRoom(payload.room_id)) { // Queue the room instead of pushing it immediately. We're probably just // waiting for a room join to complete. this.waitingRooms.push({ roomId: payload.room_id, addedTs: Date.now() }); diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index db9e57f46c2..2509dc92a32 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -426,6 +426,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { roomId: Room["roomId"], beaconInfoContent: MBeaconInfoEventContent, ): Promise => { + if (!this.matrixClient) return; // explicitly stop any live beacons this user has // to ensure they remain stopped // if the new replacing beacon is redacted @@ -435,7 +436,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { // eslint-disable-next-line camelcase const { event_id } = await doMaybeLocalRoomAction( roomId, - (actualRoomId: string) => this.matrixClient.unstable_createLiveBeacon(actualRoomId, beaconInfoContent), + (actualRoomId: string) => this.matrixClient!.unstable_createLiveBeacon(actualRoomId, beaconInfoContent), this.matrixClient, ); @@ -552,7 +553,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { const updateContent = makeBeaconInfoContent(timeout, live, description, assetType, timestamp); try { - await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, updateContent); + await this.matrixClient!.unstable_setLiveBeacon(beacon.roomId, updateContent); // cleanup any errors const hadError = this.beaconUpdateErrors.has(beacon.identifier); if (hadError) { @@ -576,7 +577,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.lastPublishedPositionTimestamp = Date.now(); await Promise.all( this.healthyLiveBeaconIds.map((beaconId) => - this.sendLocationToBeacon(this.beacons.get(beaconId), position), + this.beacons.has(beaconId) ? this.sendLocationToBeacon(this.beacons.get(beaconId)!, position) : null, ), ); }; @@ -589,7 +590,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri): Promise => { const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId); try { - await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content); + await this.matrixClient!.sendEvent(beacon.roomId, M_BEACON.name, content); this.incrementBeaconLocationPublishErrorCount(beacon.identifier, false); } catch (error) { logger.error(error); diff --git a/src/stores/local-echo/RoomEchoChamber.ts b/src/stores/local-echo/RoomEchoChamber.ts index 15a3affdda4..2eac117451b 100644 --- a/src/stores/local-echo/RoomEchoChamber.ts +++ b/src/stores/local-echo/RoomEchoChamber.ts @@ -27,7 +27,7 @@ export enum CachedRoomKey { NotificationVolume, } -export class RoomEchoChamber extends GenericEchoChamber { +export class RoomEchoChamber extends GenericEchoChamber { private properties = new Map(); public constructor(context: RoomEchoContext) { @@ -67,11 +67,12 @@ export class RoomEchoChamber extends GenericEchoChamber { public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => { try { - const { rooms } = await this.matrixClient.getRoomHierarchy(space.roomId, limit, 1, true); + const { rooms } = await this.matrixClient!.getRoomHierarchy(space.roomId, limit, 1, true); const viaMap = new EnhancedMap>(); rooms.forEach((room) => { @@ -979,7 +979,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private onRoomState = (ev: MatrixEvent): void => { const room = this.matrixClient?.getRoom(ev.getRoomId()); - if (!room) return; + if (!this.matrixClient || !room) return; switch (ev.getType()) { case EventType.SpaceChild: { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 4c37ffd84c5..9e24126b15d 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -274,19 +274,22 @@ export class StopGapWidgetDriver extends WidgetDriver { await Promise.all( Object.entries(contentMap).flatMap(([userId, userContentMap]) => Object.entries(userContentMap).map(async ([deviceId, content]): Promise => { + const devices = deviceInfoMap.get(userId); + if (!devices) return; + if (deviceId === "*") { // Send the message to all devices we have keys for await client.encryptAndSendToDevices( - Array.from(deviceInfoMap.get(userId).values()).map((deviceInfo) => ({ + Array.from(devices.values()).map((deviceInfo) => ({ userId, deviceInfo, })), content, ); - } else { + } else if (devices.has(deviceId)) { // Send the message to a specific device await client.encryptAndSendToDevices( - [{ userId, deviceInfo: deviceInfoMap.get(userId).get(deviceId) }], + [{ userId, deviceInfo: devices.get(deviceId)! }], content, ); } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index f260895c30a..2bfd555ea30 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -363,7 +363,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } public getContainerWidgets(room: Optional, container: Container): IApp[] { - return this.byRoom.get(room?.roomId)?.get(container)?.ordered || []; + return (room && this.byRoom.get(room.roomId)?.get(container)?.ordered) || []; } public isInContainer(room: Room, widget: IApp, container: Container): boolean { diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts index 244a95f06cb..b6ea52c162c 100644 --- a/src/stores/widgets/WidgetPermissionStore.ts +++ b/src/stores/widgets/WidgetPermissionStore.ts @@ -58,7 +58,7 @@ export class WidgetPermissionStore { return OIDCState.Unknown; } - public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState): void { + public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string | undefined, newState: OIDCState): void { const settingsKey = this.packSettingKey(widget, kind, roomId); let currentValues = SettingsStore.getValue<{ diff --git a/src/utils/SortMembers.ts b/src/utils/SortMembers.ts index d19b461b1e9..8be4e8a9399 100644 --- a/src/utils/SortMembers.ts +++ b/src/utils/SortMembers.ts @@ -67,7 +67,7 @@ interface IActivityScore { // We do this by checking every room to see who has sent a message in the last few hours, and giving them // a score which correlates to the freshness of their message. In theory, this results in suggestions // which are closer to "continue this conversation" rather than "this person exists". -export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivityScore | undefined } { +export function buildActivityScores(cli: MatrixClient): { [userId: string]: IActivityScore } { const now = new Date().getTime(); const earliestAgeConsidered = now - 60 * 60 * 1000; // 1 hour ago const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic @@ -75,6 +75,7 @@ export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivi .flatMap((room) => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered)) .filter((ev) => ev.getTs() > earliestAgeConsidered); const senderEvents = groupBy(events, (ev) => ev.getSender()); + // If the iteratee in mapValues returns undefined that key will be removed from the resultant object return mapValues(senderEvents, (events) => { if (!events.length) return; const lastEvent = maxBy(events, (ev) => ev.getTs())!; @@ -87,7 +88,7 @@ export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivi // an approximate maximum for being selected. score: Math.max(1, inverseTime / (15 * 60 * 1000)), // 15min segments to keep scores sane }; - }); + }) as { [key: string]: IActivityScore }; } interface IMemberScore { @@ -96,13 +97,14 @@ interface IMemberScore { numRooms: number; } -export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberScore | undefined } { +export function buildMemberScores(cli: MatrixClient): { [userId: string]: IMemberScore } { const maxConsideredMembers = 200; const consideredRooms = joinedRooms(cli).filter((room) => room.getJoinedMemberCount() < maxConsideredMembers); const memberPeerEntries = consideredRooms.flatMap((room) => room.getJoinedMembers().map((member) => ({ member, roomSize: room.getJoinedMemberCount() })), ); const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId); + // If the iteratee in mapValues returns undefined that key will be removed from the resultant object return mapValues(userMeta, (roomMemberships) => { if (!roomMemberships.length) return; const maximumPeers = maxConsideredMembers * roomMemberships.length; @@ -112,5 +114,5 @@ export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberSc numRooms: roomMemberships.length, score: Math.max(0, Math.pow(1 - totalPeers / maximumPeers, 5)), }; - }); + }) as { [userId: string]: IMemberScore }; } diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index eda75bb17a4..45ae7566048 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -28,7 +28,7 @@ export const deviceNotificationSettingsKeys = [ "audioNotificationsEnabled", ]; -export function getLocalNotificationAccountDataEventType(deviceId: string): string { +export function getLocalNotificationAccountDataEventType(deviceId: string | null): string { return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`; } diff --git a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx index 28f1aa47658..c220e5a0f4a 100644 --- a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx +++ b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx @@ -43,7 +43,7 @@ describe("", () => { return result; } - function mockEdits(...edits: { msg: string; ts: number | undefined }[]) { + function mockEdits(...edits: { msg: string; ts?: number }[]) { client.relations.mockImplementation(() => Promise.resolve({ events: edits.map( From fdfe800b2c1a098c8264a1a7a9f2bced4d6e28bd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2023 10:49:05 +0100 Subject: [PATCH 029/105] Fix lack of screen reader indication when triggering auto complete (#10664) --- src/components/views/rooms/BasicMessageComposer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d7d25356101..fa45e56d199 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -798,7 +798,7 @@ export default class BasicMessageEditor extends React.Component }; const { completionIndex } = this.state; - const hasAutocomplete = Boolean(this.state.autoComplete); + const hasAutocomplete = !!this.state.autoComplete; let activeDescendant: string | undefined; if (hasAutocomplete && completionIndex! >= 0) { activeDescendant = generateCompletionDomId(completionIndex!); @@ -828,7 +828,7 @@ export default class BasicMessageEditor extends React.Component aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" - aria-expanded={hasAutocomplete ? true : undefined} + aria-expanded={hasAutocomplete ? !this.autocompleteRef.current?.state.hide : undefined} aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined} aria-activedescendant={activeDescendant} dir="auto" From 64997e6f69287aa46b188748cd8f0004a9a47118 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 21 Apr 2023 13:24:40 +0000 Subject: [PATCH 030/105] Remove obsolete `mx_InviteDialog_transferButton` class (#10687) The class has been introduced by 2cc6bcec29be908ac45bf181272e2542aadd73f9 ("Much theming & visual of transfer window dial pad"), and removed by c829cb948031f83d1422867ff54af9ebc2047ba9 ("Use flexbox to layout buttons") from _InviteDialog.scss on the same PR. The class has not used anywhere else since then. Signed-off-by: Suguru Hirahara --- src/components/views/dialogs/InviteDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 03d26459969..f71e455e22c 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1433,7 +1433,6 @@ export default class InviteDialog extends React.PureComponent {_t("Transfer")} From 5fc402cda0a6562aaf21af26c264d44ad21d2fbd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:30:21 +0100 Subject: [PATCH 031/105] Deprecate the `$spacing-` variables (#10686) * Deprecate the $spacing variables * more comments --- res/css/_font-sizes.pcss | 10 ++++++++++ res/css/_spacing.pcss | 12 ++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/res/css/_font-sizes.pcss b/res/css/_font-sizes.pcss index 5c977a740f4..5d83ff83df6 100644 --- a/res/css/_font-sizes.pcss +++ b/res/css/_font-sizes.pcss @@ -14,6 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +/* + * SCSS variables defining a range of font sizes. + * + * These are defined in `rem` so that they scale with the `font-size` of the root element (which is adjustable via the + * "Font size" setting). They exist to make the job of converting designs (which tend to be based in pixels) into CSS + * easier. + * + * That means that, slightly confusingly, `$font-10px` is only *actually* 10px at the default font size: at a base + * `font-size` of 15, it is actually 15px. + */ $font-1px: 0.1rem; $font-1-5px: 0.15rem; $font-2px: 0.2rem; diff --git a/res/css/_spacing.pcss b/res/css/_spacing.pcss index 63197f2321f..eaf46abc0e8 100644 --- a/res/css/_spacing.pcss +++ b/res/css/_spacing.pcss @@ -14,8 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* 1rem :: 10px */ - +/* SCSS variables representing a range of standard lengths. + * + * Avoid using these in new code: we cannot adjust their values without causing massive confusion, so they are + * effectively equivalent to using hardcoded values. + * + * In future, we plan to introduce variables named according to their purpose rather than their size. Additionally, + * we want switch to custom CSS properties (https://github.com/vector-im/element-web/issues/21656), so we might have + * `--spacing-standard` or something. For now, you might as well use hardcoded px values for lengths (except for font + * sizes, for which see the `$font-px` variables). + */ $spacing-2: 2px; $spacing-4: 4px; $spacing-8: 8px; From 893feed13ba32f984dcd3d14189412633be6a5cb Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 21 Apr 2023 13:41:48 +0000 Subject: [PATCH 032/105] Remove a redundant snapshot - `'Manage integrations' on General settings tab` (#10689) This snapshot became redundant thanks to a new one on general-user-settings-tab.spec.ts (User settings tab - General), and should be removed to reduce the usage of quota. Signed-off-by: Suguru Hirahara --- cypress/e2e/settings/set-integration-manager.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cypress/e2e/settings/set-integration-manager.spec.ts b/cypress/e2e/settings/set-integration-manager.spec.ts index 57981573944..879d8382c01 100644 --- a/cypress/e2e/settings/set-integration-manager.spec.ts +++ b/cypress/e2e/settings/set-integration-manager.spec.ts @@ -52,9 +52,5 @@ describe("Set integration manager", () => { cy.get(".mx_SettingsTab_subheading").should("have.css", "margin-inline-end", "0px"); }); }); - - cy.get(".mx_SetIntegrationManager").percySnapshotElement("'Manage integrations' on General settings tab", { - widths: [692], // actual width of mx_SetIntegrationManager - }); }); }); From 83f12fcba07ccbdfd2b494c2b3898b9db6c2dc7d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:48:27 +0100 Subject: [PATCH 033/105] More tests for UserInfo (#10677) * UserInfo-test: move mocking to `beforeEach` ... so that changes to the mocks do not leak between tests * Add some more tests for UserInfo --- .../views/right_panel/UserInfo-test.tsx | 260 ++++++++++++------ test/test-utils/utilities.ts | 2 +- 2 files changed, 170 insertions(+), 92 deletions(-) diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index 68274d9bf8b..8e538577233 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from "react"; -import { fireEvent, render, screen, waitFor, cleanup, act } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor, cleanup, act, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; +import { Mocked, mocked } from "jest-mock"; import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; @@ -72,73 +72,78 @@ jest.mock("../../../../src/utils/DMRoomMap", () => { }; }); -const mockRoom = mocked({ - roomId: "!fkfk", - getType: jest.fn().mockReturnValue(undefined), - isSpaceRoom: jest.fn().mockReturnValue(false), - getMember: jest.fn().mockReturnValue(undefined), - getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), - name: "test room", - on: jest.fn(), - off: jest.fn(), - currentState: { - getStateEvents: jest.fn(), - on: jest.fn(), - }, - getEventReadUpTo: jest.fn(), -} as unknown as Room); - -const mockSpace = mocked({ - roomId: "!fkfk", - getType: jest.fn().mockReturnValue("m.space"), - isSpaceRoom: jest.fn().mockReturnValue(true), - getMember: jest.fn().mockReturnValue(undefined), - getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), - name: "test room", - on: jest.fn(), - off: jest.fn(), - currentState: { - getStateEvents: jest.fn(), - on: jest.fn(), - }, - getEventReadUpTo: jest.fn(), -} as unknown as Room); - -const mockClient = mocked({ - getUser: jest.fn(), - isGuest: jest.fn().mockReturnValue(false), - isUserIgnored: jest.fn(), - getIgnoredUsers: jest.fn(), - setIgnoredUsers: jest.fn(), - isCryptoEnabled: jest.fn(), - getUserId: jest.fn(), - getSafeUserId: jest.fn(), - on: jest.fn(), - off: jest.fn(), - isSynapseAdministrator: jest.fn().mockResolvedValue(false), - isRoomEncrypted: jest.fn().mockReturnValue(false), - doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), - mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), - removeListener: jest.fn(), - currentState: { - on: jest.fn(), - }, - checkDeviceTrust: jest.fn(), - checkUserTrust: jest.fn(), - getRoom: jest.fn(), - credentials: {}, - setPowerLevel: jest.fn(), -} as unknown as MatrixClient); - +const defaultRoomId = "!fkfk"; const defaultUserId = "@user:example.com"; const defaultUser = new User(defaultUserId); +let mockRoom: Mocked; +let mockSpace: Mocked; +let mockClient: Mocked; + beforeEach(() => { - jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); -}); + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockSpace = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue("m.space"), + isSpaceRoom: jest.fn().mockReturnValue(true), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + isCryptoEnabled: jest.fn(), + getUserId: jest.fn(), + getSafeUserId: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + isRoomEncrypted: jest.fn().mockReturnValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + checkDeviceTrust: jest.fn(), + checkUserTrust: jest.fn(), + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + downloadKeys: jest.fn(), + getStoredDevicesForUser: jest.fn(), + } as unknown as MatrixClient); -afterEach(() => { - mockClient.getUser.mockClear().mockReturnValue({} as unknown as User); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); }); describe("", () => { @@ -241,14 +246,76 @@ describe("", () => { expect(screen.getByText(/try with a different client/i)).toBeInTheDocument(); }); }); + + describe("with crypto enabled", () => { + beforeEach(() => { + mockClient.isCryptoEnabled.mockReturnValue(true); + mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(false, false, false)); + mockClient.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(false, false, false, false)); + + const device1 = DeviceInfo.fromStorage( + { + unsigned: { device_display_name: "my device" }, + }, + "d1", + ); + mockClient.getStoredDevicesForUser.mockReturnValue([device1]); + }); + + it("renders a device list which can be expanded", async () => { + renderComponent(); + await act(flushPromises); + + // check the button exists with the expected text + const devicesButton = screen.getByRole("button", { name: "1 session" }); + + // click it + await userEvent.click(devicesButton); + + // there should now be a button with the device id ... + const deviceButton = screen.getByRole("button", { description: "d1" }); + + // ... which should contain the device name + expect(within(deviceButton).getByText("my device")).toBeInTheDocument(); + }); + }); + + describe("with an encrypted room", () => { + beforeEach(() => { + mockClient.isCryptoEnabled.mockReturnValue(true); + mockClient.isRoomEncrypted.mockReturnValue(true); + }); + + it("renders unverified user info", async () => { + mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(false, false, false)); + renderComponent({ room: mockRoom }); + await act(flushPromises); + + const userHeading = screen.getByRole("heading", { name: defaultUserId }); + + // there should be a "normal" E2E padlock + expect(userHeading.getElementsByClassName("mx_E2EIcon_normal")).toHaveLength(1); + }); + + it("renders verified user info", async () => { + mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(true, false, false)); + renderComponent({ room: mockRoom }); + await act(flushPromises); + + const userHeading = screen.getByRole("heading", { name: defaultUserId }); + + // there should be a "verified" E2E padlock + expect(userHeading.getElementsByClassName("mx_E2EIcon_verified")).toHaveLength(1); + }); + }); }); describe("", () => { - const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); const defaultProps = { member: defaultMember, - roomId: mockRoom.roomId, + roomId: defaultRoomId, }; const renderComponent = (props = {}) => { @@ -399,7 +466,7 @@ describe("", () => { }); describe("", () => { - const member = new RoomMember(mockRoom.roomId, defaultUserId); + const member = new RoomMember(defaultRoomId, defaultUserId); const defaultProps = { member, isIgnored: false, canInvite: false, isSpace: false }; const renderComponent = (props = {}) => { @@ -644,17 +711,20 @@ describe("", () => { }); describe("", () => { - const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); - const defaultProps = { - user: defaultMember, - room: mockRoom, - roomPermissions: { - modifyLevelMax: 100, - canEdit: false, - canInvite: false, - }, - }; + let defaultProps: Parameters[0]; + beforeEach(() => { + defaultProps = { + user: defaultMember, + room: mockRoom, + roomPermissions: { + modifyLevelMax: 100, + canEdit: false, + canInvite: false, + }, + }; + }); const renderComponent = (props = {}) => { const Wrapper = (wrapperProps = {}) => { @@ -705,11 +775,14 @@ describe("", () => { }); describe("", () => { - const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); const memberWithInviteMembership = { ...defaultMember, membership: "invite" }; const memberWithJoinMembership = { ...defaultMember, membership: "join" }; - const defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + let defaultProps: Parameters[0]; + beforeEach(() => { + defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + }); const renderComponent = (props = {}) => { const Wrapper = (wrapperProps = {}) => { @@ -805,10 +878,12 @@ describe("", () => { }); describe("", () => { - const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); const memberWithBanMembership = { ...defaultMember, membership: "ban" }; - - const defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + let defaultProps: Parameters[0]; + beforeEach(() => { + defaultProps = { room: mockRoom, member: defaultMember, startUpdating: jest.fn(), stopUpdating: jest.fn() }; + }); const renderComponent = (props = {}) => { const Wrapper = (wrapperProps = {}) => { @@ -927,16 +1002,19 @@ describe("", () => { }); describe("", () => { - const defaultMember = new RoomMember(mockRoom.roomId, defaultUserId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); defaultMember.membership = "invite"; - const defaultProps = { - room: mockRoom, - member: defaultMember, - startUpdating: jest.fn(), - stopUpdating: jest.fn(), - powerLevels: {}, - }; + let defaultProps: Parameters[0]; + beforeEach(() => { + defaultProps = { + room: mockRoom, + member: defaultMember, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + powerLevels: {}, + }; + }); const renderComponent = (props = {}) => { const Wrapper = (wrapperProps = {}) => { @@ -1039,7 +1117,7 @@ describe("disambiguateDevices", () => { describe("isMuted", () => { // this member has a power level of 0 - const isMutedMember = new RoomMember(mockRoom.roomId, defaultUserId); + const isMutedMember = new RoomMember(defaultRoomId, defaultUserId); it("returns false if either argument is falsy", () => { // @ts-ignore to let us purposely pass incorrect args diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 6590ef52306..514a9091f19 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -127,7 +127,7 @@ export function untilEmission( }); } -export const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve)); +export const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve)); // with jest's modern fake timers process.nextTick is also mocked, // flushing promises in the normal way then waits for some advancement From 7c4d0ceddcde369b103b66c13ca053802eea7e7f Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 21 Apr 2023 09:54:27 -0400 Subject: [PATCH 034/105] Fix a crash when a call ends while you're in it (#10681) --- src/models/Call.ts | 1 + test/models/Call-test.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/models/Call.ts b/src/models/Call.ts index d3b99db2843..c78aed3a402 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -773,6 +773,7 @@ export class ElementCall extends Call { } public destroy(): void { + ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.groupCall.room.roomId); WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId); this.off(CallEvent.Participants, this.onParticipants); this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants); diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index da10098e53c..43d26f0dbce 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -1050,6 +1050,12 @@ describe("ElementCall", () => { call.off(CallEvent.Destroy, onDestroy); }); + + it("clears widget persistence when destroyed", async () => { + const destroyPersistentWidgetSpy = jest.spyOn(ActiveWidgetStore.instance, "destroyPersistentWidget"); + call.destroy(); + expect(destroyPersistentWidgetSpy).toHaveBeenCalled(); + }); }); describe("instance in a video room", () => { From 92906c2130b447f4bccd07eb5800c2cb513092a4 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 21 Apr 2023 14:16:00 +0000 Subject: [PATCH 035/105] Replace Sass variables with custom properties on `_font-weights.pcss` (#10554) * Replace `$font-normal` and `$font-semi-bold` Signed-off-by: Suguru Hirahara * Replace `font-weight: 600` with the custom property Signed-off-by: Suguru Hirahara * Replace the existing variables Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- res/css/_common.pcss | 8 ++++---- res/css/_font-weights.pcss | 6 ++++-- res/css/components/views/beacon/_BeaconListItem.pcss | 2 +- .../components/views/beacon/_OwnBeaconStatus.pcss | 2 +- .../components/views/elements/_FilterDropdown.pcss | 2 +- .../components/views/elements/_FilterTabGroup.pcss | 2 +- res/css/components/views/pips/_WidgetPip.pcss | 2 +- .../components/views/spaces/_QuickThemeSwitcher.pcss | 2 +- res/css/structures/_GenericDropdownMenu.pcss | 2 +- res/css/structures/_HomePage.pcss | 6 +++--- res/css/structures/_LargeLoader.pcss | 2 +- res/css/structures/_QuickSettingsButton.pcss | 4 ++-- res/css/structures/_RightPanel.pcss | 2 +- res/css/structures/_RoomSearch.pcss | 4 ++-- res/css/structures/_SpaceHierarchy.pcss | 8 ++++---- res/css/structures/_SpacePanel.pcss | 6 +++--- res/css/structures/_SpaceRoomView.pcss | 6 +++--- res/css/structures/_ToastContainer.pcss | 6 +++--- res/css/structures/_UserMenu.pcss | 4 ++-- res/css/structures/auth/_Login.pcss | 2 +- res/css/views/auth/_AuthBody.pcss | 12 ++++++------ res/css/views/auth/_CompleteSecurityBody.pcss | 4 ++-- res/css/views/auth/_LanguageSelector.pcss | 2 +- res/css/views/auth/_LoginWithQR.pcss | 2 +- res/css/views/beta/_BetaCard.pcss | 4 ++-- .../views/context_menus/_IconizedContextMenu.pcss | 2 +- res/css/views/dialogs/_AddExistingToSpaceDialog.pcss | 6 +++--- res/css/views/dialogs/_CompoundDialog.pcss | 2 +- res/css/views/dialogs/_CreateRoomDialog.pcss | 4 ++-- res/css/views/dialogs/_ExportDialog.pcss | 2 +- res/css/views/dialogs/_FeedbackDialog.pcss | 2 +- res/css/views/dialogs/_ForwardDialog.pcss | 2 +- res/css/views/dialogs/_InviteDialog.pcss | 6 +++--- .../dialogs/_ManageRestrictedJoinRuleDialog.pcss | 2 +- res/css/views/dialogs/_NewSessionReviewDialog.pcss | 2 +- res/css/views/dialogs/_PollCreateDialog.pcss | 2 +- .../views/dialogs/_RoomSettingsDialogBridges.pcss | 2 +- res/css/views/dialogs/_ServerPickerDialog.pcss | 2 +- res/css/views/dialogs/_SpaceSettingsDialog.pcss | 4 ++-- res/css/views/dialogs/_SpotlightDialog.pcss | 2 +- res/css/views/dialogs/_VerifyEMailDialog.pcss | 2 +- .../dialogs/security/_CreateSecretStorageDialog.pcss | 4 ++-- res/css/views/elements/_AccessibleButton.pcss | 2 +- res/css/views/elements/_RoomAliasField.pcss | 2 +- res/css/views/elements/_SSOButtons.pcss | 2 +- res/css/views/elements/_ServerPicker.pcss | 2 +- res/css/views/elements/_Tooltip.pcss | 2 +- res/css/views/elements/_UseCaseSelection.pcss | 2 +- res/css/views/emojipicker/_EmojiPicker.pcss | 2 +- res/css/views/messages/_CallEvent.pcss | 2 +- res/css/views/messages/_DisambiguatedProfile.pcss | 4 ++-- res/css/views/messages/_EventTileBubble.pcss | 2 +- res/css/views/messages/_LegacyCallEvent.pcss | 2 +- res/css/views/messages/_MPollBody.pcss | 2 +- res/css/views/right_panel/_BaseCard.pcss | 6 +++--- res/css/views/right_panel/_RoomSummaryCard.pcss | 4 ++-- res/css/views/right_panel/_ThreadPanel.pcss | 4 ++-- res/css/views/right_panel/_UserInfo.pcss | 4 ++-- res/css/views/room_settings/_AliasSettings.pcss | 2 +- res/css/views/rooms/_AppsDrawer.pcss | 2 +- res/css/views/rooms/_DecryptionFailureBar.pcss | 2 +- res/css/views/rooms/_MemberInfo.pcss | 2 +- res/css/views/rooms/_MemberList.pcss | 4 ++-- res/css/views/rooms/_MessageComposerFormatBar.pcss | 2 +- res/css/views/rooms/_NewRoomIntro.pcss | 2 +- res/css/views/rooms/_PinnedEventTile.pcss | 2 +- res/css/views/rooms/_ReadReceiptGroup.pcss | 2 +- res/css/views/rooms/_RoomBreadcrumbs.pcss | 2 +- res/css/views/rooms/_RoomCallBanner.pcss | 2 +- res/css/views/rooms/_RoomHeader.pcss | 2 +- res/css/views/rooms/_RoomList.pcss | 2 +- res/css/views/rooms/_RoomListHeader.pcss | 2 +- res/css/views/rooms/_RoomPreviewBar.pcss | 4 ++-- res/css/views/rooms/_RoomPreviewCard.pcss | 2 +- res/css/views/rooms/_RoomSublist.pcss | 4 ++-- res/css/views/rooms/_RoomTile.pcss | 2 +- res/css/views/rooms/_SearchBar.pcss | 2 +- res/css/views/rooms/_ThreadSummary.pcss | 2 +- res/css/views/rooms/_WhoIsTypingTile.pcss | 2 +- res/css/views/settings/_DevicesPanel.pcss | 2 +- res/css/views/settings/_JoinRuleSettings.pcss | 4 ++-- res/css/views/settings/_Notifications.pcss | 6 +++--- res/css/views/settings/_SettingsFieldset.pcss | 2 +- res/css/views/settings/_ThemeChoicePanel.pcss | 2 +- res/css/views/settings/tabs/_SettingsTab.pcss | 4 ++-- .../settings/tabs/room/_NotificationSettingsTab.pcss | 2 +- res/css/views/spaces/_SpaceCreateMenu.pcss | 2 +- res/css/views/toasts/_AnalyticsToast.pcss | 4 ++-- res/css/views/toasts/_IncomingCallToast.pcss | 2 +- res/css/views/typography/_Heading.pcss | 8 ++++---- res/css/views/voip/_DialPad.pcss | 2 +- res/css/views/voip/_DialPadContextMenu.pcss | 6 +++--- res/css/views/voip/_DialPadModal.pcss | 4 ++-- res/css/voice-broadcast/atoms/_LiveBadge.pcss | 2 +- .../voice-broadcast/atoms/_VoiceBroadcastHeader.pcss | 2 +- 95 files changed, 149 insertions(+), 147 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 77ffe8116be..dae1f8c601c 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -132,7 +132,7 @@ input[type="password"] { font-family: inherit; padding: 9px; font-size: $font-14px; - font-weight: 600; + font-weight: var(--font-semi-bold); min-width: 0; } @@ -575,7 +575,7 @@ legend { margin-bottom: 5px; /* flip colours for the secondary ones */ - font-weight: 600; + font-weight: var(--font-semi-bold); border: 1px solid $accent; color: $accent; background-color: $button-secondary-bg-color; @@ -794,7 +794,7 @@ legend { @define-mixin LegacyCallButton { box-sizing: border-box; - font-weight: 600; + font-weight: var(--font-semi-bold); height: $font-24px; line-height: $font-24px; margin-right: 0; @@ -816,7 +816,7 @@ legend { @define-mixin ThreadRepliesAmount { color: $secondary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); white-space: nowrap; position: relative; padding: 0 $spacing-12 0 $spacing-8; diff --git a/res/css/_font-weights.pcss b/res/css/_font-weights.pcss index 6999aa31507..7931d6a56a5 100644 --- a/res/css/_font-weights.pcss +++ b/res/css/_font-weights.pcss @@ -14,5 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-normal: 400; -$font-semi-bold: 600; +:root { + --font-normal: 400; + --font-semi-bold: 600; +} diff --git a/res/css/components/views/beacon/_BeaconListItem.pcss b/res/css/components/views/beacon/_BeaconListItem.pcss index 19ac4148cca..c9b39bbebf4 100644 --- a/res/css/components/views/beacon/_BeaconListItem.pcss +++ b/res/css/components/views/beacon/_BeaconListItem.pcss @@ -55,7 +55,7 @@ limitations under the License. margin-bottom: $spacing-8; .mx_BeaconStatus_label { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/components/views/beacon/_OwnBeaconStatus.pcss b/res/css/components/views/beacon/_OwnBeaconStatus.pcss index f007124216a..dedf02da7a6 100644 --- a/res/css/components/views/beacon/_OwnBeaconStatus.pcss +++ b/res/css/components/views/beacon/_OwnBeaconStatus.pcss @@ -27,5 +27,5 @@ limitations under the License. .mx_OwnBeaconStatus_destructiveButton { /* override button link_inline styles */ color: $alert !important; - font-weight: $font-semi-bold !important; + font-weight: var(--font-semi-bold) !important; } diff --git a/res/css/components/views/elements/_FilterDropdown.pcss b/res/css/components/views/elements/_FilterDropdown.pcss index 6a9fe3dc7c0..a73a45c03ee 100644 --- a/res/css/components/views/elements/_FilterDropdown.pcss +++ b/res/css/components/views/elements/_FilterDropdown.pcss @@ -72,7 +72,7 @@ limitations under the License. } .mx_FilterDropdown_optionLabel { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); display: block; } diff --git a/res/css/components/views/elements/_FilterTabGroup.pcss b/res/css/components/views/elements/_FilterTabGroup.pcss index bbf1a279ad4..05329cb7d00 100644 --- a/res/css/components/views/elements/_FilterTabGroup.pcss +++ b/res/css/components/views/elements/_FilterTabGroup.pcss @@ -38,7 +38,7 @@ limitations under the License. &:checked + span { color: $accent; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); // underline box-shadow: 0 1.5px 0 0 currentColor; } diff --git a/res/css/components/views/pips/_WidgetPip.pcss b/res/css/components/views/pips/_WidgetPip.pcss index 80c47719251..cecc0e1365a 100644 --- a/res/css/components/views/pips/_WidgetPip.pcss +++ b/res/css/components/views/pips/_WidgetPip.pcss @@ -42,7 +42,7 @@ limitations under the License. padding: $spacing-12; display: flex; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); background: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0)); } diff --git a/res/css/components/views/spaces/_QuickThemeSwitcher.pcss b/res/css/components/views/spaces/_QuickThemeSwitcher.pcss index c0ca83eb177..a729134c124 100644 --- a/res/css/components/views/spaces/_QuickThemeSwitcher.pcss +++ b/res/css/components/views/spaces/_QuickThemeSwitcher.pcss @@ -30,7 +30,7 @@ limitations under the License. } .mx_QuickThemeSwitcher_heading { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; color: $secondary-content; diff --git a/res/css/structures/_GenericDropdownMenu.pcss b/res/css/structures/_GenericDropdownMenu.pcss index 75805f11468..c3740cc847d 100644 --- a/res/css/structures/_GenericDropdownMenu.pcss +++ b/res/css/structures/_GenericDropdownMenu.pcss @@ -92,7 +92,7 @@ limitations under the License. span:first-child { color: $primary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/structures/_HomePage.pcss b/res/css/structures/_HomePage.pcss index a1757869533..b2495634357 100644 --- a/res/css/structures/_HomePage.pcss +++ b/res/css/structures/_HomePage.pcss @@ -37,7 +37,7 @@ limitations under the License. } h1 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-32px; line-height: $font-44px; margin-bottom: 4px; @@ -45,7 +45,7 @@ limitations under the License. h2 { margin-top: 4px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-25px; color: $muted-fg-color; @@ -73,7 +73,7 @@ limitations under the License. word-break: break-word; box-sizing: border-box; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-20px; color: #fff; /* on all themes */ diff --git a/res/css/structures/_LargeLoader.pcss b/res/css/structures/_LargeLoader.pcss index ba95ea56b65..555eb4bee55 100644 --- a/res/css/structures/_LargeLoader.pcss +++ b/res/css/structures/_LargeLoader.pcss @@ -29,7 +29,7 @@ limitations under the License. .mx_LargeLoader_text { font-size: 24px; - font-weight: 600; + font-weight: var(--font-semi-bold); padding: 0 16px; position: relative; text-align: center; diff --git a/res/css/structures/_QuickSettingsButton.pcss b/res/css/structures/_QuickSettingsButton.pcss index d6686938ad4..128c8e0fbbe 100644 --- a/res/css/structures/_QuickSettingsButton.pcss +++ b/res/css/structures/_QuickSettingsButton.pcss @@ -59,7 +59,7 @@ limitations under the License. contain: unset; /* let the dropdown paint beyond the context menu */ > div > h2 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; color: $primary-content; @@ -72,7 +72,7 @@ limitations under the License. } > div > h4 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; text-transform: uppercase; diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index 27a8b2caa2e..3c6a292c9cd 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -251,7 +251,7 @@ $pulse-color: $alert; .mx_RightPanel_scopeHeader { margin: 24px; text-align: center; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-22px; diff --git a/res/css/structures/_RoomSearch.pcss b/res/css/structures/_RoomSearch.pcss index ddad7b6a40b..8252d2d9b9a 100644 --- a/res/css/structures/_RoomSearch.pcss +++ b/res/css/structures/_RoomSearch.pcss @@ -51,7 +51,7 @@ limitations under the License. /* the following rules are to match that of a real input field */ overflow: hidden; margin: 9px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_RoomSearch_shortcutPrompt { @@ -62,7 +62,7 @@ limitations under the License. font-size: $font-12px; line-height: $font-15px; font-family: inherit; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $light-fg-color; margin-right: 6px; white-space: nowrap; diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 20649e9ea53..2dedf2099c9 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -46,7 +46,7 @@ limitations under the License. .mx_SpaceHierarchy_listHeader_header { grid-column-start: 1; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin: 0; } @@ -71,7 +71,7 @@ limitations under the License. .mx_SpaceHierarchy_error { position: relative; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $alert; font-size: $font-15px; line-height: $font-18px; @@ -94,7 +94,7 @@ limitations under the License. .mx_SpaceHierarchy_roomCount { > h3 { display: inline; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-22px; color: $primary-content; @@ -167,7 +167,7 @@ limitations under the License. gap: 6px 12px; .mx_SpaceHierarchy_roomTile_item { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-18px; display: grid; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index c4a90b2eaea..73f6fde570a 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -276,7 +276,7 @@ $activeBorderColor: $primary-content; border-radius: 8px; background-color: $panel-actions; font-size: $font-15px !important; /* override inline style */ - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-18px; & + .mx_BaseAvatar_image { @@ -380,7 +380,7 @@ $activeBorderColor: $primary-content; .mx_SpacePanel_contextMenu_header { margin: 12px 16px 12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-18px; overflow: hidden; @@ -432,7 +432,7 @@ $activeBorderColor: $primary-content; color: $tertiary-content; font-size: $font-10px; line-height: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); //margin-left: 8px; } } diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index 6a71a75d954..3487253ee7a 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -24,7 +24,7 @@ $SpaceRoomViewInnerWidth: 428px; border-radius: 8px; border: 1px solid $input-border-color; font-size: $font-17px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin: 20px 0; > div { @@ -73,7 +73,7 @@ $SpaceRoomViewInnerWidth: 428px; h1 { margin: 0; font-size: $font-24px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $primary-content; width: max-content; } @@ -120,7 +120,7 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_errorText { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; color: $alert; diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 65a92e552f1..5508fec63ea 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -115,7 +115,7 @@ limitations under the License. color: $primary-content; margin: 0; font-size: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_Toast_title_countIndicator { @@ -142,7 +142,7 @@ limitations under the License. .mx_Toast_description { color: $primary-content; font-size: $font-15px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); max-width: 300px; a { @@ -152,7 +152,7 @@ limitations under the License. .mx_Toast_detail { display: block; - font-weight: $font-normal; + font-weight: var(--font-normal); margin-top: $spacing-4; max-width: 300px; } diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 9c9e1ce529e..4c23bf23c0d 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -47,7 +47,7 @@ limitations under the License. } .mx_UserMenu_name { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; margin-left: 10px; @@ -147,7 +147,7 @@ limitations under the License. display: inline-block; > span { - font-weight: 600; + font-weight: var(--font-semi-bold); display: block; & + span { diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index f57346a7073..2eba8cf3d14 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -18,7 +18,7 @@ limitations under the License. .mx_Login_submit { @mixin mx_DialogButton; font-size: 15px; - font-weight: 600; + font-weight: var(--font-semi-bold); width: 100%; margin-top: 24px; margin-bottom: 24px; diff --git a/res/css/views/auth/_AuthBody.pcss b/res/css/views/auth/_AuthBody.pcss index e1abe24d230..0c55637df63 100644 --- a/res/css/views/auth/_AuthBody.pcss +++ b/res/css/views/auth/_AuthBody.pcss @@ -25,7 +25,7 @@ limitations under the License. box-sizing: border-box; b { - font-weight: 600; + font-weight: var(--font-semi-bold); } &.mx_AuthBody_flex { @@ -35,14 +35,14 @@ limitations under the License. h1 { font-size: $font-24px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin-top: $spacing-8; color: $authpage-primary-color; } h2 { font-size: $font-14px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $authpage-secondary-color; } @@ -155,7 +155,7 @@ limitations under the License. } .mx_Login_submit { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin: 0 0 $spacing-16; } @@ -168,7 +168,7 @@ limitations under the License. } .mx_AuthBody_sign-in-instead-button { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); padding: $spacing-4; } @@ -262,7 +262,7 @@ limitations under the License. text-align: center; > a { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/views/auth/_CompleteSecurityBody.pcss b/res/css/views/auth/_CompleteSecurityBody.pcss index c23f53bd21c..dbc6849c13b 100644 --- a/res/css/views/auth/_CompleteSecurityBody.pcss +++ b/res/css/views/auth/_CompleteSecurityBody.pcss @@ -25,13 +25,13 @@ limitations under the License. h2 { font-size: $font-24px; - font-weight: 600; + font-weight: var(--font-semi-bold); margin-top: 0; } h3 { font-size: $font-14px; - font-weight: 600; + font-weight: var(--font-semi-bold); } a:link, diff --git a/res/css/views/auth/_LanguageSelector.pcss b/res/css/views/auth/_LanguageSelector.pcss index 885ee7f30d5..8a762e0de3c 100644 --- a/res/css/views/auth/_LanguageSelector.pcss +++ b/res/css/views/auth/_LanguageSelector.pcss @@ -21,7 +21,7 @@ limitations under the License. .mx_AuthBody_language .mx_Dropdown_input { border: none; font-size: $font-14px; - font-weight: 600; + font-weight: var(--font-semi-bold); color: $authpage-lang-color; width: auto; } diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index f1259fde0fa..699d7b0f38e 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -111,7 +111,7 @@ limitations under the License. .mx_LoginWithQR_confirmationDigits { text-align: center; margin: $spacing-48 auto; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-24px; color: $primary-content; } diff --git a/res/css/views/beta/_BetaCard.pcss b/res/css/views/beta/_BetaCard.pcss index e4e4db01e56..591fff2d954 100644 --- a/res/css/views/beta/_BetaCard.pcss +++ b/res/css/views/beta/_BetaCard.pcss @@ -32,7 +32,7 @@ limitations under the License. flex: 1; .mx_BetaCard_title { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-22px; color: $primary-content; @@ -126,7 +126,7 @@ limitations under the License. border-radius: 8px; text-transform: uppercase; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: 15px; color: $button-primary-fg-color; display: inline-block; diff --git a/res/css/views/context_menus/_IconizedContextMenu.pcss b/res/css/views/context_menus/_IconizedContextMenu.pcss index 29b2eef7a4f..3a1bb0eba24 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.pcss +++ b/res/css/views/context_menus/_IconizedContextMenu.pcss @@ -27,7 +27,7 @@ limitations under the License. .mx_IconizedContextMenu_optionList_label { font-size: $font-15px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } /* the notFirst class is for cases where the optionList might be under a header of sorts. */ diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index 9a6372a5adb..f71d43ba0b0 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -48,7 +48,7 @@ limitations under the License. margin: 0; color: $secondary-content; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-15px; } @@ -96,7 +96,7 @@ limitations under the License. } .mx_AddExistingToSpace_errorHeading { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-18px; color: $alert; @@ -171,7 +171,7 @@ limitations under the License. > div { > h1 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-22px; margin: 0; diff --git a/res/css/views/dialogs/_CompoundDialog.pcss b/res/css/views/dialogs/_CompoundDialog.pcss index 15df4f39511..b9ddf7837a8 100644 --- a/res/css/views/dialogs/_CompoundDialog.pcss +++ b/res/css/views/dialogs/_CompoundDialog.pcss @@ -37,7 +37,7 @@ limitations under the License. h1 { display: inline-block; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-24px; margin: 0; /* managed by header class */ } diff --git a/res/css/views/dialogs/_CreateRoomDialog.pcss b/res/css/views/dialogs/_CreateRoomDialog.pcss index cca4c4f3cf3..de0dba9a1e0 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.pcss +++ b/res/css/views/dialogs/_CreateRoomDialog.pcss @@ -19,7 +19,7 @@ limitations under the License. .mx_CreateRoomDialog_details_summary { list-style: none; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); cursor: pointer; color: $accent; @@ -96,7 +96,7 @@ limitations under the License. .mx_SettingsFlag_label { flex: 1 1 0; min-width: 0; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_ToggleSwitch { diff --git a/res/css/views/dialogs/_ExportDialog.pcss b/res/css/views/dialogs/_ExportDialog.pcss index ef96ed63818..64599c669c7 100644 --- a/res/css/views/dialogs/_ExportDialog.pcss +++ b/res/css/views/dialogs/_ExportDialog.pcss @@ -19,7 +19,7 @@ limitations under the License. font-size: $font-16px; display: block; font-family: $font-family; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $primary-content; margin-top: 18px; margin-bottom: 12px; diff --git a/res/css/views/dialogs/_FeedbackDialog.pcss b/res/css/views/dialogs/_FeedbackDialog.pcss index ef7bce0cf27..aa778e1776d 100644 --- a/res/css/views/dialogs/_FeedbackDialog.pcss +++ b/res/css/views/dialogs/_FeedbackDialog.pcss @@ -41,7 +41,7 @@ limitations under the License. > h3 { margin-top: 0; margin-bottom: 8px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; line-height: $font-22px; } diff --git a/res/css/views/dialogs/_ForwardDialog.pcss b/res/css/views/dialogs/_ForwardDialog.pcss index f1818721f13..4190c052e5b 100644 --- a/res/css/views/dialogs/_ForwardDialog.pcss +++ b/res/css/views/dialogs/_ForwardDialog.pcss @@ -27,7 +27,7 @@ limitations under the License. margin: 0 0 6px; color: $secondary-content; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-15px; } diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss index 86de19b5ab8..a7c1346951a 100644 --- a/res/css/views/dialogs/_InviteDialog.pcss +++ b/res/css/views/dialogs/_InviteDialog.pcss @@ -114,7 +114,7 @@ limitations under the License. > span { color: $primary-content; - font-weight: 600; + font-weight: var(--font-semi-bold); } > p { @@ -277,7 +277,7 @@ limitations under the License. input { font-size: 18px; - font-weight: 600; + font-weight: var(--font-semi-bold); padding-top: 0; } @@ -429,7 +429,7 @@ limitations under the License. .mx_InviteDialog_tile_nameStack_name { font-size: $font-15px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $primary-content; } diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss index d99072648f6..8a2d079399c 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss @@ -54,7 +54,7 @@ limitations under the License. margin: 0; color: $secondary-content; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-15px; } diff --git a/res/css/views/dialogs/_NewSessionReviewDialog.pcss b/res/css/views/dialogs/_NewSessionReviewDialog.pcss index 0016b5b91ba..0992c980f32 100644 --- a/res/css/views/dialogs/_NewSessionReviewDialog.pcss +++ b/res/css/views/dialogs/_NewSessionReviewDialog.pcss @@ -28,7 +28,7 @@ limitations under the License. } .mx_NewSessionReviewDialog_deviceName { - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_NewSessionReviewDialog_deviceID { diff --git a/res/css/views/dialogs/_PollCreateDialog.pcss b/res/css/views/dialogs/_PollCreateDialog.pcss index e50af35a41a..476ac964b78 100644 --- a/res/css/views/dialogs/_PollCreateDialog.pcss +++ b/res/css/views/dialogs/_PollCreateDialog.pcss @@ -26,7 +26,7 @@ limitations under the License. } h2 { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; margin-top: 0; diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.pcss b/res/css/views/dialogs/_RoomSettingsDialogBridges.pcss index 72fc912ca67..681a76e9e41 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.pcss @@ -94,7 +94,7 @@ limitations under the License. .mx_RoomSettingsDialog_workspace_channel_details { color: $primary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); .mx_RoomSettingsDialog_channel { margin-inline-start: 5px; diff --git a/res/css/views/dialogs/_ServerPickerDialog.pcss b/res/css/views/dialogs/_ServerPickerDialog.pcss index 4d246512539..440ddbf5f62 100644 --- a/res/css/views/dialogs/_ServerPickerDialog.pcss +++ b/res/css/views/dialogs/_ServerPickerDialog.pcss @@ -37,7 +37,7 @@ limitations under the License. > h2 { font-size: $font-15px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $secondary-content; margin: 16px 0 16px 8px; } diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.pcss b/res/css/views/dialogs/_SpaceSettingsDialog.pcss index e0887d1b76f..78c4e42c077 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.pcss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.pcss @@ -18,7 +18,7 @@ limitations under the License. color: $primary-content; .mx_SpaceSettings_errorText { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; color: $alert; @@ -48,7 +48,7 @@ limitations under the License. margin-bottom: 4px; .mx_StyledRadioButton_content { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-18px; color: $primary-content; } diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index c85d94bf451..9303e61a330 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -163,7 +163,7 @@ limitations under the License. .mx_SpotlightDialog_section { > h4, > .mx_SpotlightDialog_sectionHeader > h4 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; color: $secondary-content; diff --git a/res/css/views/dialogs/_VerifyEMailDialog.pcss b/res/css/views/dialogs/_VerifyEMailDialog.pcss index 47541dc452a..a8db4a3d0a6 100644 --- a/res/css/views/dialogs/_VerifyEMailDialog.pcss +++ b/res/css/views/dialogs/_VerifyEMailDialog.pcss @@ -27,7 +27,7 @@ limitations under the License. h1 { font-size: $font-24px; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_VerifyEMailDialog_text-light { diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss index 5dc40898623..e695992008e 100644 --- a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss @@ -38,7 +38,7 @@ limitations under the License. .mx_SettingsFlag_label { flex: 1 1 0; min-width: 0; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_ToggleSwitch { @@ -103,7 +103,7 @@ limitations under the License. .mx_CreateSecretStorageDialog_optionTitle { color: $dialog-title-fg-color; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-18px; padding-bottom: 10px; } diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 4285cb218fd..5d2d765c669 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -90,7 +90,7 @@ limitations under the License. &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_secondary { - font-weight: 600; + font-weight: var(--font-semi-bold); } &.mx_AccessibleButton_kind_primary, diff --git a/res/css/views/elements/_RoomAliasField.pcss b/res/css/views/elements/_RoomAliasField.pcss index 94f6c12a143..b05f8a9e0cd 100644 --- a/res/css/views/elements/_RoomAliasField.pcss +++ b/res/css/views/elements/_RoomAliasField.pcss @@ -39,7 +39,7 @@ limitations under the License. color: $info-plinth-fg-color; border-left: none; border-right: none; - font-weight: 600; + font-weight: var(--font-semi-bold); padding: 9px 10px; flex: 0 0 auto; } diff --git a/res/css/views/elements/_SSOButtons.pcss b/res/css/views/elements/_SSOButtons.pcss index ed09ac31e54..d91e448b491 100644 --- a/res/css/views/elements/_SSOButtons.pcss +++ b/res/css/views/elements/_SSOButtons.pcss @@ -33,7 +33,7 @@ limitations under the License. border-radius: 8px; display: inline-block; font-size: $font-14px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); border: 1px solid $input-border-color; color: $primary-content; diff --git a/res/css/views/elements/_ServerPicker.pcss b/res/css/views/elements/_ServerPicker.pcss index d640a1e7f27..582e5d7bb5b 100644 --- a/res/css/views/elements/_ServerPicker.pcss +++ b/res/css/views/elements/_ServerPicker.pcss @@ -25,7 +25,7 @@ limitations under the License. line-height: $font-20px; > h2 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin: 0 0 20px; grid-column: 1; grid-row: 1; diff --git a/res/css/views/elements/_Tooltip.pcss b/res/css/views/elements/_Tooltip.pcss index 84274e3e5ef..23c5089ab59 100644 --- a/res/css/views/elements/_Tooltip.pcss +++ b/res/css/views/elements/_Tooltip.pcss @@ -102,7 +102,7 @@ limitations under the License. } .mx_Tooltip_title { - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_Tooltip_sub { diff --git a/res/css/views/elements/_UseCaseSelection.pcss b/res/css/views/elements/_UseCaseSelection.pcss index 26b2c5652fc..32aa208ccc4 100644 --- a/res/css/views/elements/_UseCaseSelection.pcss +++ b/res/css/views/elements/_UseCaseSelection.pcss @@ -26,7 +26,7 @@ limitations under the License. justify-content: flex-end; h1 { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-32px; text-align: center; } diff --git a/res/css/views/emojipicker/_EmojiPicker.pcss b/res/css/views/emojipicker/_EmojiPicker.pcss index 8e78061a11b..0a0220f3e37 100644 --- a/res/css/views/emojipicker/_EmojiPicker.pcss +++ b/res/css/views/emojipicker/_EmojiPicker.pcss @@ -213,7 +213,7 @@ limitations under the License. .mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { font-size: $font-16px; - font-weight: 600; + font-weight: var(--font-semi-bold); margin: 0; } diff --git a/res/css/views/messages/_CallEvent.pcss b/res/css/views/messages/_CallEvent.pcss index 599b2b86fe0..7749440963e 100644 --- a/res/css/views/messages/_CallEvent.pcss +++ b/res/css/views/messages/_CallEvent.pcss @@ -67,7 +67,7 @@ limitations under the License. } .mx_CallEvent_active .mx_CallEvent_title { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_CallEvent_columns { diff --git a/res/css/views/messages/_DisambiguatedProfile.pcss b/res/css/views/messages/_DisambiguatedProfile.pcss index ab173f27b1c..1d577a3923c 100644 --- a/res/css/views/messages/_DisambiguatedProfile.pcss +++ b/res/css/views/messages/_DisambiguatedProfile.pcss @@ -22,12 +22,12 @@ limitations under the License. cursor: pointer; .mx_DisambiguatedProfile_displayName { - font-weight: 600; + font-weight: var(--font-semi-bold); margin-inline-end: 0; } .mx_DisambiguatedProfile_mxid { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: 1.1rem; margin-inline-start: 5px; opacity: 0.5; /* Match mx_TextualEvent */ diff --git a/res/css/views/messages/_EventTileBubble.pcss b/res/css/views/messages/_EventTileBubble.pcss index bbf346cab16..b2741ac59f6 100644 --- a/res/css/views/messages/_EventTileBubble.pcss +++ b/res/css/views/messages/_EventTileBubble.pcss @@ -48,7 +48,7 @@ limitations under the License. } .mx_EventTileBubble_title { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-15px; grid-row: 1; } diff --git a/res/css/views/messages/_LegacyCallEvent.pcss b/res/css/views/messages/_LegacyCallEvent.pcss index 8d0f5abbc30..a8f6c83a13f 100644 --- a/res/css/views/messages/_LegacyCallEvent.pcss +++ b/res/css/views/messages/_LegacyCallEvent.pcss @@ -118,7 +118,7 @@ limitations under the License. min-width: 0; .mx_LegacyCallEvent_sender { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: 1.5rem; line-height: 1.8rem; margin-bottom: $spacing-4; diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index e7f3118d571..b86804e9253 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -18,7 +18,7 @@ limitations under the License. margin-top: 8px; h2 { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; margin-top: 0; diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 5f700dfbf38..39f55b052db 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -34,7 +34,7 @@ limitations under the License. > h2 { margin: 0 44px; /* TODO: Use a spacing variable */ font-size: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -199,7 +199,7 @@ limitations under the License. .mx_AccessibleButton_kind_secondary { color: $secondary-content; background-color: rgba(141, 151, 165, 0.2); - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-14px; } @@ -227,7 +227,7 @@ limitations under the License. position: initial; span:first-of-type { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: inherit; color: $primary-content; } diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index a138e332ce1..d25f5c09b17 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -21,7 +21,7 @@ limitations under the License. h1 { margin: $spacing-12 0 $spacing-4; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_RoomSummaryCard_alias { @@ -237,7 +237,7 @@ limitations under the License. margin-top: 12px; margin-bottom: 12px; font-size: $font-13px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 74d6a3175bf..470494df524 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -174,7 +174,7 @@ limitations under the License. h2 { color: $primary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; margin-top: 24px; margin-bottom: 10px; @@ -204,7 +204,7 @@ limitations under the License. line-height: $font-15px; > b { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } } diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 6d3c397f3f5..fd017d8a07c 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -43,7 +43,7 @@ limitations under the License. h2 { font-size: $font-18px; - font-weight: 600; + font-weight: var(--font-semi-bold); margin: 18px 0 0 0; /* TODO: Use a variable */ } @@ -145,7 +145,7 @@ limitations under the License. h3 { text-transform: uppercase; color: $tertiary-content; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-12px; margin: $spacing-4 0; } diff --git a/res/css/views/room_settings/_AliasSettings.pcss b/res/css/views/room_settings/_AliasSettings.pcss index a218e29e1dc..66ac17d8422 100644 --- a/res/css/views/room_settings/_AliasSettings.pcss +++ b/res/css/views/room_settings/_AliasSettings.pcss @@ -31,7 +31,7 @@ limitations under the License. summary { cursor: pointer; color: $accent; - font-weight: 600; + font-weight: var(--font-semi-bold); list-style: none; /* list-style doesn't do it for webkit */ diff --git a/res/css/views/rooms/_AppsDrawer.pcss b/res/css/views/rooms/_AppsDrawer.pcss index 91b84ef445a..bb11e92acd1 100644 --- a/res/css/views/rooms/_AppsDrawer.pcss +++ b/res/css/views/rooms/_AppsDrawer.pcss @@ -329,7 +329,7 @@ $MinWidth: 240px; } .mx_AppPermissionWarning_bolder { - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_AppPermissionWarning h4 { diff --git a/res/css/views/rooms/_DecryptionFailureBar.pcss b/res/css/views/rooms/_DecryptionFailureBar.pcss index 57dc71b7311..f32b0b2bfea 100644 --- a/res/css/views/rooms/_DecryptionFailureBar.pcss +++ b/res/css/views/rooms/_DecryptionFailureBar.pcss @@ -73,7 +73,7 @@ limitations under the License. .mx_DecryptionFailureBar_start_headline { grid-area: headline; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-16px; align-self: center; } diff --git a/res/css/views/rooms/_MemberInfo.pcss b/res/css/views/rooms/_MemberInfo.pcss index 6fc2ff072b6..021bf54f25e 100644 --- a/res/css/views/rooms/_MemberInfo.pcss +++ b/res/css/views/rooms/_MemberInfo.pcss @@ -50,7 +50,7 @@ limitations under the License. .mx_MemberInfo h2 { font-size: $font-18px; - font-weight: 600; + font-weight: var(--font-semi-bold); margin: 16px 0 16px 15px; } diff --git a/res/css/views/rooms/_MemberList.pcss b/res/css/views/rooms/_MemberList.pcss index 07cf8de33a4..ab4d436a4f2 100644 --- a/res/css/views/rooms/_MemberList.pcss +++ b/res/css/views/rooms/_MemberList.pcss @@ -31,7 +31,7 @@ limitations under the License. h2 { text-transform: uppercase; color: $h3-color; - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-13px; padding-left: 3px; padding-right: 12px; @@ -85,7 +85,7 @@ limitations under the License. display: flex; justify-content: center; color: $button-fg-color; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_MemberList_invite.mx_AccessibleButton_disabled { diff --git a/res/css/views/rooms/_MessageComposerFormatBar.pcss b/res/css/views/rooms/_MessageComposerFormatBar.pcss index 214d9a90a42..38aac6fe123 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.pcss +++ b/res/css/views/rooms/_MessageComposerFormatBar.pcss @@ -99,7 +99,7 @@ limitations under the License. .mx_MessageComposerFormatBar_buttonTooltip { white-space: nowrap; font-size: $font-13px; - font-weight: 600; + font-weight: var(--font-semi-bold); min-width: 54px; text-align: center; diff --git a/res/css/views/rooms/_NewRoomIntro.pcss b/res/css/views/rooms/_NewRoomIntro.pcss index d6eaa84fd00..efb7abddc4d 100644 --- a/res/css/views/rooms/_NewRoomIntro.pcss +++ b/res/css/views/rooms/_NewRoomIntro.pcss @@ -55,7 +55,7 @@ limitations under the License. > h2 { margin-top: 24px; font-size: $font-24px; - font-weight: 600; + font-weight: var(--font-semi-bold); } > p { diff --git a/res/css/views/rooms/_PinnedEventTile.pcss b/res/css/views/rooms/_PinnedEventTile.pcss index aa4fe7c02dd..c3960447831 100644 --- a/res/css/views/rooms/_PinnedEventTile.pcss +++ b/res/css/views/rooms/_PinnedEventTile.pcss @@ -48,7 +48,7 @@ limitations under the License. .mx_PinnedEventTile_sender { grid-area: name; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; text-overflow: ellipsis; diff --git a/res/css/views/rooms/_ReadReceiptGroup.pcss b/res/css/views/rooms/_ReadReceiptGroup.pcss index c61ec9f68eb..8b5ae78942b 100644 --- a/res/css/views/rooms/_ReadReceiptGroup.pcss +++ b/res/css/views/rooms/_ReadReceiptGroup.pcss @@ -86,7 +86,7 @@ limitations under the License. font-size: 12px; line-height: 15px; margin: 16px 16px 8px; - font-weight: 600; + font-weight: var(--font-semi-bold); /* shouldn’t be actually focusable */ outline: none; } diff --git a/res/css/views/rooms/_RoomBreadcrumbs.pcss b/res/css/views/rooms/_RoomBreadcrumbs.pcss index 531d9b12713..48daf72bd87 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.pcss +++ b/res/css/views/rooms/_RoomBreadcrumbs.pcss @@ -44,7 +44,7 @@ limitations under the License. } .mx_RoomBreadcrumbs_placeholder { - font-weight: 600; + font-weight: var(--font-semi-bold); font-size: $font-14px; line-height: 32px; /* specifically to match the height this is not scaled */ height: 32px; diff --git a/res/css/views/rooms/_RoomCallBanner.pcss b/res/css/views/rooms/_RoomCallBanner.pcss index ec26807bb18..67244b57e37 100644 --- a/res/css/views/rooms/_RoomCallBanner.pcss +++ b/res/css/views/rooms/_RoomCallBanner.pcss @@ -36,7 +36,7 @@ limitations under the License. .mx_RoomCallBanner_label { color: $primary-content; - font-weight: 600; + font-weight: var(--font-semi-bold); padding-right: $spacing-8; &::before { diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index ced702e5d3e..5c51aaf0433 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -65,7 +65,7 @@ limitations under the License. flex: 0 1 auto; overflow: hidden; color: $primary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; min-height: 24px; align-items: center; diff --git a/res/css/views/rooms/_RoomList.pcss b/res/css/views/rooms/_RoomList.pcss index eb2cc9c4ee7..93a8fc09946 100644 --- a/res/css/views/rooms/_RoomList.pcss +++ b/res/css/views/rooms/_RoomList.pcss @@ -50,7 +50,7 @@ limitations under the License. font-size: $font-14px; div:first-child { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-18px; color: $primary-content; } diff --git a/res/css/views/rooms/_RoomListHeader.pcss b/res/css/views/rooms/_RoomListHeader.pcss index 0954685fde2..7abb9d7599a 100644 --- a/res/css/views/rooms/_RoomListHeader.pcss +++ b/res/css/views/rooms/_RoomListHeader.pcss @@ -22,7 +22,7 @@ limitations under the License. .mx_RoomListHeader_contextMenuButton { font-size: $font-15px; line-height: $font-24px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); padding: 1px 24px 1px 4px; position: relative; overflow: hidden; diff --git a/res/css/views/rooms/_RoomPreviewBar.pcss b/res/css/views/rooms/_RoomPreviewBar.pcss index bccdf426dca..8d35cab7173 100644 --- a/res/css/views/rooms/_RoomPreviewBar.pcss +++ b/res/css/views/rooms/_RoomPreviewBar.pcss @@ -24,7 +24,7 @@ limitations under the License. h3 { font-size: $font-18px; - font-weight: 600; + font-weight: var(--font-semi-bold); &.mx_RoomPreviewBar_spinnerTitle { display: flex; @@ -141,7 +141,7 @@ limitations under the License. } .mx_RoomPreviewBar_inviter { - font-weight: 600; + font-weight: var(--font-semi-bold); } a.mx_RoomPreviewBar_inviter { diff --git a/res/css/views/rooms/_RoomPreviewCard.pcss b/res/css/views/rooms/_RoomPreviewCard.pcss index b7acfb1a321..383e24582e9 100644 --- a/res/css/views/rooms/_RoomPreviewCard.pcss +++ b/res/css/views/rooms/_RoomPreviewCard.pcss @@ -26,7 +26,7 @@ limitations under the License. font-size: $font-14px; .mx_RoomPreviewCard_notice { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-24px; color: $primary-content; margin-top: $spacing-24; diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index 9c41bb2ccf7..1dbfe956ef9 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -147,7 +147,7 @@ limitations under the License. max-width: calc(100% - 16px); /* 16px is the badge width */ line-height: $font-16px; font-size: $font-13px; - font-weight: 600; + font-weight: var(--font-semi-bold); /* Ellipsize any text overflow */ text-overflow: ellipsis; @@ -399,7 +399,7 @@ limitations under the License. .mx_RoomSublist_contextMenu_title { font-size: $font-15px; line-height: $font-20px; - font-weight: 600; + font-weight: var(--font-semi-bold); margin-bottom: 4px; } diff --git a/res/css/views/rooms/_RoomTile.pcss b/res/css/views/rooms/_RoomTile.pcss index 700c55b0462..d0db686da81 100644 --- a/res/css/views/rooms/_RoomTile.pcss +++ b/res/css/views/rooms/_RoomTile.pcss @@ -70,7 +70,7 @@ limitations under the License. line-height: $font-18px; &.mx_RoomTile_titleHasUnreadEvents { - font-weight: 600; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/views/rooms/_SearchBar.pcss b/res/css/views/rooms/_SearchBar.pcss index 95aa57df0e8..9bde17a71c5 100644 --- a/res/css/views/rooms/_SearchBar.pcss +++ b/res/css/views/rooms/_SearchBar.pcss @@ -49,7 +49,7 @@ limitations under the License. cursor: pointer; color: $primary-content; border-bottom: 2px solid $accent; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_SearchBar_unselected { diff --git a/res/css/views/rooms/_ThreadSummary.pcss b/res/css/views/rooms/_ThreadSummary.pcss index b4e7dd5f3c6..f57bde39bcd 100644 --- a/res/css/views/rooms/_ThreadSummary.pcss +++ b/res/css/views/rooms/_ThreadSummary.pcss @@ -107,7 +107,7 @@ limitations under the License. } .mx_ThreadSummary_sender { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_ThreadSummary_content { diff --git a/res/css/views/rooms/_WhoIsTypingTile.pcss b/res/css/views/rooms/_WhoIsTypingTile.pcss index 7157d18f2e5..b260723c7b9 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.pcss +++ b/res/css/views/rooms/_WhoIsTypingTile.pcss @@ -58,7 +58,7 @@ limitations under the License. .mx_WhoIsTypingTile_label { flex: 1; font-size: $font-14px; - font-weight: 600; + font-weight: var(--font-semi-bold); color: $roomtopic-color; } diff --git a/res/css/views/settings/_DevicesPanel.pcss b/res/css/views/settings/_DevicesPanel.pcss index 8a7842d4d0f..69b0d0a664a 100644 --- a/res/css/views/settings/_DevicesPanel.pcss +++ b/res/css/views/settings/_DevicesPanel.pcss @@ -31,7 +31,7 @@ limitations under the License. .mx_DevicesPanel_header_title { font-size: $font-18px; - font-weight: 600; + font-weight: var(--font-semi-bold); color: $primary-content; } diff --git a/res/css/views/settings/_JoinRuleSettings.pcss b/res/css/views/settings/_JoinRuleSettings.pcss index a55432b25a8..18c4395efec 100644 --- a/res/css/views/settings/_JoinRuleSettings.pcss +++ b/res/css/views/settings/_JoinRuleSettings.pcss @@ -27,7 +27,7 @@ limitations under the License. .mx_JoinRuleSettings_spacesWithAccess { > h4 { color: $secondary-content; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-12px; line-height: $font-15px; text-transform: uppercase; @@ -61,7 +61,7 @@ limitations under the License. .mx_StyledRadioButton_content { margin-left: 14px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; color: $primary-content; diff --git a/res/css/views/settings/_Notifications.pcss b/res/css/views/settings/_Notifications.pcss index 45a5b4529d8..635627e0b02 100644 --- a/res/css/views/settings/_Notifications.pcss +++ b/res/css/views/settings/_Notifications.pcss @@ -53,13 +53,13 @@ limitations under the License. } .mx_UserNotifSettings_gridRowHeading { font-size: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_UserNotifSettings_gridColumnLabel { color: $secondary-content; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_UserNotifSettings_gridRowError { // occupy full row @@ -79,7 +79,7 @@ limitations under the License. & > div:first-child { /* section header */ font-size: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } > table { diff --git a/res/css/views/settings/_SettingsFieldset.pcss b/res/css/views/settings/_SettingsFieldset.pcss index fbb192a4bc5..5e2d466568d 100644 --- a/res/css/views/settings/_SettingsFieldset.pcss +++ b/res/css/views/settings/_SettingsFieldset.pcss @@ -22,7 +22,7 @@ limitations under the License. .mx_SettingsFieldset_legend { font-size: $font-16px; display: block; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); color: $primary-content; margin-bottom: 10px; margin-top: 12px; diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 8dfd00e867d..367191430a4 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -40,7 +40,7 @@ limitations under the License. margin-right: 15px; margin-top: 10px; - font-weight: 600; + font-weight: var(--font-semi-bold); > span { justify-content: center; diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss index 544b5c623b2..4f240109c18 100644 --- a/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/res/css/views/settings/tabs/_SettingsTab.pcss @@ -33,7 +33,7 @@ limitations under the License. .mx_SettingsTab_heading { font-size: $font-20px; - font-weight: 600; + font-weight: var(--font-semi-bold); color: $primary-content; margin-top: 10px; /* TODO: Use a spacing variable */ margin-bottom: 10px; /* TODO: Use a spacing variable */ @@ -47,7 +47,7 @@ limitations under the License. .mx_SettingsTab_subheading { font-size: $font-16px; display: block; - font-weight: 600; + font-weight: var(--font-semi-bold); color: $primary-content; margin-top: $spacing-12; margin-bottom: 10px; /* TODO: Use a spacing variable */ diff --git a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss index 7d978cb5f76..780401e8f1b 100644 --- a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss +++ b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss @@ -22,7 +22,7 @@ limitations under the License. color: $primary-content; font-size: $font-15px; line-height: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); margin-top: 16px; position: relative; padding-left: 8px; diff --git a/res/css/views/spaces/_SpaceCreateMenu.pcss b/res/css/views/spaces/_SpaceCreateMenu.pcss index 972c7461f34..3b04be9ff4d 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.pcss +++ b/res/css/views/spaces/_SpaceCreateMenu.pcss @@ -33,7 +33,7 @@ $spacePanelWidth: 68px; > div { > h2 { - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-18px; margin-top: 4px; } diff --git a/res/css/views/toasts/_AnalyticsToast.pcss b/res/css/views/toasts/_AnalyticsToast.pcss index 80e95535a5d..130dff4db97 100644 --- a/res/css/views/toasts/_AnalyticsToast.pcss +++ b/res/css/views/toasts/_AnalyticsToast.pcss @@ -19,13 +19,13 @@ limitations under the License. background-color: $accent; color: #ffffff; border: 1px solid $accent; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } .mx_AccessibleButton_kind_primary { background-color: $accent; color: #ffffff; border: 1px solid $accent; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); } } diff --git a/res/css/views/toasts/_IncomingCallToast.pcss b/res/css/views/toasts/_IncomingCallToast.pcss index 4d879f4f958..dc006208261 100644 --- a/res/css/views/toasts/_IncomingCallToast.pcss +++ b/res/css/views/toasts/_IncomingCallToast.pcss @@ -36,7 +36,7 @@ limitations under the License. .mx_IncomingCallToast_room { display: inline-block; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); font-size: $font-15px; line-height: $font-24px; diff --git a/res/css/views/typography/_Heading.pcss b/res/css/views/typography/_Heading.pcss index 84a008c18f8..a61174ec086 100644 --- a/res/css/views/typography/_Heading.pcss +++ b/res/css/views/typography/_Heading.pcss @@ -16,7 +16,7 @@ limitations under the License. .mx_Heading_h1 { font-size: $font-32px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-39px; margin-inline: unset; margin-block: unset; @@ -24,7 +24,7 @@ limitations under the License. .mx_Heading_h2 { font-size: $font-24px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-29px; margin-inline: unset; margin-block: unset; @@ -32,7 +32,7 @@ limitations under the License. .mx_Heading_h3 { font-size: $font-18px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-22px; margin-inline: unset; margin-block: unset; @@ -40,7 +40,7 @@ limitations under the License. .mx_Heading_h4 { font-size: $font-15px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); line-height: $font-20px; margin-inline: unset; margin-block: unset; diff --git a/res/css/views/voip/_DialPad.pcss b/res/css/views/voip/_DialPad.pcss index 05d1b57eeba..3a54e5b5d4f 100644 --- a/res/css/views/voip/_DialPad.pcss +++ b/res/css/views/voip/_DialPad.pcss @@ -36,7 +36,7 @@ limitations under the License. background-color: $quinary-content; border-radius: 40px; font-size: 18px; - font-weight: 600; + font-weight: var(--font-semi-bold); text-align: center; vertical-align: middle; margin-left: auto; diff --git a/res/css/views/voip/_DialPadContextMenu.pcss b/res/css/views/voip/_DialPadContextMenu.pcss index 046db3133e9..ebed03985f7 100644 --- a/res/css/views/voip/_DialPadContextMenu.pcss +++ b/res/css/views/voip/_DialPadContextMenu.pcss @@ -48,19 +48,19 @@ limitations under the License. .mx_DialPadContextMenu_title { color: $muted-fg-color; font-size: 12px; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_DialPadContextMenu_dialled { height: 1.5em; font-size: 18px; - font-weight: 600; + font-weight: var(--font-semi-bold); border: none; margin: 0px; } .mx_DialPadContextMenu_dialled input { font-size: 18px; - font-weight: 600; + font-weight: var(--font-semi-bold); overflow: hidden; max-width: 185px; text-align: left; diff --git a/res/css/views/voip/_DialPadModal.pcss b/res/css/views/voip/_DialPadModal.pcss index 75ad8a19029..1b57220e0ff 100644 --- a/res/css/views/voip/_DialPadModal.pcss +++ b/res/css/views/voip/_DialPadModal.pcss @@ -41,7 +41,7 @@ limitations under the License. .mx_DialPadModal_title { color: $muted-fg-color; font-size: 12px; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_DialPadModal_cancel { @@ -65,7 +65,7 @@ limitations under the License. .mx_DialPadModal_field input { font-size: 18px; - font-weight: 600; + font-weight: var(--font-semi-bold); } .mx_DialPadModal_dialPad { diff --git a/res/css/voice-broadcast/atoms/_LiveBadge.pcss b/res/css/voice-broadcast/atoms/_LiveBadge.pcss index eb0dbd8e4b3..0a1c15de2cc 100644 --- a/res/css/voice-broadcast/atoms/_LiveBadge.pcss +++ b/res/css/voice-broadcast/atoms/_LiveBadge.pcss @@ -21,7 +21,7 @@ limitations under the License. color: $live-badge-color; display: inline-flex; font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); gap: $spacing-4; padding: 2px 4px; } diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss index a30beb27b6a..4b8bcff47c7 100644 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss @@ -34,7 +34,7 @@ limitations under the License. .mx_VoiceBroadcastHeader_room { font-size: $font-12px; - font-weight: $font-semi-bold; + font-weight: var(--font-semi-bold); min-width: 0; overflow: hidden; text-overflow: ellipsis; From ba796504f50b9aca25c7ad9a3e279da1737308c7 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 23 Apr 2023 07:37:20 +0000 Subject: [PATCH 036/105] Tidy up IRCLayout.pcss (#10189) * Specify the data-layout=irc against ':not([data-layout=bubble])' Signed-off-by: Suguru Hirahara * Include mx_EventTile_info in mx_EventTile[data-layout=irc] Signed-off-by: Suguru Hirahara * Move declarations up, removing a default declaration Signed-off-by: Suguru Hirahara * Tidy up mx_EventTile:hover Signed-off-by: Suguru Hirahara * Use a spacing variable Signed-off-by: Suguru Hirahara * Sort by order properties Signed-off-by: Suguru Hirahara * Move and add comments Signed-off-by: Suguru Hirahara * Merge style declarations - flex-shrink and height Signed-off-by: Suguru Hirahara * Consider cascading order Signed-off-by: Suguru Hirahara * Move order declaration top Signed-off-by: Suguru Hirahara * Fix an invalid comment Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- res/css/views/rooms/_IRCLayout.pcss | 90 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.pcss b/res/css/views/rooms/_IRCLayout.pcss index 8163b29fc98..cd4371b6a02 100644 --- a/res/css/views/rooms/_IRCLayout.pcss +++ b/res/css/views/rooms/_IRCLayout.pcss @@ -17,57 +17,54 @@ limitations under the License. $irc-line-height: $font-18px; .mx_IRCLayout { - --name-width: 80px; // cf. ircDisplayNameWidth on Settings.tsx + --name-width: 80px; /* cf. ircDisplayNameWidth on Settings.tsx */ --icon-width: 14px; - --right-padding: 5px; --line-height: $irc-line-height; + --right-padding: 5px; /* TODO: Use a spacing variable */ line-height: var(--line-height) !important; + blockquote { + margin: 0; + } + .mx_NewRoomIntro { > h2 { line-height: initial; /* Cancel $irc-line-height */ } } - .mx_EventTile { + .mx_EventTile[data-layout="irc"] { --EventTile_irc_line-padding-block: 1px; - /* timestamps are links which shouldn't be underlined */ - > a { - text-decoration: none; - min-width: $MessageTimestamp_width; - } - display: flex; - flex-direction: row; align-items: flex-start; padding-top: 0; + > a { + text-decoration: none; /* timestamps are links which shouldn't be underlined */ + min-width: $MessageTimestamp_width; /* ensure space for EventTile without timestamp */ + } + > * { margin-right: var(--right-padding); } - .mx_EventTile_msgOption { - order: 5; - flex-shrink: 0; + .mx_EventTile_avatar, + .mx_EventTile_e2eIcon { + height: $irc-line-height; } - .mx_EventTile_line, - .mx_EventTile_reply { - display: flex; - flex-direction: column; - order: 3; - flex-grow: 1; - flex-shrink: 1; - min-width: 0; + .mx_EventTile_avatar, + .mx_DisambiguatedProfile, + .mx_EventTile_e2eIcon, + .mx_EventTile_msgOption { + flex-shrink: 0; } .mx_EventTile_avatar { order: 1; position: relative; - flex-shrink: 0; - height: $irc-line-height; display: flex; align-items: center; @@ -82,10 +79,9 @@ $irc-line-height: $font-18px; } .mx_DisambiguatedProfile { + order: 2; width: var(--name-width); margin-inline-end: 0; /* override mx_EventTile > * */ - order: 2; - flex-shrink: 0; > .mx_DisambiguatedProfile_displayName { width: 100%; @@ -96,9 +92,8 @@ $irc-line-height: $font-18px; > .mx_DisambiguatedProfile_mxid { visibility: collapse; - /* Override the inherited margin. */ - margin-left: 0; - padding: 0 5px; + margin-left: 0; /* Override the inherited margin. */ + padding: 0 5px; /* TODO: Use a spacing variable */ } &:hover { @@ -110,7 +105,7 @@ $irc-line-height: $font-18px; display: inline; background-color: $event-selected-color; border-radius: 8px 0 0 8px; - padding-right: 8px; + padding-right: $spacing-8; } > .mx_DisambiguatedProfile_mxid { @@ -123,12 +118,7 @@ $irc-line-height: $font-18px; .mx_EventTile_e2eIcon { padding: 0; - - flex-shrink: 0; flex-grow: 0; - - height: $font-18px; - background-position: center; } @@ -155,13 +145,34 @@ $irc-line-height: $font-18px; } } + .mx_EventTile_line, + .mx_EventTile_reply { + order: 3; + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + } + .mx_EventTile_reply { order: 4; } + .mx_EventTile_msgOption { + order: 5; + } + .mx_EditMessageComposer_buttons { position: relative; } + + &.mx_EventTile_info { + .mx_ViewSourceEvent, /* For hidden events */ + .mx_TextualEvent { + line-height: $irc-line-height; + } + } } .mx_EventTile_emote { @@ -171,17 +182,6 @@ $irc-line-height: $font-18px; } } - blockquote { - margin: 0; - } - - .mx_EventTile.mx_EventTile_info { - .mx_ViewSourceEvent, /* For hidden events */ - .mx_TextualEvent { - line-height: $irc-line-height; - } - } - .mx_ReplyChain { .mx_DisambiguatedProfile { width: unset; From 96c8267f71ce32657ff54159dd9c28ec8b952e4e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2023 08:29:01 +0100 Subject: [PATCH 037/105] Improve types to match reality (#10691) --- src/BasePlatform.ts | 2 +- src/utils/AutoDiscoveryUtils.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index edafff229fa..da083f27167 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -75,7 +75,7 @@ export default abstract class BasePlatform { this.startUpdateCheck = this.startUpdateCheck.bind(this); } - public abstract getConfig(): Promise; + public abstract getConfig(): Promise; public abstract getDefaultDeviceDisplayName(): string; diff --git a/src/utils/AutoDiscoveryUtils.tsx b/src/utils/AutoDiscoveryUtils.tsx index 70f40af883e..aaa602abb40 100644 --- a/src/utils/AutoDiscoveryUtils.tsx +++ b/src/utils/AutoDiscoveryUtils.tsx @@ -189,12 +189,12 @@ export default class AutoDiscoveryUtils { * @returns {Promise} Resolves to the validated configuration. */ public static buildValidatedConfigFromDiscovery( - serverName: string, - discoveryResult: ClientConfig, + serverName?: string, + discoveryResult?: ClientConfig, syntaxOnly = false, isSynthetic = false, ): ValidatedServerConfig { - if (!discoveryResult || !discoveryResult["m.homeserver"]) { + if (!discoveryResult?.["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of code but otherwise tell the user "it broke". logger.error("Ended up in a state of not knowing which homeserver to connect to."); @@ -249,7 +249,7 @@ export default class AutoDiscoveryUtils { throw new UserFriendlyError("Unexpected error resolving homeserver configuration"); } - let preferredHomeserverName = serverName ? serverName : hsResult["server_name"]; + let preferredHomeserverName = serverName ?? hsResult["server_name"]; const url = new URL(preferredHomeserverUrl); if (!preferredHomeserverName) preferredHomeserverName = url.hostname; From e24e85f7a5310030726ab9f92021742ac85855da Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 24 Apr 2023 07:56:25 +0000 Subject: [PATCH 038/105] Merge `set-integration-manager.spec.ts` with `general-user-settings-tab.spec.ts` (#10698) * Merge `set-integration-manager.spec.ts` with `general-user-settings-tab.spec.ts` Signed-off-by: Suguru Hirahara * Remove `set-integration-manager.spec.ts` Signed-off-by: Suguru Hirahara * Remove a duplicate command Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- .../general-user-settings-tab.spec.ts | 11 ++++ .../settings/set-integration-manager.spec.ts | 56 ------------------- 2 files changed, 11 insertions(+), 56 deletions(-) delete mode 100644 cypress/e2e/settings/set-integration-manager.spec.ts diff --git a/cypress/e2e/settings/general-user-settings-tab.spec.ts b/cypress/e2e/settings/general-user-settings-tab.spec.ts index 837ae5aaaa5..b818ec229d3 100644 --- a/cypress/e2e/settings/general-user-settings-tab.spec.ts +++ b/cypress/e2e/settings/general-user-settings-tab.spec.ts @@ -151,6 +151,17 @@ describe("General user settings tab", () => { // Make sure integration manager's toggle switch is enabled cy.get(".mx_ToggleSwitch_enabled").should("be.visible"); + + // Assert space between "Manage integrations" and the integration server address is set to 4px; + cy.get(".mx_SetIntegrationManager_heading_manager").should("have.css", "column-gap", "4px"); + + cy.get(".mx_SetIntegrationManager_heading_manager").within(() => { + cy.get(".mx_SettingsTab_heading").should("have.text", "Manage integrations"); + + // Assert the headings' inline end margin values are set to zero in favor of the column-gap declaration + cy.get(".mx_SettingsTab_heading").should("have.css", "margin-inline-end", "0px"); + cy.get(".mx_SettingsTab_subheading").should("have.css", "margin-inline-end", "0px"); + }); }); // Assert the account deactivation button is displayed diff --git a/cypress/e2e/settings/set-integration-manager.spec.ts b/cypress/e2e/settings/set-integration-manager.spec.ts deleted file mode 100644 index 879d8382c01..00000000000 --- a/cypress/e2e/settings/set-integration-manager.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2023 Suguru Hirahara - -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 { HomeserverInstance } from "../../plugins/utils/homeserver"; - -const USER_NAME = "Alice"; - -describe("Set integration manager", () => { - let homeserver: HomeserverInstance; - - beforeEach(() => { - cy.startHomeserver("default").then((data) => { - homeserver = data; - cy.initTestUser(homeserver, USER_NAME); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - it("should be correctly rendered", () => { - cy.openUserSettings("General"); - - cy.get(".mx_SetIntegrationManager").within(() => { - // Assert the toggle switch is enabled by default - cy.get(".mx_ToggleSwitch_enabled").should("exist"); - - // Assert space between "Manage integrations" and the integration server address is set to 4px; - cy.get(".mx_SetIntegrationManager_heading_manager").should("have.css", "column-gap", "4px"); - - cy.get(".mx_SetIntegrationManager_heading_manager").within(() => { - cy.get(".mx_SettingsTab_heading").should("have.text", "Manage integrations"); - - // Assert the headings' inline end margin values are set to zero in favor of the column-gap declaration - cy.get(".mx_SettingsTab_heading").should("have.css", "margin-inline-end", "0px"); - cy.get(".mx_SettingsTab_subheading").should("have.css", "margin-inline-end", "0px"); - }); - }); - }); -}); From 16ab5e9db0f35b6f50559a37abfc55b63cf3f480 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 Apr 2023 03:41:09 -0500 Subject: [PATCH 039/105] Properly translate errors in `ChangePassword.tsx` so they show up translated to the user but not in our logs (#10615) * Properly translate errors in `ChangePassword.tsx` So they show up translated to the user but not in our logs. Part of https://github.com/vector-im/element-web/issues/9597 and also fixes it since it's the last piece mentioned (there could be other cases we log translated strings) Fix https://github.com/vector-im/element-web/issues/9597 * Make more useful * Update i18n strings * No need to checkPassword since field validation already covers this See https://github.com/matrix-org/matrix-react-sdk/pull/10615#discussion_r1167363765 Both of the error cases are covered by the logic in `verifyFieldsBeforeSubmit()` just above and there is no way `checkPassword` would ever throw one of these errors since they are already valid by the time it reaches here. * Update i18n strings * Revert "No need to checkPassword since field validation already covers this" This reverts commit 7786dd151028e6fbf04d1a38a9c2cd47a3fbfc4b. * Update i18n strings * Add todo context to note that we can remove this logic in the future * Ensure is an error * Remove else See https://github.com/matrix-org/matrix-react-sdk/pull/10615#discussion_r1173477053 --- .../views/settings/ChangePassword.tsx | 54 ++++++++++++++----- .../tabs/user/GeneralUserSettingsTab.tsx | 43 ++++++++++----- src/i18n/strings/en_EN.json | 4 ++ 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/components/views/settings/ChangePassword.tsx b/src/components/views/settings/ChangePassword.tsx index 8ea83263250..0a326468781 100644 --- a/src/components/views/settings/ChangePassword.tsx +++ b/src/components/views/settings/ChangePassword.tsx @@ -24,7 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import Spinner from "../elements/Spinner"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td } from "../../../languageHandler"; +import { UserFriendlyError, _t, _td } from "../../../languageHandler"; import Modal from "../../../Modal"; import PassphraseField from "../auth/PassphraseField"; import { PASSWORD_MIN_SCORE } from "../auth/RegistrationForm"; @@ -48,7 +48,7 @@ interface IProps { /** Was one or more other devices logged out whilst changing the password */ didLogoutOutOtherDevices: boolean; }) => void; - onError: (error: { error: string }) => void; + onError: (error: Error) => void; rowClassName?: string; buttonClassName?: string; buttonKind?: string; @@ -183,7 +183,16 @@ export default class ChangePassword extends React.Component { } }, (err) => { - this.props.onError(err); + if (err instanceof Error) { + this.props.onError(err); + } else { + this.props.onError( + new UserFriendlyError("Error while changing password: %(error)s", { + error: String(err), + cause: undefined, + }), + ); + } }, ) .finally(() => { @@ -196,15 +205,19 @@ export default class ChangePassword extends React.Component { }); } - private checkPassword(oldPass: string, newPass: string, confirmPass: string): { error: string } | undefined { + /** + * Checks the `newPass` and throws an error if it is unacceptable. + * @param oldPass The old password + * @param newPass The new password that the user is trying to be set + * @param confirmPass The confirmation password where the user types the `newPass` + * again for confirmation and should match the `newPass` before we accept their new + * password. + */ + private checkPassword(oldPass: string, newPass: string, confirmPass: string): void { if (newPass !== confirmPass) { - return { - error: _t("New passwords don't match"), - }; + throw new UserFriendlyError("New passwords don't match"); } else if (!newPass || newPass.length === 0) { - return { - error: _t("Passwords can't be empty"), - }; + throw new UserFriendlyError("Passwords can't be empty"); } } @@ -307,11 +320,24 @@ export default class ChangePassword extends React.Component { const oldPassword = this.state.oldPassword; const newPassword = this.state.newPassword; const confirmPassword = this.state.newPasswordConfirm; - const err = this.checkPassword(oldPassword, newPassword, confirmPassword); - if (err) { - this.props.onError(err); - } else { + try { + // TODO: We can remove this check (but should add some Cypress tests to + // sanity check this flow). This logic is redundant with the input field + // validation we do and `verifyFieldsBeforeSubmit()` above. See + // https://github.com/matrix-org/matrix-react-sdk/pull/10615#discussion_r1167364214 + this.checkPassword(oldPassword, newPassword, confirmPassword); return this.onChangePassword(oldPassword, newPassword); + } catch (err) { + if (err instanceof Error) { + this.props.onError(err); + } else { + this.props.onError( + new UserFriendlyError("Error while changing password: %(error)s", { + error: String(err), + cause: undefined, + }), + ); + } } }; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 0827065fac3..7b10119705f 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -21,9 +21,9 @@ import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; import { logger } from "matrix-js-sdk/src/logger"; import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; +import { HTTPError } from "matrix-js-sdk/src/matrix"; -import { _t } from "../../../../../languageHandler"; +import { UserFriendlyError, _t } from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; import * as languageHandler from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; @@ -43,7 +43,7 @@ import Spinner from "../../../elements/Spinner"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { UIFeature } from "../../../../../settings/UIFeature"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import ErrorDialog from "../../../dialogs/ErrorDialog"; +import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog"; import AccountPhoneNumbers from "../../account/PhoneNumbers"; import AccountEmailAddresses from "../../account/EmailAddresses"; import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; @@ -260,18 +260,35 @@ export default class GeneralUserSettingsTab extends React.Component { - // TODO: Figure out a design that doesn't involve replacing the current dialog - let errMsg = err.error || err.message || ""; - if (err.httpStatus === 403) { - errMsg = _t("Failed to change password. Is your password correct?"); - } else if (!errMsg) { - errMsg += ` (HTTP status ${err.httpStatus})`; + private onPasswordChangeError = (err: Error): void => { + logger.error("Failed to change password: " + err); + + let underlyingError = err; + if (err instanceof UserFriendlyError && err.cause instanceof Error) { + underlyingError = err.cause; } - logger.error("Failed to change password: " + errMsg); + + const errorMessage = extractErrorMessageFromError( + err, + _t("Unknown password change error (%(stringifiedError)s)", { + stringifiedError: String(err), + }), + ); + + let errorMessageToDisplay = errorMessage; + if (underlyingError instanceof HTTPError && underlyingError.httpStatus === 403) { + errorMessageToDisplay = _t("Failed to change password. Is your password correct?"); + } else if (underlyingError instanceof HTTPError) { + errorMessageToDisplay = _t("%(errorMessage)s (HTTP status %(httpStatus)s)", { + errorMessage, + httpStatus: underlyingError.httpStatus, + }); + } + + // TODO: Figure out a design that doesn't involve replacing the current dialog Modal.createDialog(ErrorDialog, { - title: _t("Error"), - description: errMsg, + title: _t("Error changing password"), + description: errorMessageToDisplay, }); }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dd9afaf5d75..003c0b49c83 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1344,6 +1344,7 @@ "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.": "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.", "You can also ask your homeserver admin to upgrade the server to change this behaviour.": "You can also ask your homeserver admin to upgrade the server to change this behaviour.", "Export E2E room keys": "Export E2E room keys", + "Error while changing password: %(error)s": "Error while changing password: %(error)s", "New passwords don't match": "New passwords don't match", "Passwords can't be empty": "Passwords can't be empty", "Do you want to set an email address?": "Do you want to set an email address?", @@ -1546,7 +1547,10 @@ "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Customise your appearance": "Customise your appearance", "Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.", + "Unknown password change error (%(stringifiedError)s)": "Unknown password change error (%(stringifiedError)s)", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", + "%(errorMessage)s (HTTP status %(httpStatus)s)": "%(errorMessage)s (HTTP status %(httpStatus)s)", + "Error changing password": "Error changing password", "Your password was successfully changed.": "Your password was successfully changed.", "You will not receive push notifications on other devices until you sign back in to them.": "You will not receive push notifications on other devices until you sign back in to them.", "Success": "Success", From ca102c4d3623c9cf5121d7f69f463d62ddd3095c Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 24 Apr 2023 08:21:24 +0000 Subject: [PATCH 040/105] Remove some obsolete button styles from `_common.pcss` (#10548) * Remove obsolete style rules - `mx_linkButton` The block was added with fbab8ceeef0d4e313d58a774915452718ed475bf for CreateKeyBackupDialog.js. It has been replaced with AccessibleButton element (see: onSkipPassPhraseClick on L283 and changeText on L314-316). The last function which had used mx_linkButton (_onOptOutClick) has been removed. Signed-off-by: Suguru Hirahara * Remove obsolete style rules - `mx_textButton` mx_textButton has been replaced with AccessibleButton (with kind="link"). There is also not an element which uses the class. Signed-off-by: Suguru Hirahara --------- Signed-off-by: Suguru Hirahara --- res/css/_common.pcss | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index dae1f8c601c..d847381077c 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -657,11 +657,6 @@ legend { margin: auto; } -.mx_linkButton { - cursor: pointer; - color: $accent; -} - .mx_TextInputDialog_label { text-align: left; padding-bottom: 12px; @@ -676,16 +671,6 @@ legend { background-color: $background; } -@define-mixin mx_DialogButton_small { - @mixin mx_DialogButton; - font-size: $font-15px; - padding: 0px 1.5em 0px 1.5em; -} - -.mx_textButton { - @mixin mx_DialogButton_small; -} - .mx_button_row { margin-top: 69px; } From 2486e7a90aefc4f1f88108c631612d6167055d6d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 24 Apr 2023 11:24:28 +0300 Subject: [PATCH 041/105] Fix default content in devtools event sender (#10699) It regressed from `{\n\n}` to an empty string in #10391 Signed-off-by: Tulir Asokan --- src/components/views/dialogs/devtools/Event.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 3e0e681c225..c63659a9150 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -182,7 +182,7 @@ export const TimelineEventEditor: React.FC = ({ mxEvent, onBack }) return cli.sendEvent(context.room.roomId, eventType, content || {}); }; - let defaultContent = ""; + let defaultContent: string | undefined; if (mxEvent) { const originalContent = mxEvent.getContent(); From 25ac825ae558368c18f7ea41ea7d160fd4b8716d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2023 09:24:50 +0100 Subject: [PATCH 042/105] Cypress use the simulated merge commit for the source of tests (#10688) * Cypress use the simulated merge commit for the source of tests * Add comments * Update .github/workflows/cypress.yaml Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- .github/workflows/cypress.yaml | 29 ++++++++++++++++++++--------- .github/workflows/element-web.yaml | 2 ++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 8c91f56ad6c..374d51da986 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -106,15 +106,6 @@ jobs: # supposed to be covered by STIXGeneral. tools: fonts-stix - - uses: actions/checkout@v3 - with: - # XXX: We're checking out untrusted code in a secure context - # We need to be careful to not trust anything this code outputs/may do - # We need to check this out to access the cypress tests which are on the head branch - repository: ${{ github.event.workflow_run.head_repository.full_name }} - ref: ${{ github.event.workflow_run.head_sha }} - persist-credentials: false - # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess: - name: 📥 Download artifact @@ -124,6 +115,25 @@ jobs: name: previewbuild path: webapp + # The workflow_run.head_sha is the sha of the head commit but the element-web was built using a simulated + # merge commit - https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request + # so use the sha from the tarball for the checkout of the cypress tests + # to make sure we get a matching set of code and tests. + - name: Grab sha from webapp + id: sha + run: | + echo "sha=$(cat webapp/sha)" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v3 + with: + # XXX: We're checking out untrusted code in a secure context + # We need to be careful to not trust anything this code outputs/may do + # We need to check this out to access the cypress tests which are on the head branch + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ steps.sha.outputs.sha }} + persist-credentials: false + path: matrix-react-sdk + - name: Run Cypress tests uses: cypress-io/github-action@a29a621b736c9a8547ba0236cc9cee1e49725bd5 with: @@ -137,6 +147,7 @@ jobs: command-prefix: "yarn percy exec --parallel --" config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" } }' ci-build-id: ${{ needs.prepare.outputs.uuid }} + project: matrix-react-sdk env: # pass the Dashboard record key as an environment variable CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/.github/workflows/element-web.yaml b/.github/workflows/element-web.yaml index e5fb48fb888..022c293abe1 100644 --- a/.github/workflows/element-web.yaml +++ b/.github/workflows/element-web.yaml @@ -43,6 +43,7 @@ jobs: run: cp element.io/develop/config.json config.json working-directory: ./element-web + # After building we write the version file and the react-sdk sha so our cypress tests are from the same sha - name: Build env: CI_PACKAGE: true @@ -50,6 +51,7 @@ jobs: run: | yarn build echo $VERSION > webapp/version + echo $GITHUB_SHA > webapp/sha working-directory: ./element-web - name: Upload Artifact From 2ae50940c87210920c0a97bbe29746ce1766eae3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2023 11:01:55 +0100 Subject: [PATCH 043/105] Update cypress.yaml --- .github/workflows/cypress.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 374d51da986..11c92dbd7eb 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -136,18 +136,18 @@ jobs: - name: Run Cypress tests uses: cypress-io/github-action@a29a621b736c9a8547ba0236cc9cee1e49725bd5 + working-directory: matrix-react-sdk with: # The built-in Electron runner seems to grind to a halt trying # to run the tests, so use chrome. browser: "${{ env.BROWSER_PATH }}" - start: npx serve -p 8080 webapp + start: npx serve -p 8080 ../webapp wait-on: "http://localhost:8080" record: true parallel: true command-prefix: "yarn percy exec --parallel --" config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" } }' ci-build-id: ${{ needs.prepare.outputs.uuid }} - project: matrix-react-sdk env: # pass the Dashboard record key as an environment variable CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} From 8c811776bba6a99ce8456f9fded0003d05858039 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2023 11:08:35 +0100 Subject: [PATCH 044/105] Update cypress.yaml --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 11c92dbd7eb..e10272b3adc 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -136,8 +136,8 @@ jobs: - name: Run Cypress tests uses: cypress-io/github-action@a29a621b736c9a8547ba0236cc9cee1e49725bd5 - working-directory: matrix-react-sdk with: + working-directory: matrix-react-sdk # The built-in Electron runner seems to grind to a halt trying # to run the tests, so use chrome. browser: "${{ env.BROWSER_PATH }}" From aa8c0f5cc7e06d8a76a719cdb6de40436e3fdd1d Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 24 Apr 2023 11:02:17 +0100 Subject: [PATCH 045/105] Honour feature toggles in guest mode (#10651) * Honour feature toggles in guest mode * Suppress TS warning about returning null MatrixClient * Revert "Suppress TS warning about returning null MatrixClient" Don't ts-ignore this - we will eventually fix it via the strict work. This reverts commit 0c657e6afd4c69d07c39bc2783184f36feaf940f. --- .../handlers/DeviceSettingsHandler.ts | 11 +-- .../handlers/DeviceSettingsHandler-test.ts | 81 +++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 test/settings/handlers/DeviceSettingsHandler-test.ts diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts index e50015f847e..978c83c1a6f 100644 --- a/src/settings/handlers/DeviceSettingsHandler.ts +++ b/src/settings/handlers/DeviceSettingsHandler.ts @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClientPeg } from "../../MatrixClientPeg"; import { SettingLevel } from "../SettingLevel"; import { CallbackFn, WatchManager } from "../WatchManager"; import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; @@ -117,10 +116,12 @@ export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsH // public for access to migrations - not exposed from the SettingsHandler interface public readFeature(featureName: string): boolean | null { - if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) { - // Guests should not have any labs features enabled. - return false; - } + // Previously, we disabled all features for guests, but since different + // installations can have site-specific config files which might set up + // different behaviour that is relevant to guests, we removed that + // special behaviour. See + // https://github.com/vector-im/element-web/issues/24513 for the + // discussion. // XXX: This turns they key names into `mx_labs_feature_feature_x` (double feature). // This is because all feature names start with `feature_` as a matter of policy. diff --git a/test/settings/handlers/DeviceSettingsHandler-test.ts b/test/settings/handlers/DeviceSettingsHandler-test.ts new file mode 100644 index 00000000000..19d19d4c819 --- /dev/null +++ b/test/settings/handlers/DeviceSettingsHandler-test.ts @@ -0,0 +1,81 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import DeviceSettingsHandler from "../../../src/settings/handlers/DeviceSettingsHandler"; +import { CallbackFn, WatchManager } from "../../../src/settings/WatchManager"; +import { stubClient } from "../../test-utils/test-utils"; + +describe("DeviceSettingsHandler", () => { + const ROOM_ID_IS_UNUSED = ""; + + const unknownSettingKey = "unknown_setting"; + const featureKey = "my_feature"; + + let watchers: WatchManager; + let handler: DeviceSettingsHandler; + let settingListener: CallbackFn; + + beforeEach(() => { + watchers = new WatchManager(); + handler = new DeviceSettingsHandler([featureKey], watchers); + settingListener = jest.fn(); + }); + + afterEach(() => { + watchers.unwatchSetting(settingListener); + }); + + it("Returns undefined for an unknown setting", () => { + expect(handler.getValue(unknownSettingKey, ROOM_ID_IS_UNUSED)).toBeUndefined(); + }); + + it("Returns the value for a disabled feature", () => { + handler.setValue(featureKey, ROOM_ID_IS_UNUSED, false); + expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(false); + }); + + it("Returns the value for an enabled feature", () => { + handler.setValue(featureKey, ROOM_ID_IS_UNUSED, true); + expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(true); + }); + + describe("If I am a guest", () => { + let client: MatrixClient; + + beforeEach(() => { + client = stubClient(); + mocked(client.isGuest).mockReturnValue(true); + }); + + afterEach(() => { + MatrixClientPeg.get = () => null; + }); + + it("Returns the value for a disabled feature", () => { + handler.setValue(featureKey, ROOM_ID_IS_UNUSED, false); + expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(false); + }); + + it("Returns the value for an enabled feature", () => { + handler.setValue(featureKey, ROOM_ID_IS_UNUSED, true); + expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(true); + }); + }); +}); From d7bb8043ea6b342c5835c78692abae2251f49339 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 Apr 2023 14:19:46 +0100 Subject: [PATCH 046/105] Replace uses of `checkDeviceTrust` with `getDeviceVerificationStatus` (#10663) matrix-org/matrix-js-sdk#3287 and matrix-org/matrix-js-sdk#3303 added a new API called getDeviceVerificationStatus. Let's use it. --- src/DeviceListener.ts | 12 +++- src/SlashCommands.tsx | 4 +- src/components/views/right_panel/UserInfo.tsx | 58 ++++++++++++------- src/components/views/rooms/EventTile.tsx | 14 ++++- src/components/views/rooms/MemberTile.tsx | 7 ++- .../views/settings/DevicesPanel.tsx | 46 +++++++-------- .../views/settings/devices/useOwnDevices.ts | 34 ++++++----- src/i18n/strings/en_EN.json | 1 - src/stores/SetupEncryptionStore.ts | 22 ++++--- src/toasts/UnverifiedSessionToast.tsx | 2 +- src/utils/ShieldUtils.ts | 6 +- src/utils/arrays.ts | 10 ++++ src/utils/device/isDeviceVerified.ts | 6 +- test/DeviceListener-test.ts | 30 ++++++---- .../views/right_panel/UserInfo-test.tsx | 46 ++++++++++----- .../components/views/rooms/EventTile-test.tsx | 31 ++++++---- .../views/settings/DevicesPanel-test.tsx | 8 ++- .../tabs/user/SessionManagerTab-test.tsx | 56 ++++++++++-------- test/test-utils/test-utils.ts | 2 +- test/toasts/UnverifiedSessionToast-test.tsx | 9 ++- test/utils/ShieldUtils-test.ts | 19 +++--- test/utils/arrays-test.ts | 24 ++++++++ 22 files changed, 286 insertions(+), 161 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 3281c43267f..afec1f62f47 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -310,7 +310,11 @@ export default class DeviceListener { const newUnverifiedDeviceIds = new Set(); const isCurrentDeviceTrusted = - crossSigningReady && (await cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!).isCrossSigningVerified()); + crossSigningReady && + Boolean( + (await cli.getCrypto()?.getDeviceVerificationStatus(cli.getUserId()!, cli.deviceId!)) + ?.crossSigningVerified, + ); // as long as cross-signing isn't ready, // you can't see or dismiss any device toasts @@ -319,8 +323,10 @@ export default class DeviceListener { for (const device of devices) { if (device.deviceId === cli.deviceId) continue; - const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!); - if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { + const deviceTrust = await cli + .getCrypto()! + .getDeviceVerificationStatus(cli.getUserId()!, device.deviceId!); + if (!deviceTrust?.crossSigningVerified && !this.dismissed.has(device.deviceId)) { if (this.ourDeviceIdsAtStart?.has(device.deviceId)) { oldUnverifiedDeviceIds.add(device.deviceId); } else { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index d06e6858bc1..f3355388f28 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1073,9 +1073,9 @@ export const Commands = [ }, ); } - const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); + const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); - if (deviceTrust.isVerified()) { + if (deviceTrust?.isVerified()) { if (device.getFingerprint() === fingerprint) { throw new UserFriendlyError("Session already verified!"); } else { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 270e837a703..f6f90313191 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -79,6 +79,7 @@ import PosthogTrackers from "../../../PosthogTrackers"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { asyncSome } from "../../../utils/arrays"; export interface IDevice extends DeviceInfo { ambiguous?: boolean; @@ -101,22 +102,22 @@ export const disambiguateDevices = (devices: IDevice[]): void => { } }; -export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice[]): E2EStatus => { +export const getE2EStatus = async (cli: MatrixClient, userId: string, devices: IDevice[]): Promise => { const isMe = userId === cli.getUserId(); const userTrust = cli.checkUserTrust(userId); if (!userTrust.isCrossSigningVerified()) { return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal; } - const anyDeviceUnverified = devices.some((device) => { + const anyDeviceUnverified = await asyncSome(devices, async (device) => { const { deviceId } = device; // For your own devices, we use the stricter check of cross-signing // verification to encourage everyone to trust their own devices via // cross-signing so that other users can then safely trust you. // For other people's devices, the more general verified check that // includes locally verified devices can be used. - const deviceTrust = cli.checkDeviceTrust(userId, deviceId); - return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); + const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); + return isMe ? !deviceTrust?.crossSigningVerified : !deviceTrust?.isVerified(); }); return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified; }; @@ -161,14 +162,20 @@ function useHasCrossSigningKeys( export function DeviceItem({ userId, device }: { userId: string; device: IDevice }): JSX.Element { const cli = useContext(MatrixClientContext); const isMe = userId === cli.getUserId(); - const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); const userTrust = cli.checkUserTrust(userId); - // For your own devices, we use the stricter check of cross-signing - // verification to encourage everyone to trust their own devices via - // cross-signing so that other users can then safely trust you. - // For other people's devices, the more general verified check that - // includes locally verified devices can be used. - const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); + + /** is the device verified? */ + const isVerified = useAsyncMemo(async () => { + const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, device.deviceId); + if (!deviceTrust) return false; + + // For your own devices, we use the stricter check of cross-signing + // verification to encourage everyone to trust their own devices via + // cross-signing so that other users can then safely trust you. + // For other people's devices, the more general verified check that + // includes locally verified devices can be used. + return isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified(); + }, [cli, userId, device]); const classes = classNames("mx_UserInfo_device", { mx_UserInfo_device_verified: isVerified, @@ -199,7 +206,10 @@ export function DeviceItem({ userId, device }: { userId: string; device: IDevice let trustedLabel: string | undefined; if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); - if (isVerified) { + if (isVerified === undefined) { + // we're still deciding if the device is verified + return
    ; + } else if (isVerified) { return (
    @@ -232,15 +242,17 @@ function DevicesSection({ const [isExpanded, setExpanded] = useState(false); - if (loading) { + const deviceTrusts = useAsyncMemo(() => { + const cryptoApi = cli.getCrypto(); + if (!cryptoApi) return Promise.resolve(undefined); + return Promise.all(devices.map((d) => cryptoApi.getDeviceVerificationStatus(userId, d.deviceId))); + }, [cli, userId, devices]); + + if (loading || deviceTrusts === undefined) { // still loading return ; } - if (devices === null) { - return

    {_t("Unable to load session list")}

    ; - } const isMe = userId === cli.getUserId(); - const deviceTrusts = devices.map((d) => cli.checkDeviceTrust(userId, d.deviceId)); let expandSectionDevices: IDevice[] = []; const unverifiedDevices: IDevice[] = []; @@ -258,7 +270,7 @@ function DevicesSection({ // cross-signing so that other users can then safely trust you. // For other people's devices, the more general verified check that // includes locally verified devices can be used. - const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); + const isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified()); if (isVerified) { expandSectionDevices.push(device); @@ -1611,10 +1623,12 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha const isRoomEncrypted = useIsEncrypted(cli, room); const devices = useDevices(user.userId) ?? []; - let e2eStatus: E2EStatus | undefined; - if (isRoomEncrypted && devices) { - e2eStatus = getE2EStatus(cli, user.userId, devices); - } + const e2eStatus = useAsyncMemo(async () => { + if (!isRoomEncrypted || !devices) { + return undefined; + } + return await getE2EStatus(cli, user.userId, devices); + }, [cli, isRoomEncrypted, user.userId, devices]); const classes = ["mx_UserInfo"]; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index fb44993553a..a6393b7c825 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -265,6 +265,8 @@ export class UnwrappedEventTile extends React.Component public static contextType = RoomContext; public context!: React.ContextType; + private unmounted = false; + public constructor(props: EventTileProps, context: React.ContextType) { super(props, context); @@ -420,6 +422,7 @@ export class UnwrappedEventTile extends React.Component this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); } this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); + this.unmounted = false; } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -561,7 +564,7 @@ export class UnwrappedEventTile extends React.Component this.verifyEvent(); }; - private verifyEvent(): void { + private async verifyEvent(): Promise { // if the event was edited, show the verification info for the edit, not // the original const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; @@ -590,7 +593,14 @@ export class UnwrappedEventTile extends React.Component } const eventSenderTrust = - encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId); + senderId && + encryptionInfo.sender && + (await MatrixClientPeg.get() + .getCrypto() + ?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId)); + + if (this.unmounted) return; + if (!eventSenderTrust) { this.setState({ verified: E2EState.Unknown }); return; diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx index cac793066b3..7ff46c922f7 100644 --- a/src/components/views/rooms/MemberTile.tsx +++ b/src/components/views/rooms/MemberTile.tsx @@ -33,6 +33,7 @@ import MemberAvatar from "./../avatars/MemberAvatar"; import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { E2EState } from "./E2EIcon"; +import { asyncSome } from "../../../utils/arrays"; interface IProps { member: RoomMember; @@ -127,15 +128,15 @@ export default class MemberTile extends React.Component { } const devices = cli.getStoredDevicesForUser(userId); - const anyDeviceUnverified = devices.some((device) => { + const anyDeviceUnverified = await asyncSome(devices, async (device) => { const { deviceId } = device; // For your own devices, we use the stricter check of cross-signing // verification to encourage everyone to trust their own devices via // cross-signing so that other users can then safely trust you. // For other people's devices, the more general verified check that // includes locally verified devices can be used. - const deviceTrust = cli.checkDeviceTrust(userId, deviceId); - return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); + const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); + return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified()); }); this.setState({ e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified, diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index 603438e7e5f..06bdc5fea99 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -26,14 +26,15 @@ import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { isDeviceVerified } from "../../../utils/device/isDeviceVerified"; +import { fetchExtendedDeviceInformation } from "./devices/useOwnDevices"; +import { DevicesDictionary, ExtendedDevice } from "./devices/types"; interface IProps { className?: string; } interface IState { - devices: IMyDevice[]; + devices?: DevicesDictionary; deviceLoadError?: string; selectedDevices: string[]; deleting?: boolean; @@ -47,7 +48,6 @@ export default class DevicesPanel extends React.Component { public constructor(props: IProps) { super(props); this.state = { - devices: [], selectedDevices: [], }; this.loadDevices = this.loadDevices.bind(this); @@ -70,18 +70,16 @@ export default class DevicesPanel extends React.Component { private loadDevices(): void { const cli = this.context; - cli.getDevices().then( - (resp) => { + fetchExtendedDeviceInformation(cli).then( + (devices) => { if (this.unmounted) { return; } this.setState((state, props) => { - const deviceIds = resp.devices.map((device) => device.device_id); - const selectedDevices = state.selectedDevices.filter((deviceId) => deviceIds.includes(deviceId)); return { - devices: resp.devices || [], - selectedDevices, + devices: devices, + selectedDevices: state.selectedDevices.filter((deviceId) => devices.hasOwnProperty(deviceId)), }; }); }, @@ -119,10 +117,6 @@ export default class DevicesPanel extends React.Component { return idA < idB ? -1 : idA > idB ? 1 : 0; } - private isDeviceVerified(device: IMyDevice): boolean | null { - return isDeviceVerified(this.context, device.device_id); - } - private onDeviceSelectionToggled = (device: IMyDevice): void => { if (this.unmounted) { return; @@ -205,15 +199,15 @@ export default class DevicesPanel extends React.Component { } }; - private renderDevice = (device: IMyDevice): JSX.Element => { - const myDeviceId = this.context.getDeviceId(); - const myDevice = this.state.devices.find((device) => device.device_id === myDeviceId); + private renderDevice = (device: ExtendedDevice): JSX.Element => { + const myDeviceId = this.context.getDeviceId()!; + const myDevice = this.state.devices?.[myDeviceId]; const isOwnDevice = device.device_id === myDeviceId; // If our own device is unverified, it can't verify other // devices, it can only request verification for itself - const canBeVerified = (myDevice && this.isDeviceVerified(myDevice)) || isOwnDevice; + const canBeVerified = (myDevice && myDevice.isVerified) || isOwnDevice; return ( { device={device} selected={this.state.selectedDevices.includes(device.device_id)} isOwnDevice={isOwnDevice} - verified={this.isDeviceVerified(device)} + verified={device.isVerified} canBeVerified={canBeVerified} onDeviceChange={this.loadDevices} onDeviceToggled={this.onDeviceSelectionToggled} @@ -242,21 +236,21 @@ export default class DevicesPanel extends React.Component { return ; } - const myDeviceId = this.context.getDeviceId(); - const myDevice = devices.find((device) => device.device_id === myDeviceId); + const myDeviceId = this.context.getDeviceId()!; + const myDevice = devices[myDeviceId]; if (!myDevice) { return loadError; } - const otherDevices = devices.filter((device) => device.device_id !== myDeviceId); + const otherDevices = Object.values(devices).filter((device) => device.device_id !== myDeviceId); otherDevices.sort(this.deviceCompare); - const verifiedDevices: IMyDevice[] = []; - const unverifiedDevices: IMyDevice[] = []; - const nonCryptoDevices: IMyDevice[] = []; + const verifiedDevices: ExtendedDevice[] = []; + const unverifiedDevices: ExtendedDevice[] = []; + const nonCryptoDevices: ExtendedDevice[] = []; for (const device of otherDevices) { - const verified = this.isDeviceVerified(device); + const verified = device.isVerified; if (verified === true) { verifiedDevices.push(device); } else if (verified === false) { @@ -266,7 +260,7 @@ export default class DevicesPanel extends React.Component { } } - const section = (trustIcon: JSX.Element, title: string, deviceList: IMyDevice[]): JSX.Element => { + const section = (trustIcon: JSX.Element, title: string, deviceList: ExtendedDevice[]): JSX.Element => { if (deviceList.length === 0) { return ; } diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index ffd0e00eba2..e4753cad43f 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -50,24 +50,26 @@ const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyD }; }; -const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise => { +/** + * Fetch extended details of the user's own devices + * + * @param matrixClient - Matrix Client + * @returns A dictionary mapping from device ID to ExtendedDevice + */ +export async function fetchExtendedDeviceInformation(matrixClient: MatrixClient): Promise { const { devices } = await matrixClient.getDevices(); - const devicesDict = devices.reduce( - (acc, device: IMyDevice) => ({ - ...acc, - [device.device_id]: { - ...device, - isVerified: isDeviceVerified(matrixClient, device.device_id), - ...parseDeviceExtendedInformation(matrixClient, device), - ...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]), - }, - }), - {}, - ); - + const devicesDict: DevicesDictionary = {}; + for (const device of devices) { + devicesDict[device.device_id] = { + ...device, + isVerified: await isDeviceVerified(matrixClient, device.device_id), + ...parseDeviceExtendedInformation(matrixClient, device), + ...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]), + }; + } return devicesDict; -}; +} export enum OwnDevicesError { Unsupported = "Unsupported", @@ -112,7 +114,7 @@ export const useOwnDevices = (): DevicesState => { const refreshDevices = useCallback(async (): Promise => { setIsLoadingDeviceList(true); try { - const devices = await fetchDevicesWithVerification(matrixClient); + const devices = await fetchExtendedDeviceInformation(matrixClient); setDevices(devices); const { pushers } = await matrixClient.getPushers(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 003c0b49c83..4b71b883b05 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2256,7 +2256,6 @@ "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", - "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index d2d2ee0a4d8..46c0193f94f 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -109,14 +109,20 @@ export class SetupEncryptionStore extends EventEmitter { const dehydratedDevice = await cli.getDehydratedDevice(); const ownUserId = cli.getUserId()!; const crossSigningInfo = cli.getStoredCrossSigningForUser(ownUserId); - this.hasDevicesToVerifyAgainst = cli - .getStoredDevicesForUser(ownUserId) - .some( - (device) => - device.getIdentityKey() && - (!dehydratedDevice || device.deviceId != dehydratedDevice.device_id) && - crossSigningInfo?.checkDeviceTrust(crossSigningInfo, device, false, true).isCrossSigningVerified(), - ); + this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(ownUserId).some((device) => { + if (!device.getIdentityKey() || (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id)) { + return false; + } + // check if the device is signed by the cross-signing key stored for our user. Note that this is + // *different* to calling `cryptoApi.getDeviceVerificationStatus`, because even if we have stored + // a cross-signing key for our user, we don't necessarily trust it yet (In legacy Crypto, we have not + // yet imported it into `Crypto.crossSigningInfo`, which for maximal confusion is a different object to + // `Crypto.getStoredCrossSigningForUser(ownUserId)`). + // + // TODO: figure out wtf to to here for rust-crypto + const verificationStatus = crossSigningInfo?.checkDeviceTrust(crossSigningInfo, device, false, true); + return !!verificationStatus?.isCrossSigningVerified(); + }); this.phase = Phase.Intro; this.emit("update"); diff --git a/src/toasts/UnverifiedSessionToast.tsx b/src/toasts/UnverifiedSessionToast.tsx index 8d619d87675..b3e9a63b591 100644 --- a/src/toasts/UnverifiedSessionToast.tsx +++ b/src/toasts/UnverifiedSessionToast.tsx @@ -48,7 +48,7 @@ export const showToast = async (deviceId: string): Promise => { const device = await cli.getDevice(deviceId); const extendedDevice = { ...device, - isVerified: isDeviceVerified(cli, deviceId), + isVerified: await isDeviceVerified(cli, deviceId), deviceType: DeviceType.Unknown, }; diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 089cf3feeb7..a9efe4584f7 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -18,6 +18,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import DMRoomMap from "./DMRoomMap"; +import { asyncSome } from "./arrays"; export enum E2EStatus { Warning = "warning", @@ -54,8 +55,9 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro const targets = includeUser ? [...verified, client.getUserId()!] : verified; for (const userId of targets) { const devices = client.getStoredDevicesForUser(userId); - const anyDeviceNotVerified = devices.some(({ deviceId }) => { - return !client.checkDeviceTrust(userId, deviceId).isVerified(); + const anyDeviceNotVerified = await asyncSome(devices, async ({ deviceId }) => { + const verificationStatus = await client.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); + return !verificationStatus?.isVerified(); }); if (anyDeviceNotVerified) { return E2EStatus.Warning; diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index bbc75fa4f7e..d9c7c3735ea 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -324,6 +324,16 @@ export async function asyncEvery(values: T[], predicate: (value: T) => Promis return true; } +/** + * Async version of Array.some. + */ +export async function asyncSome(values: T[], predicate: (value: T) => Promise): Promise { + for (const value of values) { + if (await predicate(value)) return true; + } + return false; +} + export function filterBoolean(values: Array): T[] { return values.filter(Boolean) as T[]; } diff --git a/src/utils/device/isDeviceVerified.ts b/src/utils/device/isDeviceVerified.ts index 0f8fdb6082e..1671fa01a30 100644 --- a/src/utils/device/isDeviceVerified.ts +++ b/src/utils/device/isDeviceVerified.ts @@ -25,10 +25,10 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; * @returns `true` if the device has been correctly cross-signed. `false` if the device is unknown or not correctly * cross-signed. `null` if there was an error fetching the device info. */ -export const isDeviceVerified = (client: MatrixClient, deviceId: string): boolean | null => { +export const isDeviceVerified = async (client: MatrixClient, deviceId: string): Promise => { try { - const trustLevel = client.checkDeviceTrust(client.getSafeUserId(), deviceId); - return trustLevel.isCrossSigningVerified(); + const trustLevel = await client.getCrypto()?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId); + return trustLevel?.crossSigningVerified ?? false; } catch (e) { console.error("Error getting device cross-signing info", e); return null; diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts index c0470200d7b..fe6a61c90ae 100644 --- a/test/DeviceListener-test.ts +++ b/test/DeviceListener-test.ts @@ -15,10 +15,10 @@ limitations under the License. */ import { Mocked, mocked } from "jest-mock"; -import { MatrixEvent, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room, MatrixClient, DeviceVerificationStatus, CryptoApi } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; -import { CrossSigningInfo, DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; @@ -60,6 +60,7 @@ const flushPromises = async () => await new Promise(process.nextTick); describe("DeviceListener", () => { let mockClient: Mocked | undefined; + let mockCrypto: Mocked | undefined; // spy on various toasts' hide and show functions // easier than mocking @@ -75,6 +76,11 @@ describe("DeviceListener", () => { mockPlatformPeg({ getAppVersion: jest.fn().mockResolvedValue("1.2.3"), }); + mockCrypto = { + getDeviceVerificationStatus: jest.fn().mockResolvedValue({ + crossSigningVerified: false, + }), + } as unknown as Mocked; mockClient = getMockClientWithEventEmitter({ isGuest: jest.fn(), getUserId: jest.fn().mockReturnValue(userId), @@ -97,7 +103,7 @@ describe("DeviceListener", () => { setAccountData: jest.fn(), getAccountData: jest.fn(), deleteAccountData: jest.fn(), - checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)), + getCrypto: jest.fn().mockReturnValue(mockCrypto), }); jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); @@ -391,14 +397,14 @@ describe("DeviceListener", () => { const device2 = new DeviceInfo("d2"); const device3 = new DeviceInfo("d3"); - const deviceTrustVerified = new DeviceTrustLevel(true, false, false, false); - const deviceTrustUnverified = new DeviceTrustLevel(false, false, false, false); + const deviceTrustVerified = new DeviceVerificationStatus({ crossSigningVerified: true }); + const deviceTrustUnverified = new DeviceVerificationStatus({}); beforeEach(() => { mockClient!.isCrossSigningReady.mockResolvedValue(true); mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2, device3]); // all devices verified by default - mockClient!.checkDeviceTrust.mockReturnValue(deviceTrustVerified); + mockCrypto!.getDeviceVerificationStatus.mockResolvedValue(deviceTrustVerified); mockClient!.deviceId = currentDevice.deviceId; jest.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === UIFeature.BulkUnverifiedSessionsReminder, @@ -423,7 +429,7 @@ describe("DeviceListener", () => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); // currentDevice, device2 are verified, device3 is unverified // ie if reminder was enabled it should be shown - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case currentDevice.deviceId: case device2.deviceId: @@ -438,7 +444,7 @@ describe("DeviceListener", () => { it("hides toast when current device is unverified", async () => { // device2 verified, current and device3 unverified - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case device2.deviceId: return deviceTrustVerified; @@ -454,7 +460,7 @@ describe("DeviceListener", () => { it("hides toast when reminder is snoozed", async () => { mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true); // currentDevice, device2 are verified, device3 is unverified - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case currentDevice.deviceId: case device2.deviceId: @@ -470,7 +476,7 @@ describe("DeviceListener", () => { it("shows toast with unverified devices at app start", async () => { // currentDevice, device2 are verified, device3 is unverified - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case currentDevice.deviceId: case device2.deviceId: @@ -488,7 +494,7 @@ describe("DeviceListener", () => { it("hides toast when unverified sessions at app start have been dismissed", async () => { // currentDevice, device2 are verified, device3 is unverified - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case currentDevice.deviceId: case device2.deviceId: @@ -510,7 +516,7 @@ describe("DeviceListener", () => { it("hides toast when unverified sessions are added after app start", async () => { // currentDevice, device2 are verified, device3 is unverified - mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { switch (deviceId) { case currentDevice.deviceId: case device2.deviceId: diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index 8e538577233..e7f3feb70cf 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -18,9 +18,18 @@ import React from "react"; import { fireEvent, render, screen, waitFor, cleanup, act, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Mocked, mocked } from "jest-mock"; -import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { + Room, + User, + MatrixClient, + RoomMember, + MatrixEvent, + EventType, + CryptoApi, + DeviceVerificationStatus, +} from "matrix-js-sdk/src/matrix"; import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { defer } from "matrix-js-sdk/src/utils"; @@ -79,6 +88,7 @@ const defaultUser = new User(defaultUserId); let mockRoom: Mocked; let mockSpace: Mocked; let mockClient: Mocked; +let mockCrypto: Mocked; beforeEach(() => { mockRoom = mocked({ @@ -115,6 +125,10 @@ beforeEach(() => { getEventReadUpTo: jest.fn(), } as unknown as Room); + mockCrypto = mocked({ + getDeviceVerificationStatus: jest.fn(), + } as unknown as CryptoApi); + mockClient = mocked({ getUser: jest.fn(), isGuest: jest.fn().mockReturnValue(false), @@ -134,13 +148,13 @@ beforeEach(() => { currentState: { on: jest.fn(), }, - checkDeviceTrust: jest.fn(), checkUserTrust: jest.fn(), getRoom: jest.fn(), credentials: {}, setPowerLevel: jest.fn(), downloadKeys: jest.fn(), getStoredDevicesForUser: jest.fn(), + getCrypto: jest.fn().mockReturnValue(mockCrypto), } as unknown as MatrixClient); jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); @@ -251,7 +265,6 @@ describe("", () => { beforeEach(() => { mockClient.isCryptoEnabled.mockReturnValue(true); mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(false, false, false)); - mockClient.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(false, false, false, false)); const device1 = DeviceInfo.fromStorage( { @@ -370,10 +383,10 @@ describe("", () => { mockClient.checkUserTrust.mockReturnValue({ isVerified: () => isVerified } as UserTrustLevel); }; const setMockDeviceTrust = (isVerified = false, isCrossSigningVerified = false) => { - mockClient.checkDeviceTrust.mockReturnValue({ + mockCrypto.getDeviceVerificationStatus.mockResolvedValue({ isVerified: () => isVerified, - isCrossSigningVerified: () => isCrossSigningVerified, - } as DeviceTrustLevel); + crossSigningVerified: isCrossSigningVerified, + } as DeviceVerificationStatus); }; const mockVerifyDevice = jest.spyOn(mockVerification, "verifyDevice"); @@ -384,7 +397,7 @@ describe("", () => { }); afterEach(() => { - mockClient.checkDeviceTrust.mockReset(); + mockCrypto.getDeviceVerificationStatus.mockReset(); mockClient.checkUserTrust.mockReset(); mockVerifyDevice.mockClear(); }); @@ -393,32 +406,36 @@ describe("", () => { mockVerifyDevice.mockRestore(); }); - it("with unverified user and device, displays button without a label", () => { + it("with unverified user and device, displays button without a label", async () => { renderComponent(); + await act(flushPromises); expect(screen.getByRole("button", { name: device.getDisplayName()! })).toBeInTheDocument; expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument(); }); - it("with verified user only, displays button with a 'Not trusted' label", () => { + it("with verified user only, displays button with a 'Not trusted' label", async () => { setMockUserTrust(true); renderComponent(); + await act(flushPromises); expect(screen.getByRole("button", { name: `${device.getDisplayName()} Not trusted` })).toBeInTheDocument; }); - it("with verified device only, displays no button without a label", () => { + it("with verified device only, displays no button without a label", async () => { setMockDeviceTrust(true); renderComponent(); + await act(flushPromises); expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); expect(screen.queryByText(/trusted/)).not.toBeInTheDocument(); }); - it("when userId is the same as userId from client, uses isCrossSigningVerified to determine if button is shown", () => { + it("when userId is the same as userId from client, uses isCrossSigningVerified to determine if button is shown", async () => { mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId); mockClient.getUserId.mockReturnValueOnce(defaultUserId); renderComponent(); + await act(flushPromises); // set trust to be false for isVerified, true for isCrossSigningVerified setMockDeviceTrust(false, true); @@ -428,10 +445,11 @@ describe("", () => { expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); }); - it("with verified user and device, displays no button and a 'Trusted' label", () => { + it("with verified user and device, displays no button and a 'Trusted' label", async () => { setMockUserTrust(true); setMockDeviceTrust(true); renderComponent(); + await act(flushPromises); expect(screen.queryByRole("button")).not.toBeInTheDocument; expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); @@ -441,6 +459,7 @@ describe("", () => { it("does not call verifyDevice if client.getUser returns null", async () => { mockClient.getUser.mockReturnValueOnce(null); renderComponent(); + await act(flushPromises); const button = screen.getByRole("button", { name: device.getDisplayName()! }); expect(button).toBeInTheDocument; @@ -455,6 +474,7 @@ describe("", () => { // even more mocking mockClient.isGuest.mockReturnValueOnce(true); renderComponent(); + await act(flushPromises); const button = screen.getByRole("button", { name: device.getDisplayName()! }); expect(button).toBeInTheDocument; diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index abbb3f70b49..5f067088aad 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -19,7 +19,7 @@ import { render, waitFor, screen, act, fireEvent } from "@testing-library/react" import { mocked } from "jest-mock"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; -import { TweakName } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, TweakName } from "matrix-js-sdk/src/matrix"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; @@ -30,7 +30,7 @@ import EventTile, { EventTileProps } from "../../../../src/components/views/room import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; +import { flushPromises, getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; import dis from "../../../../src/dispatcher/dispatcher"; @@ -221,13 +221,16 @@ describe("EventTile", () => { // a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not. const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false); const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false); - client.checkDeviceTrust = (userId, deviceId) => { - if (deviceId === TRUSTED_DEVICE.deviceId) { - return trustedDeviceTrustLevel; - } else { - return untrustedDeviceTrustLevel; - } - }; + const mockCrypto = { + getDeviceVerificationStatus: async (userId: string, deviceId: string) => { + if (deviceId === TRUSTED_DEVICE.deviceId) { + return trustedDeviceTrustLevel; + } else { + return untrustedDeviceTrustLevel; + } + }, + } as unknown as CryptoApi; + client.getCrypto = () => mockCrypto; }); it("shows a warning for an event from an unverified device", async () => { @@ -243,6 +246,7 @@ describe("EventTile", () => { } as IEncryptedEventInfo); const { container } = getComponent(); + await act(flushPromises); const eventTiles = container.getElementsByClassName("mx_EventTile"); expect(eventTiles).toHaveLength(1); @@ -270,6 +274,7 @@ describe("EventTile", () => { } as IEncryptedEventInfo); const { container } = getComponent(); + await act(flushPromises); const eventTiles = container.getElementsByClassName("mx_EventTile"); expect(eventTiles).toHaveLength(1); @@ -295,6 +300,7 @@ describe("EventTile", () => { } as IEncryptedEventInfo); const { container } = getComponent(); + await act(flushPromises); const eventTiles = container.getElementsByClassName("mx_EventTile"); expect(eventTiles).toHaveLength(1); @@ -317,8 +323,9 @@ describe("EventTile", () => { sender: UNTRUSTED_DEVICE, } as IEncryptedEventInfo); - act(() => { + await act(async () => { mxEvent.makeReplaced(replacementEvent); + flushPromises(); }); // check it was updated @@ -345,6 +352,7 @@ describe("EventTile", () => { } as IEncryptedEventInfo); const { container } = getComponent(); + await act(flushPromises); const eventTiles = container.getElementsByClassName("mx_EventTile"); expect(eventTiles).toHaveLength(1); @@ -363,8 +371,9 @@ describe("EventTile", () => { event: true, }); - act(() => { + await act(async () => { mxEvent.makeReplaced(replacementEvent); + await flushPromises(); }); // check it was updated diff --git a/test/components/views/settings/DevicesPanel-test.tsx b/test/components/views/settings/DevicesPanel-test.tsx index b7a0ec6a257..dd334d89475 100644 --- a/test/components/views/settings/DevicesPanel-test.tsx +++ b/test/components/views/settings/DevicesPanel-test.tsx @@ -18,7 +18,6 @@ import { act, fireEvent, render } from "@testing-library/react"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { sleep } from "matrix-js-sdk/src/utils"; import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event"; -import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel"; import { flushPromises, getMockClientWithEventEmitter, mkPusher, mockClientMethodsUser } from "../../../test-utils"; @@ -29,16 +28,21 @@ describe("", () => { const device1 = { device_id: "device_1" }; const device2 = { device_id: "device_2" }; const device3 = { device_id: "device_3" }; + const mockCrypto = { + getDeviceVerificationStatus: jest.fn().mockResolvedValue({ + crossSigningVerified: false, + }), + }; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), getDevices: jest.fn(), getDeviceId: jest.fn().mockReturnValue(device1.device_id), deleteMultipleDevices: jest.fn(), - checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)), getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")), generateClientSecret: jest.fn(), getPushers: jest.fn(), setPusher: jest.fn(), + getCrypto: jest.fn().mockReturnValue(mockCrypto), }); const getComponent = () => ( diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index b1648271869..6e982147bf5 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -18,7 +18,6 @@ import React from "react"; import { act, fireEvent, render, RenderResult } from "@testing-library/react"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { logger } from "matrix-js-sdk/src/logger"; -import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { defer, sleep } from "matrix-js-sdk/src/utils"; import { @@ -30,7 +29,10 @@ import { PUSHER_ENABLED, IAuthData, UNSTABLE_MSC3882_CAPABILITY, + CryptoApi, + DeviceVerificationStatus, } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; import { clearAllModals } from "../../../../../test-utils"; import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab"; @@ -78,9 +80,14 @@ describe("", () => { cancel: jest.fn(), on: jest.fn(), } as unknown as VerificationRequest; + + const mockCrypto = mocked({ + getDeviceVerificationStatus: jest.fn(), + } as unknown as CryptoApi); + const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(aliceId), - checkDeviceTrust: jest.fn(), + getCrypto: jest.fn().mockReturnValue(mockCrypto), getDevices: jest.fn(), getStoredDevice: jest.fn(), getDeviceId: jest.fn().mockReturnValue(deviceId), @@ -171,7 +178,7 @@ describe("", () => { const device = [alicesDevice, alicesMobileDevice].find((device) => device.device_id === id); return device ? new DeviceInfo(device.device_id) : null; }); - mockClient.checkDeviceTrust.mockReset().mockReturnValue(new DeviceTrustLevel(false, false, false, false)); + mockCrypto.getDeviceVerificationStatus.mockReset().mockResolvedValue(new DeviceVerificationStatus({})); mockClient.getDevices.mockReset().mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); @@ -221,13 +228,13 @@ describe("", () => { }); it("does not fail when checking device verification fails", async () => { - const logSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + const logSpy = jest.spyOn(console, "error").mockImplementation((e) => {}); mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice], }); - const noCryptoError = new Error("End-to-end encryption disabled"); - mockClient.checkDeviceTrust.mockImplementation(() => { - throw noCryptoError; + const failError = new Error("non-specific failure"); + mockCrypto.getDeviceVerificationStatus.mockImplementation(() => { + throw failError; }); render(getComponent()); @@ -236,9 +243,9 @@ describe("", () => { }); // called for each device despite error - expect(mockClient.checkDeviceTrust).toHaveBeenCalledWith(aliceId, alicesDevice.device_id); - expect(mockClient.checkDeviceTrust).toHaveBeenCalledWith(aliceId, alicesMobileDevice.device_id); - expect(logSpy).toHaveBeenCalledWith("Error getting device cross-signing info", noCryptoError); + expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledWith(aliceId, alicesDevice.device_id); + expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledWith(aliceId, alicesMobileDevice.device_id); + expect(logSpy).toHaveBeenCalledWith("Error getting device cross-signing info", failError); }); it("sets device verification status correctly", async () => { @@ -246,14 +253,14 @@ describe("", () => { devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice], }); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); - mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { // alices device is trusted if (deviceId === alicesDevice.device_id) { - return new DeviceTrustLevel(true, true, false, false); + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); } // alices mobile device is not if (deviceId === alicesMobileDevice.device_id) { - return new DeviceTrustLevel(false, false, false, false); + return new DeviceVerificationStatus({}); } // alicesOlderMobileDevice does not support encryption throw new Error("encryption not supported"); @@ -265,7 +272,7 @@ describe("", () => { await flushPromises(); }); - expect(mockClient.checkDeviceTrust).toHaveBeenCalledTimes(3); + expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledTimes(3); expect( getByTestId(`device-tile-${alicesDevice.device_id}`).querySelector('[aria-label="Verified"]'), ).toBeTruthy(); @@ -418,7 +425,9 @@ describe("", () => { devices: [alicesDevice, alicesMobileDevice], }); mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id)); - mockClient.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, true, false, false)); + mockCrypto.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }), + ); const { getByTestId } = render(getComponent()); @@ -520,11 +529,11 @@ describe("", () => { devices: [alicesDevice, alicesMobileDevice], }); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); - mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { if (deviceId === alicesDevice.device_id) { - return new DeviceTrustLevel(true, true, false, false); + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); } - return new DeviceTrustLevel(false, false, false, false); + return new DeviceVerificationStatus({}); }); const { getByTestId } = render(getComponent()); @@ -547,12 +556,13 @@ describe("", () => { devices: [alicesDevice, alicesMobileDevice], }); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); - mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { // current session verified = able to verify other sessions if (deviceId === alicesDevice.device_id) { - return new DeviceTrustLevel(true, true, false, false); + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); } // but alicesMobileDevice doesn't support encryption + // XXX this is not what happens if a device doesn't support encryption. throw new Error("encryption not supported"); }); @@ -581,11 +591,11 @@ describe("", () => { devices: [alicesDevice, alicesMobileDevice], }); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); - mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { if (deviceId === alicesDevice.device_id) { - return new DeviceTrustLevel(true, true, false, false); + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); } - return new DeviceTrustLevel(false, false, false, false); + return new DeviceVerificationStatus({}); }); const { getByTestId } = render(getComponent()); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index ddd6b091257..58668fa470b 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient { getDevice: jest.fn(), getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), getStoredCrossSigningForUser: jest.fn(), - checkDeviceTrust: jest.fn(), getStoredDevice: jest.fn(), requestVerification: jest.fn(), deviceId: "ABCDEFGHI", @@ -234,6 +233,7 @@ export function createTestClient(): MatrixClient { }), searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }), + getCrypto: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); diff --git a/test/toasts/UnverifiedSessionToast-test.tsx b/test/toasts/UnverifiedSessionToast-test.tsx index a65f7dec79f..2799f0f7916 100644 --- a/test/toasts/UnverifiedSessionToast-test.tsx +++ b/test/toasts/UnverifiedSessionToast-test.tsx @@ -18,9 +18,8 @@ import React from "react"; import { render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { mocked, Mocked } from "jest-mock"; -import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, DeviceVerificationStatus, IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; -import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import dis from "../../src/dispatcher/dispatcher"; import { showToast } from "../../src/toasts/UnverifiedSessionToast"; @@ -55,7 +54,11 @@ describe("UnverifiedSessionToast", () => { return null; }); - client.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, false, false, false)); + client.getCrypto.mockReturnValue({ + getDeviceVerificationStatus: jest + .fn() + .mockResolvedValue(new DeviceVerificationStatus({ crossSigningVerified: true })), + } as unknown as CryptoApi); jest.spyOn(dis, "dispatch"); jest.spyOn(DeviceListener.sharedInstance(), "dismissUnverifiedSessions"); }); diff --git a/test/utils/ShieldUtils-test.ts b/test/utils/ShieldUtils-test.ts index f971cea7798..8759252727b 100644 --- a/test/utils/ShieldUtils-test.ts +++ b/test/utils/ShieldUtils-test.ts @@ -22,21 +22,25 @@ import DMRoomMap from "../../src/utils/DMRoomMap"; function mkClient(selfTrust = false) { return { getUserId: () => "@self:localhost", + getCrypto: () => ({ + getDeviceVerificationStatus: (userId: string, deviceId: string) => + Promise.resolve({ + isVerified: () => (userId === "@self:localhost" ? selfTrust : userId[2] == "T"), + }), + }), checkUserTrust: (userId: string) => ({ isCrossSigningVerified: () => userId[1] == "T", wasCrossSigningVerified: () => userId[1] == "T" || userId[1] == "W", }), - checkDeviceTrust: (userId: string, deviceId: string) => ({ - isVerified: () => (userId === "@self:localhost" ? selfTrust : userId[2] == "T"), - }), getStoredDevicesForUser: (userId: string) => ["DEVICE"], } as unknown as MatrixClient; } describe("mkClient self-test", function () { - test.each([true, false])("behaves well for self-trust=%s", (v) => { + test.each([true, false])("behaves well for self-trust=%s", async (v) => { const client = mkClient(v); - expect(client.checkDeviceTrust("@self:localhost", "DEVICE").isVerified()).toBe(v); + const status = await client.getCrypto()!.getDeviceVerificationStatus("@self:localhost", "DEVICE"); + expect(status?.isVerified()).toBe(v); }); test.each([ @@ -53,8 +57,9 @@ describe("mkClient self-test", function () { ["@TF:h", false], ["@FT:h", true], ["@FF:h", false], - ])("behaves well for device trust %s", (userId, trust) => { - expect(mkClient().checkDeviceTrust(userId, "device").isVerified()).toBe(trust); + ])("behaves well for device trust %s", async (userId, trust) => { + const status = await mkClient().getCrypto()!.getDeviceVerificationStatus(userId, "device"); + expect(status?.isVerified()).toBe(trust); }); }); diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts index 558b85e0b5c..f843210dc4c 100644 --- a/test/utils/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -30,6 +30,7 @@ import { GroupedArray, concat, asyncEvery, + asyncSome, } from "../../src/utils/arrays"; type TestParams = { input: number[]; output: number[] }; @@ -444,4 +445,27 @@ describe("arrays", () => { expect(predicate).toHaveBeenCalledWith(2); }); }); + + describe("asyncSome", () => { + it("when called with an empty array, it should return false", async () => { + expect(await asyncSome([], jest.fn().mockResolvedValue(true))).toBe(false); + }); + + it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => { + const predicate = jest.fn().mockResolvedValue(false); + expect(await asyncSome([1, 2, 3], predicate)).toBe(false); + expect(predicate).toHaveBeenCalledTimes(3); + expect(predicate).toHaveBeenCalledWith(1); + expect(predicate).toHaveBeenCalledWith(2); + expect(predicate).toHaveBeenCalledWith(3); + }); + + it("when called with some items and the predicate resolves to true, it should short-circuit and return true", async () => { + const predicate = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true); + expect(await asyncSome([1, 2, 3], predicate)).toBe(true); + expect(predicate).toHaveBeenCalledTimes(2); + expect(predicate).toHaveBeenCalledWith(1); + expect(predicate).toHaveBeenCalledWith(2); + }); + }); }); From db4047991004d74008ae2ae0c5238573818940cd Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 24 Apr 2023 16:19:08 +0200 Subject: [PATCH 047/105] Element-R: Starting a DMs with a user (#10673) * Use `cli.getUserDeviceInfo` instead of `cli.downloadKeys` to create a room * Use `client.getCrypto().getUserDeviceInfo` instead of `client.getUserDeviceInfo` * Update `createRoom-test.ts` to use `getUserDeviceInfo` * Remove duplicate field --- src/createRoom.ts | 5 ++++- test/createRoom-test.ts | 19 ++++++++++--------- test/test-utils/test-utils.ts | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/createRoom.ts b/src/createRoom.ts index 2b5831498be..cf090b06ca4 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -397,7 +397,10 @@ export default async function createRoom(opts: IOpts): Promise { */ export async function canEncryptToAllUsers(client: MatrixClient, userIds: string[]): Promise { try { - const usersDeviceMap = await client.downloadKeys(userIds); + const usersDeviceMap = await client.getCrypto()?.getUserDeviceInfo(userIds, true); + if (!usersDeviceMap) { + return false; + } for (const devices of usersDeviceMap.values()) { if (devices.size === 0) { diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index 0c34041b071..a9387bfcdfb 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -15,8 +15,7 @@ limitations under the License. */ import { mocked, Mocked } from "jest-mock"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { CryptoApi, MatrixClient, Device } from "matrix-js-sdk/src/matrix"; import { RoomType } from "matrix-js-sdk/src/@types/event"; import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-utils"; @@ -151,30 +150,32 @@ describe("canEncryptToAllUsers", () => { const user2Id = "@user2:example.com"; const devices = new Map([ - ["DEV1", {} as unknown as DeviceInfo], - ["DEV2", {} as unknown as DeviceInfo], + ["DEV1", {} as unknown as Device], + ["DEV2", {} as unknown as Device], ]); let client: Mocked; + let cryptoApi: Mocked; beforeAll(() => { client = mocked(stubClient()); + cryptoApi = mocked(client.getCrypto()!); }); it("should return true if userIds is empty", async () => { - client.downloadKeys.mockResolvedValue(new Map()); + cryptoApi.getUserDeviceInfo.mockResolvedValue(new Map()); const result = await canEncryptToAllUsers(client, []); expect(result).toBe(true); }); it("should return true if download keys does not return any user", async () => { - client.downloadKeys.mockResolvedValue(new Map()); + cryptoApi.getUserDeviceInfo.mockResolvedValue(new Map()); const result = await canEncryptToAllUsers(client, [user1Id, user2Id]); expect(result).toBe(true); }); it("should return false if none of the users has a device", async () => { - client.downloadKeys.mockResolvedValue( + cryptoApi.getUserDeviceInfo.mockResolvedValue( new Map([ [user1Id, new Map()], [user2Id, new Map()], @@ -185,7 +186,7 @@ describe("canEncryptToAllUsers", () => { }); it("should return false if some of the users don't have a device", async () => { - client.downloadKeys.mockResolvedValue( + cryptoApi.getUserDeviceInfo.mockResolvedValue( new Map([ [user1Id, new Map()], [user2Id, devices], @@ -196,7 +197,7 @@ describe("canEncryptToAllUsers", () => { }); it("should return true if all users have a device", async () => { - client.downloadKeys.mockResolvedValue( + cryptoApi.getUserDeviceInfo.mockResolvedValue( new Map([ [user1Id, devices], [user2Id, devices], diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 58668fa470b..1c190b46164 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -120,6 +120,7 @@ export function createTestClient(): MatrixClient { downloadKeys: jest.fn(), }, }, + getCrypto: jest.fn().mockReturnValue({ getUserDeviceInfo: jest.fn() }), getPushActionsForEvent: jest.fn(), getRoom: jest.fn().mockImplementation((roomId) => mkStubRoom(roomId, "My room", client)), @@ -233,7 +234,6 @@ export function createTestClient(): MatrixClient { }), searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }), - getCrypto: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); From 619a9e85424c6fd32060313af99571634ac8f478 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 09:28:48 +0100 Subject: [PATCH 048/105] Conform more of the codebase with `strictNullChecks` (#10703) --- src/IdentityAuthClient.tsx | 2 +- src/LegacyCallHandler.tsx | 16 ++++++--- src/Lifecycle.ts | 16 ++++----- src/ScalarAuthClient.ts | 5 +-- src/components/views/auth/CountryDropdown.tsx | 2 +- .../views/dialogs/ServerPickerDialog.tsx | 8 ++--- .../dialogs/spotlight/SpotlightDialog.tsx | 3 +- .../views/elements/CopyableText.tsx | 5 +-- .../views/emojipicker/EmojiPicker.tsx | 12 +++---- .../views/rooms/EditMessageComposer.tsx | 9 +++-- src/components/views/rooms/RoomList.tsx | 15 +++++---- src/components/views/rooms/RoomTile.tsx | 3 +- .../hooks/usePlainTextListeners.ts | 2 +- .../views/settings/SecureBackupPanel.tsx | 6 ++-- .../tabs/user/LabsUserSettingsTab.tsx | 2 +- .../views/spaces/QuickSettingsButton.tsx | 4 +-- .../views/spaces/SpaceTreeLevel.tsx | 2 +- .../views/toasts/VerificationRequestToast.tsx | 8 +++-- src/customisations/Security.ts | 5 ++- src/integrations/IntegrationManagers.ts | 4 +-- src/linkify-matrix.ts | 10 +++--- src/stores/RoomViewStore.tsx | 7 ++-- src/stores/ThreepidInviteStore.ts | 6 +++- src/stores/spaces/SpaceStore.ts | 33 ++++++++++--------- 24 files changed, 108 insertions(+), 77 deletions(-) diff --git a/src/IdentityAuthClient.tsx b/src/IdentityAuthClient.tsx index 12f42a3add1..4df9959511f 100644 --- a/src/IdentityAuthClient.tsx +++ b/src/IdentityAuthClient.tsx @@ -118,7 +118,7 @@ export default class IdentityAuthClient { } private async checkToken(token: string): Promise { - const identityServerUrl = this.matrixClient.getIdentityServerUrl(); + const identityServerUrl = this.matrixClient.getIdentityServerUrl()!; try { await this.matrixClient.getIdentityAccount(token); diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index f84509238b7..71891a3e130 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -579,7 +579,7 @@ export default class LegacyCallHandler extends EventEmitter { }); }); call.on(CallEvent.Hangup, () => { - if (!this.matchesCallForThisRoom(call)) return; + if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; this.removeCallForRoom(mappedRoomId); }); @@ -587,7 +587,7 @@ export default class LegacyCallHandler extends EventEmitter { this.onCallStateChanged(newState, oldState, call); }); call.on(CallEvent.Replaced, (newCall: MatrixCall) => { - if (!this.matchesCallForThisRoom(call)) return; + if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; logger.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); @@ -603,7 +603,7 @@ export default class LegacyCallHandler extends EventEmitter { this.setCallState(newCall, newCall.state); }); call.on(CallEvent.AssertedIdentityChanged, async (): Promise => { - if (!this.matchesCallForThisRoom(call)) return; + if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); @@ -634,7 +634,7 @@ export default class LegacyCallHandler extends EventEmitter { const newMappedRoomId = this.roomIdForCall(call); logger.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); - if (newMappedRoomId !== mappedRoomId) { + if (newMappedRoomId && newMappedRoomId !== mappedRoomId) { this.removeCallForRoom(mappedRoomId); mappedRoomId = newMappedRoomId; logger.log("Moving call to room " + mappedRoomId); @@ -1116,6 +1116,14 @@ export default class LegacyCallHandler extends EventEmitter { public async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean): Promise { if (consultFirst) { const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination); + if (!dmRoomId) { + logger.log("Failed to transfer call, could not ensure dm exists"); + Modal.createDialog(ErrorDialog, { + title: _t("Transfer Failed"), + description: _t("Failed to transfer call"), + }); + return; + } this.placeCall(dmRoomId, call.type, call); dis.dispatch({ diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 6c2fbec8219..3c679c8a6d8 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -176,7 +176,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise */ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null, null]> { const { hsUrl, userId, hasAccessToken, isGuest } = await getStoredSessionVars(); - return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null]; + return hsUrl && userId && hasAccessToken ? [userId, !!isGuest] : [null, null]; } /** @@ -343,9 +343,9 @@ export interface IStoredSession { * may not be valid, as it is not tested for consistency here. * @returns {Object} Information about the session - see implementation for variables. */ -export async function getStoredSessionVars(): Promise { - const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); - const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); +export async function getStoredSessionVars(): Promise> { + const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY) ?? undefined; + const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) ?? undefined; let accessToken: string | undefined; try { accessToken = await StorageManager.idbLoad("account", "mx_access_token"); @@ -367,8 +367,8 @@ export async function getStoredSessionVars(): Promise { // if we pre-date storing "mx_has_access_token", but we retrieved an access // token, then we should say we have an access token const hasAccessToken = localStorage.getItem("mx_has_access_token") === "true" || !!accessToken; - const userId = localStorage.getItem("mx_user_id"); - const deviceId = localStorage.getItem("mx_device_id"); + const userId = localStorage.getItem("mx_user_id") ?? undefined; + const deviceId = localStorage.getItem("mx_device_id") ?? undefined; let isGuest: boolean; if (localStorage.getItem("mx_is_guest") !== null) { @@ -447,7 +447,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): } let decryptedAccessToken = accessToken; - const pickleKey = await PlatformPeg.get()?.getPickleKey(userId, deviceId); + const pickleKey = await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? ""); if (pickleKey) { logger.log("Got pickle key"); if (typeof accessToken !== "string") { @@ -740,7 +740,7 @@ export function logout(): void { _isLoggingOut = true; const client = MatrixClientPeg.get(); - PlatformPeg.get()?.destroyPickleKey(client.getSafeUserId(), client.getDeviceId()); + PlatformPeg.get()?.destroyPickleKey(client.getSafeUserId(), client.getDeviceId() ?? ""); client.logout(true).then(onLoggedOut, (err) => { // Just throwing an error here is going to be very unhelpful // if you're trying to log out because your server's down and diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index 558e6362b9f..5a57729e53b 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -50,7 +50,7 @@ export default class ScalarAuthClient { } private writeTokenToStore(): void { - window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); + window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken ?? ""); if (this.isDefaultManager) { // We remove the old token from storage to migrate upwards. This is safe // to do because even if the user switches to /app when this is on /develop @@ -260,7 +260,7 @@ export default class ScalarAuthClient { const roomId = room.roomId; const roomName = room.name; let url = this.uiUrl; - url += "?scalar_token=" + encodeURIComponent(this.scalarToken); + if (this.scalarToken) url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); url += "&room_name=" + encodeURIComponent(roomName); url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); @@ -274,6 +274,7 @@ export default class ScalarAuthClient { } public getStarterLink(starterLinkUrl: string): string { + if (!this.scalarToken) return starterLinkUrl; return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index a9061f6e5af..dc8ff2e1a3c 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -69,7 +69,7 @@ export default class CountryDropdown extends React.Component { const locale = new Intl.Locale(navigator.language ?? navigator.languages[0]); const code = locale.region ?? locale.language ?? locale.baseName; const displayNames = new Intl.DisplayNames(["en"], { type: "region" }); - const displayName = displayNames.of(code).toUpperCase(); + const displayName = displayNames.of(code)?.toUpperCase(); defaultCountry = COUNTRIES.find( (c) => c.iso2 === code.toUpperCase() || c.name.toUpperCase() === displayName, ); diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 2b208b29f3f..d7469b86946 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -49,7 +49,7 @@ export default class ServerPickerDialog extends React.PureComponent => { ev.preventDefault(); - const valid = await this.fieldRef.current.validate({ allowEmpty: false }); + const valid = await this.fieldRef.current?.validate({ allowEmpty: false }); if (!valid && !this.state.defaultChosen) { - this.fieldRef.current.focus(); - this.fieldRef.current.validate({ allowEmpty: false, focused: true }); + this.fieldRef.current?.focus(); + this.fieldRef.current?.validate({ allowEmpty: false, focused: true }); return; } diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 05ab8c1749a..2939d7f46d1 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -1089,7 +1089,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ev.stopPropagation(); ev.preventDefault(); - if (rovingContext.state.refs.length > 0) { + if (rovingContext.state.activeRef && rovingContext.state.refs.length > 0) { let refs = rovingContext.state.refs; if (!query && !filter !== null) { // If the current selection is not in the recently viewed row then only include the @@ -1112,6 +1112,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n if ( !query && !filter !== null && + rovingContext.state.activeRef && rovingContext.state.refs.length > 0 && refIsForRecentlyViewed(rovingContext.state.activeRef) ) { diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx index e1783013cb8..36da6cb4a21 100644 --- a/src/components/views/elements/CopyableText.tsx +++ b/src/components/views/elements/CopyableText.tsx @@ -25,7 +25,7 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton"; interface IProps { children?: React.ReactNode; - getTextToCopy: () => string; + getTextToCopy: () => string | null; border?: boolean; className?: string; } @@ -35,7 +35,8 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border = true const onCopyClickInternal = async (e: ButtonEvent): Promise => { e.preventDefault(); - const successful = await copyPlaintext(getTextToCopy()); + const text = getTextToCopy(); + const successful = !!text && (await copyPlaintext(text)); setTooltip(successful ? _t("Copied!") : _t("Failed to copy")); }; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 7a62c4dd079..45464a2a74f 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -162,9 +162,9 @@ class EmojiPicker extends React.Component { }; private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch): void { - const node = state.activeRef.current; - const parent = node.parentElement; - if (!parent) return; + const node = state.activeRef?.current; + const parent = node?.parentElement; + if (!parent || !state.activeRef) return; const rowIndex = Array.from(parent.children).indexOf(node); const refIndex = state.refs.indexOf(state.activeRef); @@ -173,12 +173,12 @@ class EmojiPicker extends React.Component { switch (ev.key) { case Key.ARROW_LEFT: focusRef = state.refs[refIndex - 1]; - newParent = focusRef?.current?.parentElement; + newParent = focusRef?.current?.parentElement ?? undefined; break; case Key.ARROW_RIGHT: focusRef = state.refs[refIndex + 1]; - newParent = focusRef?.current?.parentElement; + newParent = focusRef?.current?.parentElement ?? undefined; break; case Key.ARROW_UP: @@ -188,7 +188,7 @@ class EmojiPicker extends React.Component { ev.key === Key.ARROW_UP ? state.refs[refIndex - rowIndex - 1] : state.refs[refIndex - rowIndex + EMOJIS_PER_ROW]; - newParent = ref?.current?.parentElement; + newParent = ref?.current?.parentElement ?? undefined; const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)]; focusRef = state.refs.find((r) => r.current === newTarget); break; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 96c66dddfb6..c1fdecdd475 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -386,8 +386,8 @@ class EditMessageComposer extends React.Component ; -const auxButtonContextMenuPosition = (handle: RefObject): MenuProps => { - const rect = handle.current.getBoundingClientRect(); +const auxButtonContextMenuPosition = (handle: HTMLDivElement): MenuProps => { + const rect = handle.getBoundingClientRect(); return { chevronFace: ChevronFace.None, left: rect.left - 7, @@ -126,11 +126,11 @@ const DmAuxButton: React.FC = ({ tabIndex, dispatcher = default if (activeSpace && (showCreateRooms || showInviteUsers)) { let contextMenu: JSX.Element | undefined; - if (menuDisplayed) { + if (menuDisplayed && handle.current) { const canInvite = shouldShowSpaceInvite(activeSpace); contextMenu = ( - + {showCreateRooms && ( = ({ tabIndex }) => { } let contextMenu: JSX.Element | null = null; - if (menuDisplayed) { + if (menuDisplayed && handle.current) { contextMenu = ( - + {contextMenuContent} ); @@ -491,6 +491,7 @@ export default class RoomList extends React.PureComponent { if (payload.action === Action.ViewRoomDelta) { const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload; const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); + if (!currentRoomId) return; const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread); if (room) { defaultDispatcher.dispatch({ diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 87166c94a19..58664951df0 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -207,7 +207,8 @@ export class RoomTile extends React.PureComponent { return; } - const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + const messagePreview = + (await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag)) ?? undefined; this.setState({ messagePreview }); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index f8b045ad657..f918ceb50df 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -38,7 +38,7 @@ export function usePlainTextListeners( onChange?: (content: string) => void, onSend?: () => void, ): { - ref: RefObject; + ref: RefObject; content?: string; onInput(event: SyntheticEvent): void; onPaste(event: SyntheticEvent): void; diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 2b19a8af583..da67d6a919e 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -295,7 +295,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { const verify = (sub: string): JSX.Element => ( { {}, { validity }, ); - } else if (sig.valid && sig.deviceTrust.isVerified()) { + } else if (sig.valid && sig.deviceTrust?.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + "verified session ", @@ -361,7 +361,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { {}, { validity, verify, device }, ); - } else if (!sig.valid && sig.deviceTrust.isVerified()) { + } else if (!sig.valid && sig.deviceTrust?.isVerified()) { sigStatus = _t( "Backup has an invalid signature from " + "verified session ", diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 498aaf03317..f505ebb76f9 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -67,7 +67,7 @@ export default class LabsUserSettingsTab extends React.Component<{}> { const groups = new EnhancedMap(); this.labs.forEach((f) => { groups - .getOrCreate(SettingsStore.getLabGroup(f), []) + .getOrCreate(SettingsStore.getLabGroup(f)!, []) .push(); }); diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 4f9529466b0..458dcaeac49 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -68,14 +68,14 @@ const QuickSettingsButton: React.FC<{ {_t("All settings")} - {SettingsStore.getValue("developerMode") && ( + {SettingsStore.getValue("developerMode") && SdkContextClass.instance.roomViewStore.getRoomId() && ( { closeMenu(); Modal.createDialog( DevtoolsDialog, { - roomId: SdkContextClass.instance.roomViewStore.getRoomId(), + roomId: SdkContextClass.instance.roomViewStore.getRoomId()!, }, "mx_DevtoolsDialog_wrapper", ); diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index a2f8719add0..1d31e9d141f 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -122,7 +122,7 @@ export const SpaceButton = forwardRef( } let contextMenu: JSX.Element | undefined; - if (menuDisplayed && handle.current && ContextMenuComponent) { + if (space && menuDisplayed && handle.current && ContextMenuComponent) { contextMenu = ( { +function getDehydrationKey( + keyInfo: ISecretStorageKeyInfo, + checkFunc: (key: Uint8Array) => void, +): Promise { return Promise.resolve(null); } diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index 9adcb561623..da1bb62ba98 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -153,7 +153,7 @@ export class IntegrationManagers { if (kind === Kind.Account) { // Order by state_keys (IDs) - managers.sort((a, b) => compare(a.id, b.id)); + managers.sort((a, b) => compare(a.id ?? "", b.id ?? "")); } ordered.push(...managers); @@ -199,7 +199,7 @@ export class IntegrationManagers { logger.log("Looking up integration manager via .well-known"); if (domainName.startsWith("http:") || domainName.startsWith("https:")) { // trim off the scheme and just use the domain - domainName = url.parse(domainName).host; + domainName = url.parse(domainName).host!; } let wkConfig: IClientWellKnown; diff --git a/src/linkify-matrix.ts b/src/linkify-matrix.ts index 3369a18157b..6f921469c76 100644 --- a/src/linkify-matrix.ts +++ b/src/linkify-matrix.ts @@ -16,7 +16,7 @@ limitations under the License. */ import * as linkifyjs from "linkifyjs"; -import { Opts, registerCustomProtocol, registerPlugin } from "linkifyjs"; +import { EventListeners, Opts, registerCustomProtocol, registerPlugin } from "linkifyjs"; import linkifyElement from "linkify-element"; import linkifyString from "linkify-string"; import { User } from "matrix-js-sdk/src/matrix"; @@ -136,7 +136,7 @@ export const ELEMENT_URL_PATTERN = ")(#.*)"; export const options: Opts = { - events: function (href: string, type: string): Partial { + events: function (href: string, type: string): EventListeners { switch (type as Type) { case Type.URL: { // intercept local permalinks to users and show them like userids (in userinfo of current room) @@ -185,9 +185,11 @@ export const options: Opts = { }, }; } + + return {}; }, - formatHref: function (href: string, type: Type | string): string { + formatHref: function (href: string, type: Type | string): string | null { switch (type) { case Type.RoomAlias: case Type.UserId: @@ -205,7 +207,7 @@ export const options: Opts = { className: "linkified", - target: function (href: string, type: Type | string): string { + target: function (href: string, type: Type | string): string | null { if (type === Type.URL) { try { const transformed = tryTransformPermalinkToLocalHref(href); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index dd87a97a57e..7713e3f005a 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -228,6 +228,7 @@ export class RoomViewStore extends EventEmitter { } private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void { + if (!this.stores.client) return; doMaybeSetCurrentVoiceBroadcastPlayback( room, this.stores.client, @@ -532,8 +533,8 @@ export class RoomViewStore extends EventEmitter { const cli = MatrixClientPeg.get(); // take a copy of roomAlias & roomId as they may change by the time the join is complete - const { roomAlias, roomId } = this.state; - const address = roomAlias || roomId; + const { roomAlias, roomId = payload.roomId } = this.state; + const address = roomAlias || roomId!; const viaServers = this.state.viaServers || []; try { await retry( @@ -554,7 +555,7 @@ export class RoomViewStore extends EventEmitter { // room. this.dis.dispatch({ action: Action.JoinRoomReady, - roomId, + roomId: roomId!, metricsTrigger: payload.metricsTrigger, }); } catch (err) { diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts index 6dfb7d05610..c62974378f1 100644 --- a/src/stores/ThreepidInviteStore.ts +++ b/src/stores/ThreepidInviteStore.ts @@ -83,7 +83,11 @@ export default class ThreepidInviteStore extends EventEmitter { for (let i = 0; i < localStorage.length; i++) { const keyName = localStorage.key(i); if (!keyName?.startsWith(STORAGE_PREFIX)) continue; - results.push(JSON.parse(localStorage.getItem(keyName)) as IPersistedThreepidInvite); + try { + results.push(JSON.parse(localStorage.getItem(keyName)!) as IPersistedThreepidInvite); + } catch (e) { + console.warn("Failed to parse 3pid invite", e); + } } return results; } diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index 4251826f81c..73ec56556d7 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -144,7 +144,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _activeSpace: SpaceKey = MetaSpace.Home; // set properly by onReady private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); - private spaceOrderLocalEchoMap = new Map(); + private spaceOrderLocalEchoMap = new Map(); // The following properties are set by onReady as they live in account_data private _allRoomsInHome = false; private _enabledMetaSpaces: MetaSpace[] = []; @@ -338,7 +338,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false): Promise { - return this.matrixClient.sendStateEvent( + return this.matrixClient!.sendStateEvent( space.roomId, EventType.SpaceChild, { @@ -359,7 +359,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return getChildOrder(ev.getContent().order, ev.getTs(), ev.getStateKey()!); }) .map((ev) => { - const history = this.matrixClient.getRoomUpgradeHistory( + const history = this.matrixClient!.getRoomUpgradeHistory( ev.getStateKey()!, true, this._msc3946ProcessDynamicPredecessor, @@ -463,7 +463,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { ): Set => { if (space === MetaSpace.Home && this.allRoomsInHome) { return new Set( - this.matrixClient.getVisibleRooms(this._msc3946ProcessDynamicPredecessor).map((r) => r.roomId), + this.matrixClient!.getVisibleRooms(this._msc3946ProcessDynamicPredecessor).map((r) => r.roomId), ); } @@ -612,8 +612,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.roomIdsBySpace.delete(MetaSpace.Home); } else { const rooms = new Set( - this.matrixClient - .getVisibleRooms(this._msc3946ProcessDynamicPredecessor) + this.matrixClient!.getVisibleRooms(this._msc3946ProcessDynamicPredecessor) .filter(this.showInHomeSpace) .map((r) => r.roomId), ); @@ -813,9 +812,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Expand room IDs to all known versions of the given rooms const expandedRoomIds = new Set( Array.from(roomIds).flatMap((roomId) => { - return this.matrixClient - .getRoomUpgradeHistory(roomId, true, this._msc3946ProcessDynamicPredecessor) - .map((r) => r.roomId); + return this.matrixClient!.getRoomUpgradeHistory( + roomId, + true, + this._msc3946ProcessDynamicPredecessor, + ).map((r) => r.roomId); }), ); @@ -1217,7 +1218,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Persist last viewed room from a space // we don't await setActiveSpace above as we only care about this.activeSpace being up to date // synchronously for the below code - everything else can and should be async. - window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id); + window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id ?? ""); break; } @@ -1294,10 +1295,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } case "Spaces.showPeopleInSpace": - // getSpaceFilteredUserIds will return the appropriate value - this.emit(payload.roomId); - if (!this.enabledMetaSpaces.some((s) => s === MetaSpace.Home || s === MetaSpace.People)) { - this.updateNotificationStates([payload.roomId]); + if (payload.roomId) { + // getSpaceFilteredUserIds will return the appropriate value + this.emit(payload.roomId); + if (!this.enabledMetaSpaces.some((s) => s === MetaSpace.Home || s === MetaSpace.People)) { + this.updateNotificationStates([payload.roomId]); + } } break; @@ -1353,7 +1356,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); } - private async setRootSpaceOrder(space: Room, order: string): Promise { + private async setRootSpaceOrder(space: Room, order?: string): Promise { this.spaceOrderLocalEchoMap.set(space.roomId, order); try { await this.matrixClient?.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); From 961b843662303442d1fde9375cba3af6350c64ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 09:30:32 +0100 Subject: [PATCH 049/105] Merge pull request from GHSA-xv83-x443-7rmw * Escape HTML for plaintext search results * Add tests --- src/HtmlUtils.tsx | 11 ++-- .../views/rooms/SearchResultTile.tsx | 2 +- test/HtmlUtils-test.tsx | 51 +++++++++++++++++-- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f925facaf05..71f3c9e079c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,6 +28,7 @@ import { decode } from "html-entities"; import { IContent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; import _Linkify from "linkify-react"; +import escapeHtml from "escape-html"; import { _linkifyElement, @@ -355,10 +356,10 @@ abstract class BaseHighlighter { public constructor(public highlightClass: string, public highlightLink?: string) {} /** - * apply the highlights to a section of text + * Apply the highlights to a section of text * * @param {string} safeSnippet The snippet of text to apply the highlights - * to. + * to. This input must be sanitised as it will be treated as HTML. * @param {string[]} safeHighlights A list of substrings to highlight, * sorted by descending length. * @@ -367,7 +368,7 @@ abstract class BaseHighlighter { */ public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] { let lastOffset = 0; - let offset; + let offset: number; let nodes: T[] = []; const safeHighlight = safeHighlights[0]; @@ -440,7 +441,7 @@ interface IOpts { } export interface IOptsReturnNode extends IOpts { - returnString: false | undefined; + returnString?: false | undefined; } export interface IOptsReturnString extends IOpts { @@ -574,7 +575,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op safeBody = formatEmojis(safeBody, true).join(""); } } else if (highlighter) { - safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join(""); + safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join(""); } } finally { delete sanitizeParams.textFilter; diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 437b13b899c..a4b369150ea 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -71,7 +71,7 @@ export default class SearchResultTile extends React.Component { for (let j = 0; j < timeline.length; j++) { const mxEv = timeline[j]; - let highlights; + let highlights: string[] | undefined; const contextual = !this.props.ourEventsIndexes.includes(j); if (!contextual) { highlights = this.props.searchHighlights; diff --git a/test/HtmlUtils-test.tsx b/test/HtmlUtils-test.tsx index 8baeea446aa..d63470e1222 100644 --- a/test/HtmlUtils-test.tsx +++ b/test/HtmlUtils-test.tsx @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { mocked } from "jest-mock"; import { render, screen } from "@testing-library/react"; +import { IContent } from "matrix-js-sdk/src/models/event"; -import { topicToHtml } from "../src/HtmlUtils"; +import { bodyToHtml, topicToHtml } from "../src/HtmlUtils"; import SettingsStore from "../src/settings/SettingsStore"; jest.mock("../src/settings/SettingsStore"); @@ -29,7 +30,7 @@ const enableHtmlTopicFeature = () => { }); }; -describe("HtmlUtils", () => { +describe("topicToHtml", () => { function getContent() { return screen.getByRole("contentinfo").children[0].innerHTML; } @@ -62,3 +63,47 @@ describe("HtmlUtils", () => { expect(getContent()).toEqual('pizza 🍕'); }); }); + +describe("bodyToHtml", () => { + function getHtml(content: IContent, highlights?: string[]): string { + return (bodyToHtml(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; + } + + it("should apply highlights to HTML messages", () => { + const html = getHtml( + { + body: "test **foo** bar", + msgtype: "m.text", + formatted_body: "test foo bar", + format: "org.matrix.custom.html", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo bar"`); + }); + + it("should apply highlights to plaintext messages", () => { + const html = getHtml( + { + body: "test foo bar", + msgtype: "m.text", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo bar"`); + }); + + it("should not respect HTML tags in plaintext message highlighting", () => { + const html = getHtml( + { + body: "test foo bar", + msgtype: "m.text", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo <b>bar"`); + }); +}); From a1a087f75576b64ea3c14c843dbb9902facc32c3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 09:31:54 +0100 Subject: [PATCH 050/105] Fix usages of ARIA tabpanel (#10628) * RovingTabIndex handle looping around start/end * Make TabbedView expose aria tabpanel/tablist/tab roles * Fix right panel being wrongly specified as aria tabs Not all right panels map to the top right header buttons so we cannot describe it as a tabpanel relation * tsc strict * Update snapshots * Fix ARIA AXE violation * Update tests --- .../get-openid-token.spec.ts | 2 +- cypress/e2e/integration-manager/kick.spec.ts | 2 +- src/accessibility/RovingTabIndex.tsx | 15 +++- src/components/structures/TabbedView.tsx | 46 ++++++++--- .../views/right_panel/HeaderButton.tsx | 3 +- .../views/right_panel/HeaderButtons.tsx | 6 +- .../__snapshots__/TabbedView-test.tsx.snap | 27 +++++-- .../RoomSettingsDialog-test.tsx.snap | 41 ++++++---- .../UserSettingsDialog-test.tsx.snap | 77 +++++++++++++------ 9 files changed, 153 insertions(+), 66 deletions(-) diff --git a/cypress/e2e/integration-manager/get-openid-token.spec.ts b/cypress/e2e/integration-manager/get-openid-token.spec.ts index c1026a57876..b2dcb9146ae 100644 --- a/cypress/e2e/integration-manager/get-openid-token.spec.ts +++ b/cypress/e2e/integration-manager/get-openid-token.spec.ts @@ -59,7 +59,7 @@ const INTEGRATION_MANAGER_HTML = ` `; function openIntegrationManager() { - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click(); } diff --git a/cypress/e2e/integration-manager/kick.spec.ts b/cypress/e2e/integration-manager/kick.spec.ts index 4b29be4b23f..7075c1c199f 100644 --- a/cypress/e2e/integration-manager/kick.spec.ts +++ b/cypress/e2e/integration-manager/kick.spec.ts @@ -62,7 +62,7 @@ const INTEGRATION_MANAGER_HTML = ` `; function openIntegrationManager() { - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click(); } diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 7b8cb7ede58..1963459835d 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -156,6 +156,7 @@ export const reducer: Reducer = (state: IState, action: IAction }; interface IProps { + handleLoop?: boolean; handleHomeEnd?: boolean; handleUpDown?: boolean; handleLeftRight?: boolean; @@ -167,6 +168,7 @@ export const findSiblingElement = ( refs: RefObject[], startIndex: number, backwards = false, + loop = false, ): RefObject | undefined => { if (backwards) { for (let i = startIndex; i < refs.length && i >= 0; i--) { @@ -174,12 +176,18 @@ export const findSiblingElement = ( return refs[i]; } } + if (loop) { + return findSiblingElement(refs.slice(startIndex + 1), refs.length - 1, true, false); + } } else { for (let i = startIndex; i < refs.length && i >= 0; i++) { if (refs[i].current?.offsetParent !== null) { return refs[i]; } } + if (loop) { + return findSiblingElement(refs.slice(0, startIndex), 0, false, false); + } } }; @@ -188,6 +196,7 @@ export const RovingTabIndexProvider: React.FC = ({ handleHomeEnd, handleUpDown, handleLeftRight, + handleLoop, onKeyDown, }) => { const [state, dispatch] = useReducer>(reducer, { @@ -252,7 +261,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef!); - focusRef = findSiblingElement(context.state.refs, idx + 1); + focusRef = findSiblingElement(context.state.refs, idx + 1, false, handleLoop); } } break; @@ -266,7 +275,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef!); - focusRef = findSiblingElement(context.state.refs, idx - 1, true); + focusRef = findSiblingElement(context.state.refs, idx - 1, true, handleLoop); } } break; @@ -289,7 +298,7 @@ export const RovingTabIndexProvider: React.FC = ({ }); } }, - [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight], + [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight, handleLoop], ); return ( diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 0d3d01041be..1efada71436 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -22,9 +22,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../languageHandler"; import AutoHideScrollbar from "./AutoHideScrollbar"; -import AccessibleButton from "../views/elements/AccessibleButton"; import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers"; import { NonEmptyArray } from "../../@types/common"; +import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; /** * Represents a tab for the TabbedView. @@ -98,9 +98,10 @@ export default class TabbedView extends React.Component { } private renderTabLabel(tab: Tab): JSX.Element { - let classes = "mx_TabbedView_tabLabel "; - - if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active"; + const isActive = this.state.activeTabId === tab.id; + const classes = classNames("mx_TabbedView_tabLabel", { + mx_TabbedView_tabLabel_active: isActive, + }); let tabIcon: JSX.Element | undefined; if (tab.icon) { @@ -108,24 +109,35 @@ export default class TabbedView extends React.Component { } const onClickHandler = (): void => this.setActiveTab(tab); + const id = this.getTabId(tab); const label = _t(tab.label); return ( - {tabIcon} - {label} - + + {label} + + ); } + private getTabId(tab: Tab): string { + return `mx_tabpanel_${tab.id}`; + } + private renderTabPanel(tab: Tab): React.ReactNode { + const id = this.getTabId(tab); return ( -
    +
    {tab.body}
    ); @@ -147,7 +159,23 @@ export default class TabbedView extends React.Component { return (
    {screenName && } -
    {labels}
    + + {({ onKeyDownHandler }) => ( +
    + {labels} +
    + )} +
    {panel}
    ); diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 6d6872bc1e9..03106face21 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -54,8 +54,7 @@ export default class HeaderButton extends React.Component { return ( extends React.Component - {this.renderButtons()} -
    - ); + return
    {this.renderButtons()}
    ; } } diff --git a/test/components/structures/__snapshots__/TabbedView-test.tsx.snap b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap index 77ead236a31..acd6a4bae64 100644 --- a/test/components/structures/__snapshots__/TabbedView-test.tsx.snap +++ b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap @@ -6,12 +6,16 @@ exports[` renders tabs 1`] = ` class="mx_TabbedView mx_TabbedView_tabsOnLeft" >
    renders tabs 1`] = ` /> General
    Labs
    Security
    Settings tabs renders default tabs correctly 1`] = ` NodeList [
    General
    ,
    Security & Privacy
    ,
    Roles & Permissions
    ,
    Notifications
    ,
    Poll history diff --git a/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap index fb887a304df..4c1e9b20f94 100644 --- a/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap @@ -3,9 +3,11 @@ exports[` renders tabs correctly 1`] = ` NodeList [
    General
    ,
    Appearance
    ,
    Notifications
    ,
    Preferences
    ,
    Keyboard
    ,
    Sidebar
    ,
    Security & Privacy
    ,
    Labs
    ,
    Help & About From d20b3f5e069c8af5275ac5c0b2d03a97d68e7ae6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 25 Apr 2023 10:16:30 +0100 Subject: [PATCH 051/105] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 466ea48542e..129759525c5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./lib/index.ts", + "main": "./src/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -220,6 +220,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } From e263e2d1522713b68fb38ba28e8757c0dbb88069 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 25 Apr 2023 10:17:34 +0100 Subject: [PATCH 052/105] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 129759525c5..40b6b0d7741 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "25.0.0-rc.1", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.3.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 5d56a187010..675e4d2db0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,10 +1567,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.5.0.tgz#38b69c4e29d243944c5712cca7b674a3432056e6" integrity sha512-uL5kf7MqC+GxsGJtimPVbFliyaFinohTHSzohz31JTysktHsjRR2SC+vV7sy2/dstTWVdG9EGOnohyPsB+oi3A== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.6": - version "0.1.0-alpha.6" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.6.tgz#c0bdb9ab0d30179b8ef744d1b4010b0ad0ab9c3a" - integrity sha512-7hMffzw7KijxDyyH/eUyTfrLeCQHuyU3kaPOKGhcl3DZ3vx7bCncqjGMGTnxNPoP23I6gosvKSbO+3wYOT24Xg== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.7": + version "0.1.0-alpha.7" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz#136375b84fd8a7e698f70fc969f668e541a61313" + integrity sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg== "@matrix-org/matrix-wysiwyg@^2.0.0": version "2.0.0" @@ -6418,13 +6418,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@25.0.0-rc.1: - version "25.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-25.0.0-rc.1.tgz#e777dd291a61c6034590880c7ab375fa85d9afc5" - integrity sha512-B1aE1pTWC1e0hPBnzfwhFYcBmkHqysF9ctWzc+gzizJSGz1y+Q3noOMYfOQ9GSHcs2B7C1sy1M5KNhDk85Wwfg== +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": + version "25.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3498f0668e30cd545cf0614e6e6e9203a9e359d" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.6" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.7" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 621fdf13138edc8613b7afb9d44429dc9c72cd53 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 25 Apr 2023 10:59:51 +0100 Subject: [PATCH 053/105] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 885cdbe8634..25873acd7f6 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "25.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.3.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index ae73d4da07e..675e4d2db0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6418,10 +6418,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@25.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "25.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-25.0.0.tgz#a46ecb62eb96d5e93e36bf3f318ba4b4a08d56bf" - integrity sha512-eI9v0JkIYrSfsjUVk6PeZrsR7c7Qh0tirsK2biMZ4jPQo9sx/iuBjlfAEPoVlwzQKTfakGryhit15FfPhucMnw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3498f0668e30cd545cf0614e6e6e9203a9e359d" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.7" From 4b1b9b17efcc90d7a5dff0b0769b434a1a5ae5af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 15:46:18 +0100 Subject: [PATCH 054/105] Update roles cypress is looking for (#10708) * Update roles cypress is looking for * Fix sliding sync test --- cypress/e2e/integration-manager/send_event.spec.ts | 2 +- cypress/e2e/lazy-loading/lazy-loading.spec.ts | 2 +- cypress/e2e/polls/pollHistory.spec.ts | 2 +- cypress/e2e/right-panel/file-panel.spec.ts | 2 +- cypress/e2e/sliding-sync/sliding-sync.ts | 4 +--- cypress/e2e/threads/threads.spec.ts | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/integration-manager/send_event.spec.ts b/cypress/e2e/integration-manager/send_event.spec.ts index a62188b95e4..d8a746b4237 100644 --- a/cypress/e2e/integration-manager/send_event.spec.ts +++ b/cypress/e2e/integration-manager/send_event.spec.ts @@ -67,7 +67,7 @@ const INTEGRATION_MANAGER_HTML = ` `; function openIntegrationManager() { - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); cy.get(".mx_RoomSummaryCard_appsGroup").within(() => { cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click(); }); diff --git a/cypress/e2e/lazy-loading/lazy-loading.spec.ts b/cypress/e2e/lazy-loading/lazy-loading.spec.ts index 1efc69e0323..6e53fc33da9 100644 --- a/cypress/e2e/lazy-loading/lazy-loading.spec.ts +++ b/cypress/e2e/lazy-loading/lazy-loading.spec.ts @@ -117,7 +117,7 @@ describe("Lazy Loading", () => { function openMemberlist(): void { cy.get(".mx_HeaderButtons").within(() => { - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); }); cy.get(".mx_RoomSummaryCard").within(() => { diff --git a/cypress/e2e/polls/pollHistory.spec.ts b/cypress/e2e/polls/pollHistory.spec.ts index 93eefc49d21..00938ab768b 100644 --- a/cypress/e2e/polls/pollHistory.spec.ts +++ b/cypress/e2e/polls/pollHistory.spec.ts @@ -75,7 +75,7 @@ describe("Poll history", () => { }; function openPollHistory(): void { - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); cy.get(".mx_RoomSummaryCard").within(() => { cy.findByRole("button", { name: "Poll history" }).click(); }); diff --git a/cypress/e2e/right-panel/file-panel.spec.ts b/cypress/e2e/right-panel/file-panel.spec.ts index 318167bb1ee..f2e0e0a013b 100644 --- a/cypress/e2e/right-panel/file-panel.spec.ts +++ b/cypress/e2e/right-panel/file-panel.spec.ts @@ -24,7 +24,7 @@ const NAME = "Alice"; const viewRoomSummaryByName = (name: string): Chainable> => { cy.viewRoomByName(name); - cy.findByRole("tab", { name: "Room info" }).click(); + cy.findByRole("button", { name: "Room info" }).click(); return checkRoomSummaryCard(name); }; diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index 6caa01a9903..b7eccb77c62 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -241,9 +241,7 @@ describe("Sliding Sync", () => { }); it("should update user settings promptly", () => { - cy.findByRole("button", { name: "User menu" }).click(); - cy.findByRole("menuitem", { name: "All settings" }).click(); - cy.findByRole("button", { name: "Preferences" }).click(); + cy.openUserSettings("Preferences"); cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format") .should("exist") .find(".mx_ToggleSwitch_on") diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 63546bd4174..465aeb9520c 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -295,7 +295,7 @@ describe("Threads", () => { cy.get(".mx_ThreadSummary_content").findByText("How are things?").should("exist"); }); - cy.findByRole("tab", { name: "Threads" }) + cy.findByRole("button", { name: "Threads" }) .should("have.class", "mx_RightPanel_headerButton_unread") // User asserts thread list unread indicator .click(); // User opens thread list From 86ea059de613ca3505175a276f44316a45f99be8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 16:42:59 +0100 Subject: [PATCH 055/105] Make cypress happy about spec files all containing tests (#10709) --- cypress.config.ts | 2 +- .../e2e/toasts/{analytics-toast.ts => analytics-toast.spec.ts} | 0 .../{user-onboarding-new.ts => user-onboarding-new.spec.ts} | 0 .../{user-onboarding-old.ts => user-onboarding-old.spec.ts} | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename cypress/e2e/toasts/{analytics-toast.ts => analytics-toast.spec.ts} (100%) rename cypress/e2e/user-onboarding/{user-onboarding-new.ts => user-onboarding-new.spec.ts} (100%) rename cypress/e2e/user-onboarding/{user-onboarding-old.ts => user-onboarding-old.spec.ts} (100%) diff --git a/cypress.config.ts b/cypress.config.ts index f9bc521bdd9..b57fe7f6c4a 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ return require("./cypress/plugins/index.ts").default(on, config); }, baseUrl: "http://localhost:8080", - specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}", + specPattern: "cypress/e2e/**/*.spec.{js,jsx,ts,tsx}", }, env: { // Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. diff --git a/cypress/e2e/toasts/analytics-toast.ts b/cypress/e2e/toasts/analytics-toast.spec.ts similarity index 100% rename from cypress/e2e/toasts/analytics-toast.ts rename to cypress/e2e/toasts/analytics-toast.spec.ts diff --git a/cypress/e2e/user-onboarding/user-onboarding-new.ts b/cypress/e2e/user-onboarding/user-onboarding-new.spec.ts similarity index 100% rename from cypress/e2e/user-onboarding/user-onboarding-new.ts rename to cypress/e2e/user-onboarding/user-onboarding-new.spec.ts diff --git a/cypress/e2e/user-onboarding/user-onboarding-old.ts b/cypress/e2e/user-onboarding/user-onboarding-old.spec.ts similarity index 100% rename from cypress/e2e/user-onboarding/user-onboarding-old.ts rename to cypress/e2e/user-onboarding/user-onboarding-old.spec.ts From 8783021e53c67634c48b42a6e340872c00395d19 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2023 17:10:46 +0100 Subject: [PATCH 056/105] Fix Clock being read as an absolute time rather than duration (#10706) * Fix Clock being read as an absolute time rather than duration * Round durations and update snapshots --- package.json | 1 + src/components/views/audio_messages/Clock.tsx | 23 +++++-- .../VoiceBroadcastPlaybackBody-test.tsx.snap | 60 +++++++++++-------- .../VoiceBroadcastRecordingPip-test.tsx.snap | 10 ++-- yarn.lock | 15 ++++- 5 files changed, 75 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 25873acd7f6..d7b7997d878 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", "posthog-js": "1.53.2", + "proposal-temporal": "^0.9.0", "qrcode": "1.5.1", "re-resizable": "^6.9.0", "react": "17.0.2", diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index 4aad6349038..19902e7e7a6 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -15,15 +15,17 @@ limitations under the License. */ import React, { HTMLProps } from "react"; +import { Temporal } from "proposal-temporal"; import { formatSeconds } from "../../../DateUtils"; interface Props extends Pick, "aria-live" | "role"> { seconds: number; - formatFn?: (seconds: number) => string; + formatFn: (seconds: number) => string; } /** + * Clock which represents time periods rather than absolute time. * Simply converts seconds using formatFn. * Defaulting to formatSeconds(). * Note that in this case hours will not be displayed, making it possible to see "82:29". @@ -43,12 +45,23 @@ export default class Clock extends React.Component { return currentFloor !== nextFloor; } + private calculateDuration(seconds: number): string { + return new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds) + .round({ smallestUnit: "seconds", largestUnit: "hours" }) + .toString(); + } + public render(): React.ReactNode { + const { seconds, role } = this.props; return ( - - {/* formatFn set by defaultProps */} - {this.props.formatFn!(this.props.seconds)} - + ); } } diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap index c7d2d43bd2e..2850c944a25 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap @@ -112,16 +112,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
    - 00:00 - - + +
    @@ -223,16 +225,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a pause/not-live broadcast sh
    - 00:00 - - + +
    @@ -346,16 +350,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast in pip mo
    - 00:00 - - + +
    @@ -457,16 +463,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should re
    - 00:00 - - + +
    @@ -576,16 +584,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing/live broadcast shou
    - 00:00 - - + +
    @@ -667,16 +677,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should re
    - 00:00 - - + +
    diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap index 478a66e990f..043dd0bbc83 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap @@ -44,11 +44,12 @@ exports[`VoiceBroadcastRecordingPip when rendering a paused recording should ren
    - 4h 0m 0s left - +
    - 4h 0m 0s left - +
    Date: Wed, 26 Apr 2023 08:59:50 +1200 Subject: [PATCH 057/105] Unit test list ordering algorithms (#10682) * unit test ImportanceAlgorithm * strict fixes * unit test NaturalAlgorithm --- .../list-ordering/ImportanceAlgorithm-test.ts | 298 ++++++++++++++++++ .../list-ordering/NaturalAlgorithm-test.ts | 136 ++++++++ 2 files changed, 434 insertions(+) create mode 100644 test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts create mode 100644 test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts new file mode 100644 index 00000000000..6db3c369fb0 --- /dev/null +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -0,0 +1,298 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; +import { ImportanceAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm"; +import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models"; +import * as RoomNotifs from "../../../../../src/RoomNotifs"; +import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models"; +import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; +import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; + +describe("ImportanceAlgorithm", () => { + const userId = "@alice:server.org"; + const tagId = DefaultTagID.Favourite; + + const makeRoom = (id: string, name: string, order?: number): Room => { + const room = new Room(id, client, userId); + room.name = name; + const tagEvent = new MatrixEvent({ + type: "m.tag", + content: { + tags: { + [tagId]: { + order, + }, + }, + }, + }); + room.addTags(tagEvent); + return room; + }; + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + }); + const roomA = makeRoom("!aaa:server.org", "Alpha", 2); + const roomB = makeRoom("!bbb:server.org", "Bravo", 5); + const roomC = makeRoom("!ccc:server.org", "Charlie", 1); + const roomD = makeRoom("!ddd:server.org", "Delta", 4); + const roomE = makeRoom("!eee:server.org", "Echo", 3); + const roomX = makeRoom("!xxx:server.org", "Xylophone", 99); + + const unreadStates: Record> = { + red: { symbol: null, count: 1, color: NotificationColor.Red }, + grey: { symbol: null, count: 1, color: NotificationColor.Grey }, + none: { symbol: null, count: 0, color: NotificationColor.None }, + }; + + beforeEach(() => { + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + }); + + const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { + const algorithm = new ImportanceAlgorithm(tagId, sortAlgorithm); + algorithm.setRooms(rooms || [roomA, roomB, roomC]); + return algorithm; + }; + + describe("When sortAlgorithm is manual", () => { + const sortAlgorithm = SortAlgorithm.Manual; + it("orders rooms by tag order without categorizing", () => { + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState"); + const algorithm = setupAlgorithm(sortAlgorithm); + + // didn't check notif state + expect(RoomNotificationStateStore.instance.getRoomState).not.toHaveBeenCalled(); + // sorted according to room tag order + expect(algorithm.orderedRooms).toEqual([roomC, roomA, roomB]); + }); + + describe("handleRoomUpdate", () => { + // XXX: This doesn't work because manual ordered rooms dont get categoryindices + // possibly related https://github.com/vector-im/element-web/issues/25099 + it.skip("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB]); + }); + + // XXX: This doesn't work because manual ordered rooms dont get categoryindices + it.skip("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomD, roomE]); + }); + + it("does nothing and returns false for a timeline update", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const beforeRooms = algorithm.orderedRooms; + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(false); + // strict equal + expect(algorithm.orderedRooms).toBe(beforeRooms); + }); + + it("does nothing and returns false for a read receipt update", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const beforeRooms = algorithm.orderedRooms; + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.ReadReceipt); + + expect(shouldTriggerUpdate).toBe(false); + // strict equal + expect(algorithm.orderedRooms).toBe(beforeRooms); + }); + + it("throws for an unhandle update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + }); + }); + + describe("When sortAlgorithm is alphabetical", () => { + const sortAlgorithm = SortAlgorithm.Alphabetic; + + beforeEach(async () => { + // destroy roomMap so we can start fresh + // @ts-ignore private property + RoomNotificationStateStore.instance.roomMap = new Map(); + + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + jest.spyOn(RoomNotifs, "determineUnreadState") + .mockClear() + .mockImplementation((room) => { + switch (room) { + // b and e have red notifs + case roomB: + case roomE: + return unreadStates.red; + // c is grey + case roomC: + return unreadStates.grey; + default: + return unreadStates.none; + } + }); + }); + + it("orders rooms by alpha when they have the same notif state", () => { + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to alpha + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + it("orders rooms by notification state then alpha", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + + expect(algorithm.orderedRooms).toEqual([ + // alpha within red + roomB, + roomE, + // grey + roomC, + // alpha within none + roomA, + roomD, + ]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC]); + // no re-sorting on a remove + expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + + it("warns and returns without change when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + // inserted according to notif state + expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId); + }); + + it("throws for an unhandled update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + + describe("time and read receipt updates", () => { + it("throws for when a room is not indexed", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + + expect(() => algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline)).toThrow( + `Room ${roomX.roomId} has no index in ${tagId}`, + ); + }); + + it("re-sorts category when updated room has not changed category", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA, roomD]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId); + }); + + it("re-sorts category when updated room has changed category", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + // change roomE to unreadState.none + jest.spyOn(RoomNotifs, "determineUnreadState").mockImplementation((room) => { + switch (room) { + // b and e have red notifs + case roomB: + return unreadStates.red; + // c is grey + case roomC: + return unreadStates.grey; + case roomE: + default: + return unreadStates.none; + } + }); + // @ts-ignore don't bother mocking rest of emit properties + roomE.emit(RoomEvent.Timeline, new MatrixEvent({ type: "whatever", room_id: roomE.roomId })); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC, roomA, roomD, roomE]); + + // only sorted within roomE's new category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId); + }); + }); + }); + }); +}); diff --git a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts new file mode 100644 index 00000000000..21879586b38 --- /dev/null +++ b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts @@ -0,0 +1,136 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { NaturalAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm"; +import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models"; +import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models"; +import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; + +describe("NaturalAlgorithm", () => { + const userId = "@alice:server.org"; + const tagId = DefaultTagID.Favourite; + + const makeRoom = (id: string, name: string): Room => { + const room = new Room(id, client, userId); + room.name = name; + return room; + }; + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + }); + const roomA = makeRoom("!aaa:server.org", "Alpha"); + const roomB = makeRoom("!bbb:server.org", "Bravo"); + const roomC = makeRoom("!ccc:server.org", "Charlie"); + const roomD = makeRoom("!ddd:server.org", "Delta"); + const roomE = makeRoom("!eee:server.org", "Echo"); + const roomX = makeRoom("!xxx:server.org", "Xylophone"); + + const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { + const algorithm = new NaturalAlgorithm(tagId, sortAlgorithm); + algorithm.setRooms(rooms || [roomA, roomB, roomC]); + return algorithm; + }; + + describe("When sortAlgorithm is alphabetical", () => { + const sortAlgorithm = SortAlgorithm.Alphabetic; + + beforeEach(async () => { + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + }); + + it("orders rooms by alpha", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to alpha + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC]); + }); + + it("warns when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC, roomE]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith( + [roomA, roomB, roomC, roomE], + tagId, + ); + }); + + it("throws for an unhandled update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + + describe("time and read receipt updates", () => { + it("handles when a room is not indexed", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline); + + // for better or worse natural alg sets this to true + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + it("re-sorts rooms when timeline updates", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomB, roomC], tagId); + }); + }); + }); + }); +}); From 382d2787c2a9c884dbbeceb6f9a80a44d80edb85 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 26 Apr 2023 04:47:56 +0000 Subject: [PATCH 058/105] Remove obsolete style rules - mx_DialogDesignChanges_wrapper (#10550) The style rules have been obsolete since IA 1.1 feedback toast was removed by 5d89230271359e1f22ef0df42b3e565dca230653. Signed-off-by: Suguru Hirahara --- res/css/_common.pcss | 9 --------- 1 file changed, 9 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index d847381077c..79dd8218f26 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -638,15 +638,6 @@ legend { overflow-y: hidden; } -.mx_DialogDesignChanges_wrapper .mx_Dialog_fixedWidth { - max-width: 636px; /* match splash image width */ - - .mx_AccessibleButton_kind_link { - font-size: inherit; - padding: 0; - } -} - /* TODO: Review mx_GeneralButton usage to see if it can use a different class */ /* These classes were brought in from the old UserSettings and are included here to avoid */ /* breaking the app. */ From b32482bc4aa5d0548c441c33ccac15a237d3573b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 05:08:25 +0000 Subject: [PATCH 059/105] Update cypress-io/github-action digest to 59c3b9b (#10639) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Kerry --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index e10272b3adc..d753ec0312d 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -135,7 +135,7 @@ jobs: path: matrix-react-sdk - name: Run Cypress tests - uses: cypress-io/github-action@a29a621b736c9a8547ba0236cc9cee1e49725bd5 + uses: cypress-io/github-action@59c3b9b4a1a6e623c29806797d849845443487d1 with: working-directory: matrix-react-sdk # The built-in Electron runner seems to grind to a halt trying From 857e22f943cbac78bd514f387f09f22ad9258ae4 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 26 Apr 2023 09:57:33 +0200 Subject: [PATCH 060/105] Fix condition of logging possible room list corruption (#10685) * Revert condition of logging possible room list corruption * Fix loop condition --- .../room-list/algorithms/list-ordering/ImportanceAlgorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 7c6ae4a617f..e0dfb5adca3 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -287,7 +287,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } // Do a quick check to see if we've completely broken the index - for (let i = 1; i <= CATEGORY_ORDER.length; i++) { + for (let i = 1; i < CATEGORY_ORDER.length; i++) { const lastCat = CATEGORY_ORDER[i - 1]; const lastCatIndex = indices[lastCat]; const thisCat = CATEGORY_ORDER[i]; From e4610e467232b822a7c58a060c4e26d86c445e5c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 Apr 2023 09:03:35 +0100 Subject: [PATCH 061/105] Posthog properly handle Analytics ID changing from under us (#10702) * Posthog properly handle Analytics ID changing from under us * Update tests --- src/PosthogAnalytics.ts | 8 ++++++++ test/PosthogAnalytics-test.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 5c5805af938..2a7e24294ec 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -312,6 +312,14 @@ export class PosthogAnalytics { Object.assign({ id: analyticsID }, accountData), ); } + if (this.posthog.get_distinct_id() === analyticsID) { + // No point identifying again + return; + } + if (this.posthog.persistence.get_user_state() === "identified") { + // Analytics ID has changed, reset as Posthog will refuse to merge in this case + this.posthog.reset(); + } this.posthog.identify(analyticsID); } catch (e) { // The above could fail due to network requests, but not essential to starting the application, diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 61a46a54058..21d0d7cf0f4 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -33,6 +33,10 @@ const getFakePosthog = (): PostHog => identify: jest.fn(), reset: jest.fn(), register: jest.fn(), + get_distinct_id: jest.fn(), + persistence: { + get_user_state: jest.fn(), + }, } as unknown as PostHog); interface ITestEvent extends IPosthogEvent { From 6166dbb661b6749b949376a099ee355a52f8083f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 Apr 2023 10:36:00 +0100 Subject: [PATCH 062/105] Make existing and new issue URLs configurable (#10710) * Make existing and new issue URLs configurable * Apply a deep merge over sdk config to allow sane nested structures * Defaultize * Fix types * Iterate * Add FeedbackDialog snapshot test * Add SdkConfig snapshot tests * Iterate * Fix tests * Iterate types * Fix test --- src/@types/common.ts | 22 +++++ src/@types/global.d.ts | 3 +- src/IConfigOptions.ts | 5 ++ src/Modal.tsx | 9 +- src/SdkConfig.ts | 66 +++++++++++---- src/components/structures/LoggedInView.tsx | 4 +- .../views/dialogs/FeedbackDialog.tsx | 7 +- src/components/views/rooms/RoomHeader.tsx | 6 +- src/utils/device/clientInformation.ts | 3 +- src/utils/objects.ts | 9 ++ test/LegacyCallHandler-test.ts | 4 +- test/PosthogAnalytics-test.ts | 2 +- test/SdkConfig-test.ts | 12 +++ .../components/structures/auth/Login-test.tsx | 2 +- .../structures/auth/Registration-test.tsx | 2 +- .../views/auth/CountryDropdown-test.tsx | 2 +- .../views/dialogs/FeedbackDialog-test.tsx | 35 ++++++++ .../FeedbackDialog-test.tsx.snap | 82 +++++++++++++++++++ .../views/rooms/RoomHeader-test.tsx | 2 +- test/languageHandler-test.ts | 2 +- test/utils/device/clientInformation-test.ts | 5 +- .../utils/getChunkLength-test.ts | 26 +++--- .../utils/getMaxBroadcastLength-test.ts | 25 ++---- 23 files changed, 258 insertions(+), 77 deletions(-) create mode 100644 test/components/views/dialogs/FeedbackDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/FeedbackDialog-test.tsx.snap diff --git a/src/@types/common.ts b/src/@types/common.ts index 3281ad68722..e6e69ab1ec7 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -54,3 +54,25 @@ export type KeysStartingWith = { }[keyof Input]; export type NonEmptyArray = [T, ...T[]]; + +export type Defaultize = P extends any + ? string extends keyof P + ? P + : Pick> & + Partial>> & + Partial>> + : never; + +export type DeepReadonly = T extends (infer R)[] + ? DeepReadonlyArray + : T extends Function + ? T + : T extends object + ? DeepReadonlyObject + : T; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in keyof T]: DeepReadonly; +}; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 587dc99dc7e..d8f01cd4be5 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,6 +49,7 @@ import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import AutoRageshakeStore from "../stores/AutoRageshakeStore"; import { IConfigOptions } from "../IConfigOptions"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; +import { DeepReadonly } from "./common"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -59,7 +60,7 @@ declare global { Olm: { init: () => Promise; }; - mxReactSdkConfig: IConfigOptions; + mxReactSdkConfig: DeepReadonly; // Needed for Safari, unknown to TypeScript webkitAudioContext: typeof AudioContext; diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index b2e44f23ce2..c6913d2eb25 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -186,6 +186,11 @@ export interface IConfigOptions { description: string; show_once?: boolean; }; + + feedback: { + existing_issues_url: string; + new_issue_url: string; + }; } export interface ISsoRedirectOptions { diff --git a/src/Modal.tsx b/src/Modal.tsx index c92741cfc6d..e8c514b8012 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -23,6 +23,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter" import dis from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; +import { Defaultize } from "./@types/common"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -32,14 +33,6 @@ export type ComponentType = React.ComponentType<{ onFinished?(...args: any): void; }>; -type Defaultize = P extends any - ? string extends keyof P - ? P - : Pick> & - Partial>> & - Partial>> - : never; - // Generic type which returns the props of the Modal component with the onFinished being optional. export type ComponentProps = Defaultize< Omit, "onFinished">, diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 70032cdabb8..be94bb5b08d 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -16,12 +16,15 @@ limitations under the License. */ import { Optional } from "matrix-events-sdk"; +import { mergeWith } from "lodash"; import { SnakedObject } from "./utils/SnakedObject"; import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions"; +import { isObject, objectClone } from "./utils/objects"; +import { DeepReadonly, Defaultize } from "./@types/common"; // see element-web config.md for docs, or the IConfigOptions interface for dev docs -export const DEFAULTS: IConfigOptions = { +export const DEFAULTS: DeepReadonly = { brand: "Element", integrations_ui_url: "https://scalar.vector.im/", integrations_rest_url: "https://scalar.vector.im/api", @@ -50,13 +53,43 @@ export const DEFAULTS: IConfigOptions = { chunk_length: 2 * 60, // two minutes max_length: 4 * 60 * 60, // four hours }, + + feedback: { + existing_issues_url: + "https://github.com/vector-im/element-web/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc", + new_issue_url: "https://github.com/vector-im/element-web/issues/new/choose", + }, }; +export type ConfigOptions = Defaultize; + +function mergeConfig( + config: DeepReadonly, + changes: DeepReadonly>, +): DeepReadonly { + // return { ...config, ...changes }; + return mergeWith(objectClone(config), changes, (objValue, srcValue) => { + // Don't merge arrays, prefer values from newer object + if (Array.isArray(objValue)) { + return srcValue; + } + + // Don't allow objects to get nulled out, this will break our types + if (isObject(objValue) && !isObject(srcValue)) { + return objValue; + } + }); +} + +type ObjectType = IConfigOptions[K] extends object + ? SnakedObject> + : Optional>>; + export default class SdkConfig { - private static instance: IConfigOptions; - private static fallback: SnakedObject; + private static instance: DeepReadonly; + private static fallback: SnakedObject>; - private static setInstance(i: IConfigOptions): void { + private static setInstance(i: DeepReadonly): void { SdkConfig.instance = i; SdkConfig.fallback = new SnakedObject(i); @@ -69,7 +102,7 @@ export default class SdkConfig { public static get( key?: K, altCaseName?: string, - ): IConfigOptions | IConfigOptions[K] { + ): DeepReadonly | DeepReadonly[K] { if (key === undefined) { // safe to cast as a fallback - we want to break the runtime contract in this case return SdkConfig.instance || {}; @@ -77,32 +110,29 @@ export default class SdkConfig { return SdkConfig.fallback.get(key, altCaseName); } - public static getObject( - key: K, - altCaseName?: string, - ): Optional>> { + public static getObject(key: K, altCaseName?: string): ObjectType { const val = SdkConfig.get(key, altCaseName); - if (val !== null && val !== undefined) { + if (isObject(val)) { return new SnakedObject(val); } // return the same type for sensitive callers (some want `undefined` specifically) - return val === undefined ? undefined : null; + return (val === undefined ? undefined : null) as ObjectType; } - public static put(cfg: Partial): void { - SdkConfig.setInstance({ ...DEFAULTS, ...cfg }); + public static put(cfg: DeepReadonly): void { + SdkConfig.setInstance(mergeConfig(DEFAULTS, cfg)); } /** - * Resets the config to be completely empty. + * Resets the config. */ - public static unset(): void { - SdkConfig.setInstance({}); // safe to cast - defaults will be applied + public static reset(): void { + SdkConfig.setInstance(mergeConfig(DEFAULTS, {})); // safe to cast - defaults will be applied } - public static add(cfg: Partial): void { - SdkConfig.put({ ...SdkConfig.get(), ...cfg }); + public static add(cfg: Partial): void { + SdkConfig.put(mergeConfig(SdkConfig.get(), cfg)); } } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9c72b269f1c..afc35508ac4 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -66,11 +66,11 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { TimelineRenderingType } from "../../contexts/RoomContext"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload"; -import { IConfigOptions } from "../../IConfigOptions"; import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning"; import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"; import { PipContainer } from "./PipContainer"; import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushRules"; +import { ConfigOptions } from "../../SdkConfig"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -98,7 +98,7 @@ interface IProps { roomOobData?: IOOBData; currentRoomId: string; collapseLhs: boolean; - config: IConfigOptions; + config: ConfigOptions; currentUserId?: string; justRegistered?: boolean; roomJustCreatedOpts?: IOpts; diff --git a/src/components/views/dialogs/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index 7ee24e05a45..6b204d68e11 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -28,10 +28,6 @@ import { submitFeedback } from "../../../rageshake/submit-rageshake"; import { useStateToggle } from "../../../hooks/useStateToggle"; import StyledCheckbox from "../elements/StyledCheckbox"; -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 { feature?: string; onFinished(): void; @@ -117,6 +113,9 @@ const FeedbackDialog: React.FC = (props: IProps) => { ); } + const existingIssuesUrl = SdkConfig.getObject("feedback").get("existing_issues_url"); + const newIssueUrl = SdkConfig.getObject("feedback").get("new_issue_url"); + return ( = ({ room, busy, setBusy, behavi let menu: JSX.Element | null = null; if (menuOpen) { const buttonRect = buttonRef.current!.getBoundingClientRect(); - const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; + const brand = SdkConfig.get("element_call").brand; menu = ( @@ -250,7 +250,7 @@ const CallButtons: FC = ({ room }) => { const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, room]); const useElementCallExclusively = useMemo(() => { - return SdkConfig.get("element_call").use_exclusively ?? DEFAULTS.element_call.use_exclusively; + return SdkConfig.get("element_call").use_exclusively; }, []); const hasLegacyCall = useEventEmitterState( diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 86ab7faa2d4..4b7d8e60e0d 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -18,6 +18,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import BasePlatform from "../../BasePlatform"; import { IConfigOptions } from "../../IConfigOptions"; +import { DeepReadonly } from "../../@types/common"; export type DeviceClientInformation = { name?: string; @@ -49,7 +50,7 @@ export const getClientInformationEventType = (deviceId: string): string => `${cl */ export const recordClientInformation = async ( matrixClient: MatrixClient, - sdkConfig: IConfigOptions, + sdkConfig: DeepReadonly, platform?: BasePlatform, ): Promise => { const deviceId = matrixClient.getDeviceId()!; diff --git a/src/utils/objects.ts b/src/utils/objects.ts index c2496b4c7c4..f505b71a4c6 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -141,3 +141,12 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] { export function objectClone(obj: O): O { return JSON.parse(JSON.stringify(obj)); } + +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +export function isObject(item: any): item is object { + return item && typeof item === "object" && !Array.isArray(item); +} diff --git a/test/LegacyCallHandler-test.ts b/test/LegacyCallHandler-test.ts index aafbc1275c7..be902e54f83 100644 --- a/test/LegacyCallHandler-test.ts +++ b/test/LegacyCallHandler-test.ts @@ -305,7 +305,7 @@ describe("LegacyCallHandler", () => { MatrixClientPeg.unset(); document.body.removeChild(audioElement); - SdkConfig.unset(); + SdkConfig.reset(); }); it("should look up the correct user and start a call in the room when a phone number is dialled", async () => { @@ -516,7 +516,7 @@ describe("LegacyCallHandler without third party protocols", () => { MatrixClientPeg.unset(); document.body.removeChild(audioElement); - SdkConfig.unset(); + SdkConfig.reset(); }); it("should still start a native call", async () => { diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 21d0d7cf0f4..1ba7c01d533 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -77,7 +77,7 @@ describe("PosthogAnalytics", () => { Object.defineProperty(window, "crypto", { value: null, }); - SdkConfig.unset(); // we touch the config, so clean up + SdkConfig.reset(); // we touch the config, so clean up }); describe("Initialisation", () => { diff --git a/test/SdkConfig-test.ts b/test/SdkConfig-test.ts index a6ac58e9c59..aba0e9646af 100644 --- a/test/SdkConfig-test.ts +++ b/test/SdkConfig-test.ts @@ -30,6 +30,9 @@ describe("SdkConfig", () => { chunk_length: 42, max_length: 1337, }, + feedback: { + existing_issues_url: "https://existing", + } as any, }); }); @@ -37,7 +40,16 @@ describe("SdkConfig", () => { const customConfig = JSON.parse(JSON.stringify(DEFAULTS)); customConfig.voice_broadcast.chunk_length = 42; customConfig.voice_broadcast.max_length = 1337; + customConfig.feedback.existing_issues_url = "https://existing"; expect(SdkConfig.get()).toEqual(customConfig); }); + + it("should allow overriding individual fields of sub-objects", () => { + const feedback = SdkConfig.getObject("feedback"); + expect(feedback.get("existing_issues_url")).toMatchInlineSnapshot(`"https://existing"`); + expect(feedback.get("new_issue_url")).toMatchInlineSnapshot( + `"https://github.com/vector-im/element-web/issues/new/choose"`, + ); + }); }); }); diff --git a/test/components/structures/auth/Login-test.tsx b/test/components/structures/auth/Login-test.tsx index bf9e8d567ce..a84e88b17c1 100644 --- a/test/components/structures/auth/Login-test.tsx +++ b/test/components/structures/auth/Login-test.tsx @@ -61,7 +61,7 @@ describe("Login", function () { afterEach(function () { fetchMock.restore(); - SdkConfig.unset(); // we touch the config, so clean up + SdkConfig.reset(); // we touch the config, so clean up unmockPlatformPeg(); }); diff --git a/test/components/structures/auth/Registration-test.tsx b/test/components/structures/auth/Registration-test.tsx index 16b64bc393c..3f6f44db7ec 100644 --- a/test/components/structures/auth/Registration-test.tsx +++ b/test/components/structures/auth/Registration-test.tsx @@ -66,7 +66,7 @@ describe("Registration", function () { afterEach(function () { fetchMock.restore(); - SdkConfig.unset(); // we touch the config, so clean up + SdkConfig.reset(); // we touch the config, so clean up unmockPlatformPeg(); }); diff --git a/test/components/views/auth/CountryDropdown-test.tsx b/test/components/views/auth/CountryDropdown-test.tsx index a6beeda233d..95cd5abe75f 100644 --- a/test/components/views/auth/CountryDropdown-test.tsx +++ b/test/components/views/auth/CountryDropdown-test.tsx @@ -23,7 +23,7 @@ import SdkConfig from "../../../../src/SdkConfig"; describe("CountryDropdown", () => { describe("default_country_code", () => { afterEach(() => { - SdkConfig.unset(); + SdkConfig.reset(); }); it.each([ diff --git a/test/components/views/dialogs/FeedbackDialog-test.tsx b/test/components/views/dialogs/FeedbackDialog-test.tsx new file mode 100644 index 00000000000..73dadd00b57 --- /dev/null +++ b/test/components/views/dialogs/FeedbackDialog-test.tsx @@ -0,0 +1,35 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render } from "@testing-library/react"; + +import SdkConfig from "../../../../src/SdkConfig"; +import FeedbackDialog from "../../../../src/components/views/dialogs/FeedbackDialog"; + +describe("FeedbackDialog", () => { + it("should respect feedback config", () => { + SdkConfig.put({ + feedback: { + existing_issues_url: "http://existing?foo=bar", + new_issue_url: "https://new.issue.url?foo=bar", + }, + }); + + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/dialogs/__snapshots__/FeedbackDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/FeedbackDialog-test.tsx.snap new file mode 100644 index 00000000000..2682f5234cc --- /dev/null +++ b/test/components/views/dialogs/__snapshots__/FeedbackDialog-test.tsx.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FeedbackDialog should respect feedback config 1`] = ` + +
    + +
    + +`; diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 35cb5d200c9..c74d856a85f 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -120,7 +120,7 @@ describe("RoomHeader", () => { await Promise.all([CallStore.instance, WidgetStore.instance].map(resetAsyncStoreWithClient)); client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]); jest.restoreAllMocks(); - SdkConfig.put({}); + SdkConfig.reset(); }); const mockRoomType = (type: string) => { diff --git a/test/languageHandler-test.ts b/test/languageHandler-test.ts index 556c12fe05d..0f22962831e 100644 --- a/test/languageHandler-test.ts +++ b/test/languageHandler-test.ts @@ -42,7 +42,7 @@ async function setupTranslationOverridesForTests(overrides: ICustomTranslations) describe("languageHandler", () => { afterEach(() => { - SdkConfig.unset(); + SdkConfig.reset(); CustomTranslationOptions.lookupFn = undefined; }); diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index 4133619f917..9b90d6cb104 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -20,6 +20,8 @@ import BasePlatform from "../../../src/BasePlatform"; import { IConfigOptions } from "../../../src/IConfigOptions"; import { getDeviceClientInformation, recordClientInformation } from "../../../src/utils/device/clientInformation"; import { getMockClientWithEventEmitter } from "../../test-utils"; +import { DEFAULTS } from "../../../src/SdkConfig"; +import { DeepReadonly } from "../../../src/@types/common"; describe("recordClientInformation()", () => { const deviceId = "my-device-id"; @@ -31,7 +33,8 @@ describe("recordClientInformation()", () => { setAccountData: jest.fn(), }); - const sdkConfig: IConfigOptions = { + const sdkConfig: DeepReadonly = { + ...DEFAULTS, brand: "Test Brand", element_call: { url: "", use_exclusively: false, brand: "Element Call" }, }; diff --git a/test/voice-broadcast/utils/getChunkLength-test.ts b/test/voice-broadcast/utils/getChunkLength-test.ts index a046a47f760..5610bd6caf1 100644 --- a/test/voice-broadcast/utils/getChunkLength-test.ts +++ b/test/voice-broadcast/utils/getChunkLength-test.ts @@ -14,24 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked } from "jest-mock"; - -import SdkConfig, { DEFAULTS } from "../../../src/SdkConfig"; +import SdkConfig from "../../../src/SdkConfig"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Features } from "../../../src/settings/Settings"; import SettingsStore from "../../../src/settings/SettingsStore"; import { getChunkLength } from "../../../src/voice-broadcast/utils/getChunkLength"; -jest.mock("../../../src/SdkConfig"); - describe("getChunkLength", () => { afterEach(() => { - jest.resetAllMocks(); + SdkConfig.reset(); }); describe("when there is a value provided by Sdk config", () => { beforeEach(() => { - mocked(SdkConfig.get).mockReturnValue({ chunk_length: 42 }); + SdkConfig.add({ + voice_broadcast: { + chunk_length: 42, + }, + }); }); it("should return this value", () => { @@ -41,9 +41,11 @@ describe("getChunkLength", () => { describe("when Sdk config does not provide a value", () => { beforeEach(() => { - DEFAULTS.voice_broadcast = { - chunk_length: 23, - }; + SdkConfig.add({ + voice_broadcast: { + chunk_length: 23, + }, + }); }); it("should return this value", () => { @@ -52,10 +54,6 @@ describe("getChunkLength", () => { }); describe("when there are no defaults", () => { - beforeEach(() => { - DEFAULTS.voice_broadcast = undefined; - }); - it("should return the fallback value", () => { expect(getChunkLength()).toBe(120); }); diff --git a/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts b/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts index f2dd1389548..3f40dd0efc2 100644 --- a/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts +++ b/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts @@ -14,21 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked } from "jest-mock"; - import SdkConfig, { DEFAULTS } from "../../../src/SdkConfig"; import { getMaxBroadcastLength } from "../../../src/voice-broadcast"; -jest.mock("../../../src/SdkConfig"); - describe("getMaxBroadcastLength", () => { afterEach(() => { - jest.resetAllMocks(); + SdkConfig.reset(); }); describe("when there is a value provided by Sdk config", () => { beforeEach(() => { - mocked(SdkConfig.get).mockReturnValue({ max_length: 42 }); + SdkConfig.put({ + voice_broadcast: { + max_length: 42, + }, + }); }); it("should return this value", () => { @@ -37,23 +37,14 @@ describe("getMaxBroadcastLength", () => { }); describe("when Sdk config does not provide a value", () => { - beforeEach(() => { - DEFAULTS.voice_broadcast = { - max_length: 23, - }; - }); - it("should return this value", () => { - expect(getMaxBroadcastLength()).toBe(23); + expect(getMaxBroadcastLength()).toBe(DEFAULTS.voice_broadcast!.max_length); }); }); describe("if there are no defaults", () => { - beforeEach(() => { - DEFAULTS.voice_broadcast = undefined; - }); - it("should return the fallback value", () => { + expect(DEFAULTS.voice_broadcast!.max_length).toBe(4 * 60 * 60); expect(getMaxBroadcastLength()).toBe(4 * 60 * 60); }); }); From 9970ee6973ae784930ea815a9028d78250568942 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 09:55:22 +0000 Subject: [PATCH 063/105] Update typescript-eslint monorepo to v5.59.0 (#10648) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 90 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe034d20548..12d8ea6a021 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2430,14 +2430,14 @@ integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== "@typescript-eslint/eslint-plugin@^5.35.1": - version "5.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz#b1d4b0ad20243269d020ef9bbb036a40b0849829" - integrity sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA== + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz#c0e10eeb936debe5d1c3433cf36206a95befefd0" + integrity sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.58.0" - "@typescript-eslint/type-utils" "5.58.0" - "@typescript-eslint/utils" "5.58.0" + "@typescript-eslint/scope-manager" "5.59.0" + "@typescript-eslint/type-utils" "5.59.0" + "@typescript-eslint/utils" "5.59.0" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2446,13 +2446,13 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.58.0.tgz#2ac4464cf48bef2e3234cb178ede5af352dddbc6" - integrity sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ== + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.0.tgz#0ad7cd019346cc5d150363f64869eca10ca9977c" + integrity sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w== dependencies: - "@typescript-eslint/scope-manager" "5.58.0" - "@typescript-eslint/types" "5.58.0" - "@typescript-eslint/typescript-estree" "5.58.0" + "@typescript-eslint/scope-manager" "5.59.0" + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/typescript-estree" "5.59.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.58.0": @@ -2463,13 +2463,21 @@ "@typescript-eslint/types" "5.58.0" "@typescript-eslint/visitor-keys" "5.58.0" -"@typescript-eslint/type-utils@5.58.0": - version "5.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz#f7d5b3971483d4015a470d8a9e5b8a7d10066e52" - integrity sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w== +"@typescript-eslint/scope-manager@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz#86501d7a17885710b6716a23be2e93fc54a4fe8c" + integrity sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ== dependencies: - "@typescript-eslint/typescript-estree" "5.58.0" - "@typescript-eslint/utils" "5.58.0" + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/visitor-keys" "5.59.0" + +"@typescript-eslint/type-utils@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz#8e8d1420fc2265989fa3a0d897bde37f3851e8c9" + integrity sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.0" + "@typescript-eslint/utils" "5.59.0" debug "^4.3.4" tsutils "^3.21.0" @@ -2478,6 +2486,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.58.0.tgz#54c490b8522c18986004df7674c644ffe2ed77d8" integrity sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g== +"@typescript-eslint/types@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" + integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== + "@typescript-eslint/typescript-estree@5.58.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz#4966e6ff57eaf6e0fce2586497edc097e2ab3e61" @@ -2491,7 +2504,34 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.58.0", "@typescript-eslint/utils@^5.10.0": +"@typescript-eslint/typescript-estree@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965" + integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg== + dependencies: + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/visitor-keys" "5.59.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5" + integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.0" + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/typescript-estree" "5.59.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/utils@^5.10.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.58.0.tgz#430d7c95f23ec457b05be5520c1700a0dfd559d5" integrity sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ== @@ -2513,6 +2553,14 @@ "@typescript-eslint/types" "5.58.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365" + integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA== + dependencies: + "@typescript-eslint/types" "5.59.0" + eslint-visitor-keys "^3.3.0" + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -7754,14 +7802,14 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.2, semver@^7.3.5, semver@^7.3.8: version "7.4.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318" integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw== dependencies: lru-cache "^6.0.0" -semver@^7.3.4: +semver@^7.3.4, semver@^7.3.7: version "7.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== From 5328f6e5fe4d5645ecd39e43c846f0076736ac88 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 26 Apr 2023 12:23:32 +0200 Subject: [PATCH 064/105] Element-R: Populate device list for right-panel (#10671) * Use `getUserDeviceInfo` instead of `downloadKeys` and `getStoredDevicesForUser` * Use new `getUserDeviceInfo` api in `UserInfo.tsx` and `UserInfo-test.tsx` * Fix missing fields * Use `getUserDeviceInfo` instead of `downloadKeys` * Move `ManualDeviceKeyVerificationDialog.tsx` from class to functional component and add tests * Fix strict errors * Update snapshot * Add snapshot test to `UserInfo-test.tsx` * Add test for * Remove useless TODO comment * Add test for ambiguous device * Rework `` test --- .../ManualDeviceKeyVerificationDialog.tsx | 120 ++++----- .../views/dialogs/UntrustedDeviceDialog.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 37 ++- src/verification.ts | 2 +- ...ManualDeviceKeyVerificationDialog-test.tsx | 113 +++++++++ ...lDeviceKeyVerificationDialog-test.tsx.snap | 231 +++++++++++++++++ .../views/right_panel/UserInfo-test.tsx | 100 +++++--- .../__snapshots__/UserInfo-test.tsx.snap | 233 ++++++++++++++++++ test/test-utils/test-utils.ts | 1 + 9 files changed, 739 insertions(+), 100 deletions(-) create mode 100644 test/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap create mode 100644 test/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx index e3bc1dc4690..8f1da8a2530 100644 --- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx +++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx @@ -18,72 +18,80 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import React, { useCallback } from "react"; +import { Device } from "matrix-js-sdk/src/models/device"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import * as FormattingUtils from "../../../utils/FormattingUtils"; import { _t } from "../../../languageHandler"; import QuestionDialog from "./QuestionDialog"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -interface IProps { +interface IManualDeviceKeyVerificationDialogProps { userId: string; - device: DeviceInfo; - onFinished(confirm?: boolean): void; + device: Device; + onFinished?(confirm?: boolean): void; } -export default class ManualDeviceKeyVerificationDialog extends React.Component { - private onLegacyFinished = (confirm: boolean): void => { - if (confirm) { - MatrixClientPeg.get().setDeviceVerified(this.props.userId, this.props.device.deviceId, true); - } - this.props.onFinished(confirm); - }; +export function ManualDeviceKeyVerificationDialog({ + userId, + device, + onFinished, +}: IManualDeviceKeyVerificationDialogProps): JSX.Element { + const mxClient = useMatrixClientContext(); - public render(): React.ReactNode { - let text; - if (MatrixClientPeg.get().getUserId() === this.props.userId) { - text = _t("Confirm by comparing the following with the User Settings in your other session:"); - } else { - text = _t("Confirm this user's session by comparing the following with their User Settings:"); - } + const onLegacyFinished = useCallback( + (confirm: boolean) => { + if (confirm && mxClient) { + mxClient.setDeviceVerified(userId, device.deviceId, true); + } + onFinished?.(confirm); + }, + [mxClient, userId, device, onFinished], + ); - const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint()); - const body = ( -
    -

    {text}

    -
    -
      -
    • - {this.props.device.getDisplayName()} -
    • -
    • - {" "} - - {this.props.device.deviceId} - -
    • -
    • - {" "} - - - {key} - - -
    • -
    -
    -

    {_t("If they don't match, the security of your communication may be compromised.")}

    + let text; + if (mxClient?.getUserId() === userId) { + text = _t("Confirm by comparing the following with the User Settings in your other session:"); + } else { + text = _t("Confirm this user's session by comparing the following with their User Settings:"); + } + + const fingerprint = device.getFingerprint(); + const key = fingerprint && FormattingUtils.formatCryptoKey(fingerprint); + const body = ( +
    +

    {text}

    +
    +
      +
    • + {device.displayName} +
    • +
    • + {" "} + + {device.deviceId} + +
    • +
    • + {" "} + + + {key} + + +
    • +
    - ); +

    {_t("If they don't match, the security of your communication may be compromised.")}

    +
    + ); - return ( - - ); - } + return ( + + ); } diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index 0d70e3b71f4..9fa6b075cde 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -59,7 +59,7 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =

    {newSessionText}

    - {device.getDisplayName()} ({device.deviceId}) + {device.displayName} ({device.deviceId})

    {askToVerifyText}

    diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index f6f90313191..b87c8ab76c1 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -30,7 +30,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; -import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { Device } from "matrix-js-sdk/src/models/device"; import dis from "../../../dispatcher/dispatcher"; import Modal from "../../../Modal"; @@ -81,14 +81,14 @@ import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-me import { SdkContextClass } from "../../../contexts/SDKContext"; import { asyncSome } from "../../../utils/arrays"; -export interface IDevice extends DeviceInfo { +export interface IDevice extends Device { ambiguous?: boolean; } export const disambiguateDevices = (devices: IDevice[]): void => { const names = Object.create(null); for (let i = 0; i < devices.length; i++) { - const name = devices[i].getDisplayName() ?? ""; + const name = devices[i].displayName ?? ""; const indexList = names[name] || []; indexList.push(i); names[name] = indexList; @@ -149,7 +149,8 @@ function useHasCrossSigningKeys( } setUpdating(true); try { - await cli.downloadKeys([member.userId]); + // We call it to populate the user keys and devices + await cli.getCrypto()?.getUserDeviceInfo([member.userId], true); const xsi = cli.getStoredCrossSigningForUser(member.userId); const key = xsi && xsi.getId(); return !!key; @@ -195,12 +196,10 @@ export function DeviceItem({ userId, device }: { userId: string; device: IDevice }; let deviceName; - if (!device.getDisplayName()?.trim()) { + if (!device.displayName?.trim()) { deviceName = device.deviceId; } else { - deviceName = device.ambiguous - ? device.getDisplayName() + " (" + device.deviceId + ")" - : device.getDisplayName(); + deviceName = device.ambiguous ? device.displayName + " (" + device.deviceId + ")" : device.displayName; } let trustedLabel: string | undefined; @@ -1190,6 +1189,19 @@ export const PowerLevelEditor: React.FC<{ ); }; +async function getUserDeviceInfo( + userId: string, + cli: MatrixClient, + downloadUncached = false, +): Promise { + const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], downloadUncached); + const devicesMap = userDeviceMap?.get(userId); + + if (!devicesMap) return; + + return Array.from(devicesMap.values()); +} + export const useDevices = (userId: string): IDevice[] | undefined | null => { const cli = useContext(MatrixClientContext); @@ -1203,10 +1215,9 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => { async function downloadDeviceList(): Promise { try { - await cli.downloadKeys([userId], true); - const devices = cli.getStoredDevicesForUser(userId); + const devices = await getUserDeviceInfo(userId, cli, true); - if (cancelled) { + if (cancelled || !devices) { // we got cancelled - presumably a different user now return; } @@ -1229,8 +1240,8 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => { useEffect(() => { let cancel = false; const updateDevices = async (): Promise => { - const newDevices = cli.getStoredDevicesForUser(userId); - if (cancel) return; + const newDevices = await getUserDeviceInfo(userId, cli); + if (cancel || !newDevices) return; setDevices(newDevices); }; const onDevicesUpdated = (users: string[]): void => { diff --git a/src/verification.ts b/src/verification.ts index c7cdd8073a8..83411d5650c 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -26,7 +26,7 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import { accessSecretStorage } from "./SecurityManager"; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; import { IDevice } from "./components/views/right_panel/UserInfo"; -import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; +import { ManualDeviceKeyVerificationDialog } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; import { findDMForUser } from "./utils/dm/findDMForUser"; diff --git a/test/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx b/test/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx new file mode 100644 index 00000000000..43912b2bc68 --- /dev/null +++ b/test/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Device } from "matrix-js-sdk/src/models/device"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import { stubClient } from "../../../test-utils"; +import { ManualDeviceKeyVerificationDialog } from "../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; + +describe("ManualDeviceKeyVerificationDialog", () => { + let mockClient: MatrixClient; + + function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) { + return render( + + + , + ); + } + + beforeEach(() => { + mockClient = stubClient(); + }); + + it("should display the device", () => { + // When + const deviceId = "XYZ"; + const device = new Device({ + userId: mockClient.getUserId()!, + deviceId, + displayName: "my device", + algorithms: [], + keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), + }); + const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn()); + + // Then + expect(container).toMatchSnapshot(); + }); + + it("should display the device of another user", () => { + // When + const userId = "@alice:example.com"; + const deviceId = "XYZ"; + const device = new Device({ + userId, + deviceId, + displayName: "my device", + algorithms: [], + keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), + }); + const { container } = renderDialog(userId, device, jest.fn()); + + // Then + expect(container).toMatchSnapshot(); + }); + + it("should call onFinished and matrixClient.setDeviceVerified", () => { + // When + const deviceId = "XYZ"; + const device = new Device({ + userId: mockClient.getUserId()!, + deviceId, + displayName: "my device", + algorithms: [], + keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), + }); + const onFinished = jest.fn(); + renderDialog(mockClient.getUserId()!, device, onFinished); + + screen.getByRole("button", { name: "Verify session" }).click(); + + // Then + expect(onFinished).toHaveBeenCalledWith(true); + expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true); + }); + + it("should call onFinished and not matrixClient.setDeviceVerified", () => { + // When + const deviceId = "XYZ"; + const device = new Device({ + userId: mockClient.getUserId()!, + deviceId, + displayName: "my device", + algorithms: [], + keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), + }); + const onFinished = jest.fn(); + renderDialog(mockClient.getUserId()!, device, onFinished); + + screen.getByRole("button", { name: "Cancel" }).click(); + + // Then + expect(onFinished).toHaveBeenCalledWith(false); + expect(mockClient.setDeviceVerified).not.toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap new file mode 100644 index 00000000000..f51e881e2a3 --- /dev/null +++ b/test/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManualDeviceKeyVerificationDialog should display the device 1`] = ` +
    +
    +