From 5fbc5af884c770c05215f72cf465901d8fbc310a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 4 Oct 2024 15:58:22 +0200 Subject: [PATCH 1/4] Add support for rendering media captions (#43) * Add support for rendering media captions * Run prettier * Deduplicate body props * Add basic test * Fix import order in test * Fix test? --- .../views/messages/MessageEvent.tsx | 57 +++++++++++------ src/components/views/messages/TextualBody.tsx | 11 ++++ src/utils/FileUtils.ts | 4 +- .../views/messages/MessageEvent-test.tsx | 63 ++++++++++++++++++- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 1a5d09e415..e4f3cb83ec 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -190,25 +190,42 @@ export default class MessageEvent extends React.Component implements IMe } } - return BodyType ? ( - - ) : null; + const hasCaption = + [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes(msgtype as MsgType) && + content.filename && + content.filename !== content.body; + const bodyProps: IBodyProps = { + ref: this.body, + mxEvent: this.props.mxEvent, + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + showUrlPreview: this.props.showUrlPreview, + forExport: this.props.forExport, + maxImageHeight: this.props.maxImageHeight, + replacingEventId: this.props.replacingEventId, + editState: this.props.editState, + onHeightChanged: this.props.onHeightChanged, + onMessageAllowed: this.onTileUpdate, + permalinkCreator: this.props.permalinkCreator, + mediaEventHelper: this.mediaHelper, + getRelationsForEvent: this.props.getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration, + inhibitInteraction: this.props.inhibitInteraction, + }; + if (hasCaption) { + return ; + } + + return BodyType ? : null; } } + +const CaptionBody: React.FunctionComponent }> = ({ + WrappedBodyType, + ...props +}) => ( +
+ + +
+); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 8b7bfb9a5a..890316fc02 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -556,6 +556,9 @@ export default class TextualBody extends React.Component { const content = mxEvent.getContent(); const isNotice = content.msgtype === MsgType.Notice; const isEmote = content.msgtype === MsgType.Emote; + const isCaption = [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes( + content.msgtype as MsgType, + ); const willHaveWrapper = this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote; @@ -635,6 +638,14 @@ export default class TextualBody extends React.Component { ); } + if (isCaption) { + return ( +
+ {body} + {widgets} +
+ ); + } return (
{body} diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 194cb31d20..37fd181900 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -38,7 +38,9 @@ export function presentableTextForFile( shortened = false, ): string { let text = fallbackText; - if (content.body?.length) { + if (content.filename?.length) { + text = content.filename; + } else if (content.body?.length) { // The content body should be the name of the file including a // file extension. text = content.body; diff --git a/test/components/views/messages/MessageEvent-test.tsx b/test/components/views/messages/MessageEvent-test.tsx index 16ed44f9a3..95481a0b12 100644 --- a/test/components/views/messages/MessageEvent-test.tsx +++ b/test/components/views/messages/MessageEvent-test.tsx @@ -8,7 +8,10 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, RenderResult } from "@testing-library/react"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, EventType, Room, MsgType } from "matrix-js-sdk/src/matrix"; +import fetchMock from "fetch-mock-jest"; +import fs from "fs"; +import path from "path"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; @@ -25,6 +28,26 @@ jest.mock("../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => VoiceBroadcastBody: () =>
, })); +jest.mock("../../../../src/components/views/messages/MImageBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/MImageReplyBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/MStickerBody", () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock("../../../../src/components/views/messages/TextualBody.tsx", () => ({ + __esModule: true, + default: () =>
, +})); + describe("MessageEvent", () => { let room: Room; let client: MatrixClient; @@ -68,4 +91,42 @@ describe("MessageEvent", () => { result.getByTestId("voice-broadcast-body"); }); }); + + describe("when an image with a caption is sent", () => { + let result: RenderResult; + + beforeEach(() => { + event = mkEvent({ + event: true, + type: EventType.RoomMessage, + user: client.getUserId()!, + room: room.roomId, + content: { + body: "caption for a test image", + format: "org.matrix.custom.html", + formatted_body: "caption for a test image", + msgtype: MsgType.Image, + filename: "image.webp", + info: { + w: 40, + h: 50, + }, + url: "mxc://server/image", + }, + }); + result = renderMessageEvent(); + }); + + it("should render a TextualBody and an ImageBody", () => { + fetchMock.getOnce( + "https://server/_matrix/media/v3/download/server/image", + { + body: fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "images", "animated-logo.webp")), + }, + { sendAsJson: false }, + ); + result.getByTestId("image-body"); + result.getByTestId("textual-body"); + }); + }); }); From a0cb9470cc4228610706488389159f387d056a6b Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 7 Oct 2024 15:56:34 +0100 Subject: [PATCH 2/4] Update to @vector-im/matrix-wysiwyg (#125) --- package.json | 2 +- .../DynamicImportWysiwygComposer.tsx | 4 ++-- .../components/FormattingButtons.tsx | 2 +- .../wysiwyg_composer/components/LinkModal.tsx | 2 +- .../components/WysiwygAutocomplete.tsx | 2 +- .../components/WysiwygComposer.tsx | 2 +- .../hooks/useInputEventProcessor.ts | 2 +- .../hooks/usePlainTextListeners.ts | 2 +- .../rooms/wysiwyg_composer/hooks/useSuggestion.ts | 2 +- .../views/rooms/wysiwyg_composer/hooks/utils.ts | 2 +- .../rooms/wysiwyg_composer/utils/autocomplete.ts | 2 +- .../wysiwyg_composer/utils/createMessageContent.ts | 2 +- .../components/FormattingButtons-test.tsx | 2 +- .../wysiwyg_composer/components/LinkModal-test.tsx | 2 +- yarn.lock | 14 +++++++------- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index f658bf04b6..49d0f1ffb3 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.26.0", "@matrix-org/emojibase-bindings": "^1.1.2", - "@matrix-org/matrix-wysiwyg": "2.37.9", + "@vector-im/matrix-wysiwyg": "2.37.13", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", diff --git a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx index 5eba213579..46210949f5 100644 --- a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx @@ -10,7 +10,7 @@ import React, { ComponentProps, lazy, Suspense } from "react"; import { ISendEventResponse } from "matrix-js-sdk/src/matrix"; // we need to import the types for TS, but do not import the sendMessage -// function to avoid importing from "@matrix-org/matrix-wysiwyg" +// function to avoid importing from "@vector-im/matrix-wysiwyg" import { SendMessageParams } from "./utils/message"; import { retry } from "../../../../utils/promise"; @@ -55,7 +55,7 @@ export const dynamicImportConversionFunctions = async (): Promise<{ */ plainToRich(plain: string, inMessageFormat: boolean): Promise; }> => { - const { richToPlain, plainToRich } = await retry(() => import("@matrix-org/matrix-wysiwyg"), RETRY_COUNT); + const { richToPlain, plainToRich } = await retry(() => import("@vector-im/matrix-wysiwyg"), RETRY_COUNT); return { richToPlain, plainToRich }; }; diff --git a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx index 619037a89f..f02f1a4c6b 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { MouseEventHandler, ReactNode } from "react"; -import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions, AllActionStates, ActionState } from "@vector-im/matrix-wysiwyg"; import classNames from "classnames"; import BoldIcon from "@vector-im/compound-design-tokens/assets/web/icons/bold"; import BulletedListIcon from "@vector-im/compound-design-tokens/assets/web/icons/list-bulleted"; diff --git a/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx b/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx index 3d77982497..3b5bfb7ad4 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/LinkModal.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions } from "@vector-im/matrix-wysiwyg"; import React, { ChangeEvent, useState } from "react"; import { _t } from "../../../../../languageHandler"; diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index a617e23db1..e7ff5f169a 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { ForwardedRef, forwardRef, FunctionComponent } from "react"; -import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions, MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { logger } from "matrix-js-sdk/src/logger"; import { useRoomContext } from "../../../../../contexts/RoomContext"; diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 34e8b18022..5b6361a58e 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { memo, MutableRefObject, ReactNode, useEffect, useMemo, useRef } from "react"; import { IEventRelation } from "matrix-js-sdk/src/matrix"; import { EMOTICON_TO_EMOJI } from "@matrix-org/emojibase-bindings"; -import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; +import { useWysiwyg, FormattingFunctions } from "@vector-im/matrix-wysiwyg"; import classNames from "classnames"; import Autocomplete from "../../Autocomplete"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 4a8ecee921..8eac63eb36 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Wysiwyg, WysiwygEvent } from "@matrix-org/matrix-wysiwyg"; +import { Wysiwyg, WysiwygEvent } from "@vector-im/matrix-wysiwyg"; import { useCallback } from "react"; import { IEventRelation, MatrixClient } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 89e0964147..742d24fe34 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { KeyboardEvent, RefObject, SyntheticEvent, useCallback, useRef, useState } from "react"; -import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; +import { AllowedMentionAttributes, MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { IEventRelation } from "matrix-js-sdk/src/matrix"; import { useSettingValue } from "../../../../../hooks/useSettings"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts index 9b728c1018..7fdd630f10 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { EMOTICON_TO_EMOJI } from "@matrix-org/emojibase-bindings"; -import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; +import { AllowedMentionAttributes, MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { SyntheticEvent, useState, SetStateAction } from "react"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 9210ef041f..39317ea88c 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { MutableRefObject, RefObject } from "react"; import { IEventRelation, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { WysiwygEvent } from "@matrix-org/matrix-wysiwyg"; +import { WysiwygEvent } from "@vector-im/matrix-wysiwyg"; import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { IRoomState } from "../../../../structures/RoomView"; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts b/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts index ef85fed055..d97b07adee 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; +import { AllowedMentionAttributes, MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { ICompletion } from "../../../../../autocomplete/Autocompleter"; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 43a4f38ff8..9787da9e3c 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg"; +import { richToPlain, plainToRich } from "@vector-im/matrix-wysiwyg"; import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; import { ReplacementEvent, RoomMessageEventContent, RoomMessageTextEventContent } from "matrix-js-sdk/src/types"; diff --git a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx index 67d98f7fc2..854069f7b0 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ActionState, ActionTypes, AllActionStates, FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; +import { ActionState, ActionTypes, AllActionStates, FormattingFunctions } from "@vector-im/matrix-wysiwyg"; import { FormattingButtons } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; import * as LinkModal from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/LinkModal"; diff --git a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx index b9b2904c41..6ee8256797 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions } from "@vector-im/matrix-wysiwyg"; import { render, screen, waitFor } from "@testing-library/react"; import React from "react"; import userEvent from "@testing-library/user-event"; diff --git a/yarn.lock b/yarn.lock index ee422b6f28..5c995a2538 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1911,13 +1911,6 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz#8d6abdb9ded8656cc9e2a7909913a34bf3fc9b3a" integrity sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA== -"@matrix-org/matrix-wysiwyg@2.37.9": - version "2.37.9" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.37.9.tgz#4d5667df3c74e11fd01c4b5be920caff72bf2f48" - integrity sha512-Jn2ug6ySX5es5/SV5BVgSBhVPS7GXggwZ/GGWmnJvhnp/IArit4kgUAsNRhQeKebuVvNphwQykCGwyBOn2PLBA== - dependencies: - eslint-plugin-unicorn "^54.0.0" - "@matrix-org/olm@3.2.15": version "3.2.15" resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec" @@ -3101,6 +3094,13 @@ ts-xor "^1.3.0" vaul "^0.7.0" +"@vector-im/matrix-wysiwyg@2.37.13": + version "2.37.13" + resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.37.13.tgz#7b64e7652a196e811c0470dcbff8d36e2229cd46" + integrity sha512-3ilnJBJSzeTfAQXNJJ29EAqa14exrkca3acTPssdoh3WhTKwXyQ0xU8sS50d6gqlAP/Nahw1y2k+PzajXo1SsQ== + dependencies: + eslint-plugin-unicorn "^54.0.0" + "@zxcvbn-ts/core@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@zxcvbn-ts/core/-/core-3.0.4.tgz#c5bde72235eb6c273cec78b672bb47c0d7045cad" From 31bd10e8871b5eae854de9f59c178cf4d19d4879 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 8 Oct 2024 10:13:44 +0200 Subject: [PATCH 3/4] Remove usage of deprecated `CryptoEvent`s (#126) * Remove usage of `CryptoEvent.DeviceVerificationChanged` * Remove usage of `CryptoEvent.KeySignatureUploadFailure` * Remove usage of `CryptoEvent.Warning` * Merge `CryptoEvent.DevicesUpdated` and `CryptoEvent.WillUpdateDevices` behaviour --- src/DeviceListener.ts | 19 +-- src/components/structures/MatrixChat.tsx | 17 --- src/components/structures/RoomView.tsx | 10 -- .../KeySignatureUploadFailedDialog.tsx | 117 ------------------ src/components/views/right_panel/UserInfo.tsx | 6 - src/components/views/rooms/EventTile.tsx | 8 -- src/components/views/rooms/MemberTile.tsx | 8 -- src/i18n/strings/en_EN.json | 12 -- 8 files changed, 1 insertion(+), 196 deletions(-) delete mode 100644 src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 813a734260..949bf3d9f4 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -85,9 +85,7 @@ export default class DeviceListener { public start(matrixClient: MatrixClient): void { this.running = true; this.client = matrixClient; - this.client.on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); - this.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); this.client.on(ClientEvent.AccountData, this.onAccountData); @@ -109,9 +107,7 @@ export default class DeviceListener { public stop(): void { this.running = false; if (this.client) { - this.client.removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); this.client.removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); - this.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); this.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); this.client.removeListener(ClientEvent.AccountData, this.onAccountData); @@ -170,7 +166,7 @@ export default class DeviceListener { return await getUserDeviceIds(cli, cli.getSafeUserId()); } - private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise => { + private onDevicesUpdated = async (users: string[], initialFetch?: boolean): Promise => { if (!this.client) return; // If we didn't know about *any* devices before (ie. it's fresh login), // then they are all pre-existing devices, so ignore this and set the @@ -180,19 +176,6 @@ export default class DeviceListener { const myUserId = this.client.getSafeUserId(); if (users.includes(myUserId)) await this.ensureDeviceIdsAtStartPopulated(); - // No need to do a recheck here: we just need to get a snapshot of our devices - // before we download any new ones. - }; - - private onDevicesUpdated = (users: string[]): void => { - if (!this.client) return; - if (!users.includes(this.client.getSafeUserId())) return; - this.recheck(); - }; - - private onDeviceVerificationChanged = (userId: string): void => { - if (!this.client) return; - if (userId !== this.client.getUserId()) return; this.recheck(); }; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index f3b75dfce8..7689017856 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -82,7 +82,6 @@ import Spinner from "../views/elements/Spinner"; import QuestionDialog from "../views/dialogs/QuestionDialog"; import UserSettingsDialog from "../views/dialogs/UserSettingsDialog"; import CreateRoomDialog from "../views/dialogs/CreateRoomDialog"; -import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog"; import IncomingSasDialog from "../views/dialogs/IncomingSasDialog"; import CompleteSecurity from "./auth/CompleteSecurity"; import Welcome from "../views/auth/Welcome"; @@ -1629,18 +1628,6 @@ export default class MatrixChat extends React.PureComponent { room.setBlacklistUnverifiedDevices(blacklistEnabled); } }); - cli.on(CryptoEvent.Warning, (type) => { - switch (type) { - case "CRYPTO_WARNING_OLD_VERSION_DETECTED": - Modal.createDialog(ErrorDialog, { - title: _t("encryption|old_version_detected_title"), - description: _t("encryption|old_version_detected_description", { - brand: SdkConfig.get().brand, - }), - }); - break; - } - }); cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => { let haveNewVersion: boolean | undefined; let newVersionInfo: KeyBackupInfo | null = null; @@ -1674,10 +1661,6 @@ export default class MatrixChat extends React.PureComponent { } }); - cli.on(CryptoEvent.KeySignatureUploadFailure, (failures, source, continuation) => { - Modal.createDialog(KeySignatureUploadFailedDialog, { failures, source, continuation }); - }); - cli.on(CryptoEvent.VerificationRequestReceived, (request) => { if (request.verifier) { Modal.createDialog( diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 958c93bebc..1b191f5d8e 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -428,7 +428,6 @@ export class RoomView extends React.Component { context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate); context.client.on(RoomEvent.MyMembership, this.onMyMembership); context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); - context.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); @@ -974,7 +973,6 @@ export class RoomView extends React.Component { this.context.client.removeListener(RoomEvent.MyMembership, this.onMyMembership); this.context.client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate); this.context.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); - this.context.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); this.context.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); this.context.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); this.context.client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); @@ -1438,14 +1436,6 @@ export class RoomView extends React.Component { ); }; - private onDeviceVerificationChanged = (userId: string): void => { - const room = this.state.room; - if (!room?.currentState.getMember(userId)) { - return; - } - this.updateE2EStatus(room); - }; - private onUserVerificationChanged = (userId: string): void => { const room = this.state.room; if (!room || !room.currentState.getMember(userId)) { diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx deleted file mode 100644 index 9684f25134..0000000000 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useState, useCallback, useRef } from "react"; - -import { _t } from "../../../languageHandler"; -import SdkConfig from "../../../SdkConfig"; -import BaseDialog from "./BaseDialog"; -import DialogButtons from "../elements/DialogButtons"; -import Spinner from "../elements/Spinner"; - -interface IProps { - failures: Record< - string, - Record< - string, - { - errcode: string; - error: string; - } - > - >; - source: string; - continuation: (opts: { shouldEmit: boolean }) => Promise; - onFinished(): void; -} - -const KeySignatureUploadFailedDialog: React.FC = ({ failures, source, continuation, onFinished }) => { - const RETRIES = 2; - const [retry, setRetry] = useState(RETRIES); - const [cancelled, setCancelled] = useState(false); - const [retrying, setRetrying] = useState(false); - const [success, setSuccess] = useState(false); - const onCancel = useRef(onFinished); - - const causes = new Map([ - ["_afterCrossSigningLocalKeyChange", _t("encryption|key_signature_upload_failed_master_key_signature")], - ["checkOwnCrossSigningTrust", _t("encryption|key_signature_upload_failed_cross_signing_key_signature")], - ["setDeviceVerification", _t("encryption|key_signature_upload_failed_device_cross_signing_key_signature")], - ]); - const defaultCause = _t("encryption|key_signature_upload_failed_key_signature"); - - const onRetry = useCallback(async (): Promise => { - try { - setRetrying(true); - const cancel = new Promise((resolve, reject) => { - onCancel.current = reject; - }).finally(() => { - setCancelled(true); - }); - await Promise.race([continuation({ shouldEmit: false }), cancel]); - setSuccess(true); - } catch (e) { - setRetry((r) => r - 1); - } finally { - onCancel.current = onFinished; - setRetrying(false); - } - }, [continuation, onFinished]); - - let body; - if (!success && !cancelled && retry > 0) { - const reason = causes.get(source) || defaultCause; - const brand = SdkConfig.get().brand; - - body = ( -
-

{_t("encryption|key_signature_upload_failed_body", { brand })}

-

{reason}

- {retrying && } -
{JSON.stringify(failures, null, 2)}
- -
- ); - } else { - let text = _t("encryption|key_signature_upload_completed"); - if (!success) { - text = cancelled - ? _t("encryption|key_signature_upload_cancelled") - : _t("encryption|key_signature_upload_failed"); - } - - body = ( -
- {text} - -
- ); - } - - return ( - {}} - > - {body} - - ); -}; - -export default KeySignatureUploadFailedDialog; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index dd4752b783..38c234318b 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1381,22 +1381,16 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => { if (!users.includes(userId)) return; updateDevices(); }; - const onDeviceVerificationChanged = (_userId: string, deviceId: string): void => { - if (_userId !== userId) return; - updateDevices(); - }; const onUserTrustStatusChanged = (_userId: string, trustLevel: UserVerificationStatus): void => { if (_userId !== userId) return; updateDevices(); }; cli.on(CryptoEvent.DevicesUpdated, onDevicesUpdated); - cli.on(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged); cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged); // Handle being unmounted return () => { cancel = true; cli.removeListener(CryptoEvent.DevicesUpdated, onDevicesUpdated); - cli.removeListener(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged); cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged); }; }, [cli, userId]); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a41dbc218f..81791bdcce 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -385,7 +385,6 @@ export class UnwrappedEventTile extends React.Component this.suppressReadReceiptAnimation = false; const client = MatrixClientPeg.safeGet(); if (!this.props.forExport) { - client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onReplaced); @@ -425,7 +424,6 @@ export class UnwrappedEventTile extends React.Component public componentWillUnmount(): void { const client = MatrixClientPeg.get(); if (client) { - client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); client.removeListener(RoomEvent.Receipt, this.onRoomReceipt); const room = client.getRoom(this.props.mxEvent.getRoomId()); @@ -564,12 +562,6 @@ export class UnwrappedEventTile extends React.Component this.forceUpdate(this.props.onHeightChanged); }; - private onDeviceVerificationChanged = (userId: string, device: string): void => { - if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(); - } - }; - private onUserVerificationChanged = (userId: string, _trustStatus: UserVerificationStatus): void => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(); diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx index d81a54494e..61b6c750fe 100644 --- a/src/components/views/rooms/MemberTile.tsx +++ b/src/components/views/rooms/MemberTile.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { RoomMember, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; -import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; @@ -62,7 +61,6 @@ export default class MemberTile extends React.Component { }); if (isRoomEncrypted) { cli.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); - cli.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); this.updateE2EStatus(); } else { // Listen for room to become encrypted @@ -77,7 +75,6 @@ export default class MemberTile extends React.Component { if (cli) { cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); - cli.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); } } @@ -100,11 +97,6 @@ export default class MemberTile extends React.Component { this.updateE2EStatus(); }; - private onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => { - if (userId !== this.props.member.userId) return; - this.updateE2EStatus(); - }; - private async updateE2EStatus(): Promise { const cli = MatrixClientPeg.safeGet(); const { userId } = this.props.member; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21619b99d7..8b43b71ac1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -904,16 +904,6 @@ "incompatible_database_disable": "Continue With Encryption Disabled", "incompatible_database_sign_out_description": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this", "incompatible_database_title": "Incompatible Database", - "key_signature_upload_cancelled": "Cancelled signature upload", - "key_signature_upload_completed": "Upload completed", - "key_signature_upload_failed": "Unable to upload", - "key_signature_upload_failed_body": "%(brand)s encountered an error during upload of:", - "key_signature_upload_failed_cross_signing_key_signature": "a new cross-signing key signature", - "key_signature_upload_failed_device_cross_signing_key_signature": "a device cross-signing signature", - "key_signature_upload_failed_key_signature": "a key signature", - "key_signature_upload_failed_master_key_signature": "a new master key signature", - "key_signature_upload_failed_title": "Signature upload failed", - "key_signature_upload_success_title": "Signature upload success", "messages_not_secure": { "cause_1": "Your homeserver", "cause_2": "The homeserver the user you're verifying is connected to", @@ -929,8 +919,6 @@ "warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." }, "not_supported": "", - "old_version_detected_description": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", - "old_version_detected_title": "Old cryptography data detected", "recovery_method_removed": { "description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.", "description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.", From 197168b3ba41aaf8e4524aa4099e7d94fca05e51 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Oct 2024 10:12:21 +0100 Subject: [PATCH 4/4] Refactor stores and their relationship to the MatrixClientPeg (#124) * Refactor stores and their relationship to the MatrixClientPeg to avoid import cycles and webpack weirdness Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/MatrixClientPeg.ts | 2 + src/stores/AsyncStoreWithClient.ts | 9 ++++- src/stores/AutoRageshakeStore.ts | 4 +- src/stores/BreadcrumbsStore.ts | 4 +- src/stores/CallStore.ts | 1 - src/stores/ModalWidgetStore.ts | 1 - src/stores/OwnBeaconStore.ts | 1 - src/stores/OwnProfileStore.ts | 1 - src/stores/ReadyWatchingStore.ts | 37 +++++++++++++------ src/stores/VoiceRecordingStore.ts | 1 - src/stores/WidgetStore.ts | 1 - src/stores/local-echo/EchoStore.ts | 1 - .../RoomNotificationStateStore.ts | 1 - src/stores/right-panel/RightPanelStore.ts | 1 - src/stores/room-list/MessagePreviewStore.ts | 6 +-- src/stores/room-list/RoomListLayoutStore.ts | 1 - src/stores/room-list/RoomListStore.ts | 2 - src/stores/spaces/SpaceStore.ts | 6 +-- src/stores/widgets/WidgetLayoutStore.ts | 1 - src/stores/widgets/WidgetMessagingStore.ts | 1 - test/stores/AutoRageshakeStore-test.ts | 2 +- test/stores/OwnProfileStore-test.ts | 6 +-- test/stores/SpaceStore-test.ts | 4 +- test/stores/VoiceRecordingStore-test.ts | 2 +- test/stores/WidgetLayoutStore-test.ts | 8 ++-- .../room-list/MessagePreviewStore-test.ts | 2 +- test/stores/room-list/RoomListStore-test.ts | 15 +++++--- .../room-list/SlidingRoomListStore-test.ts | 16 ++++---- 28 files changed, 71 insertions(+), 66 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 0ba2ec65d7..e5ca4464ba 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -43,6 +43,7 @@ import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { Features } from "./settings/Settings"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; +import { ReadyWatchingStore } from "./stores/ReadyWatchingStore.ts"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -309,6 +310,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { MatrixActionCreators.start(this.matrixClient); MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; MatrixClientBackedController.matrixClient = this.matrixClient; + ReadyWatchingStore.matrixClient = this.matrixClient; return opts; } diff --git a/src/stores/AsyncStoreWithClient.ts b/src/stores/AsyncStoreWithClient.ts index 7567eac9ac..d9facfc51a 100644 --- a/src/stores/AsyncStoreWithClient.ts +++ b/src/stores/AsyncStoreWithClient.ts @@ -36,8 +36,13 @@ export abstract class AsyncStoreWithClient extends AsyncStore< })(dispatcher); } - public async start(): Promise { - await this.readyStore.start(); + protected async start(matrixClient: MatrixClient | null): Promise { + await this.readyStore.start(matrixClient); + } + + // XXX: This method is intended only for use in tests. + public async useUnitTestClient(cli: MatrixClient): Promise { + await this.readyStore.useUnitTestClient(cli); } public get matrixClient(): MatrixClient | null { diff --git a/src/stores/AutoRageshakeStore.ts b/src/stores/AutoRageshakeStore.ts index 284c3e24a4..e83baf5a9c 100644 --- a/src/stores/AutoRageshakeStore.ts +++ b/src/stores/AutoRageshakeStore.ts @@ -46,9 +46,7 @@ interface IState { */ export default class AutoRageshakeStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { - const instance = new AutoRageshakeStore(); - instance.start(); - return instance; + return new AutoRageshakeStore(); })(); private constructor() { diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 9859f24015..546f1e63ae 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -30,9 +30,7 @@ interface IState { export class BreadcrumbsStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { - const instance = new BreadcrumbsStore(); - instance.start(); - return instance; + return new BreadcrumbsStore(); })(); private waitingRooms: { roomId: string; addedTs: number }[] = []; diff --git a/src/stores/CallStore.ts b/src/stores/CallStore.ts index 115a56aced..f0120ee6a6 100644 --- a/src/stores/CallStore.ts +++ b/src/stores/CallStore.ts @@ -31,7 +31,6 @@ export class CallStore extends AsyncStoreWithClient<{}> { public static get instance(): CallStore { if (!this._instance) { this._instance = new CallStore(); - this._instance.start(); } return this._instance; } diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts index 59437db403..aa4d5d1466 100644 --- a/src/stores/ModalWidgetStore.ts +++ b/src/stores/ModalWidgetStore.ts @@ -24,7 +24,6 @@ interface IState { export class ModalWidgetStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { const instance = new ModalWidgetStore(); - instance.start(); return instance; })(); private modalInstance: IHandle | null = null; diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index f60dae07fe..4240d7b7df 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -87,7 +87,6 @@ const getLocallyCreatedBeaconEventIds = (): string[] => { export class OwnBeaconStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { const instance = new OwnBeaconStore(); - instance.start(); return instance; })(); // users beacons, keyed by event type diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index be8c72aa23..f0b94df553 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -28,7 +28,6 @@ const KEY_AVATAR_URL = "mx_profile_avatar_url"; export class OwnProfileStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { const instance = new OwnProfileStore(); - instance.start(); return instance; })(); diff --git a/src/stores/ReadyWatchingStore.ts b/src/stores/ReadyWatchingStore.ts index a46a09899a..393b6fea11 100644 --- a/src/stores/ReadyWatchingStore.ts +++ b/src/stores/ReadyWatchingStore.ts @@ -9,27 +9,40 @@ import { MatrixClient, SyncState } from "matrix-js-sdk/src/matrix"; import { EventEmitter } from "events"; -import { MatrixClientPeg } from "../MatrixClientPeg"; import { ActionPayload } from "../dispatcher/payloads"; import { IDestroyable } from "../utils/IDestroyable"; import { Action } from "../dispatcher/actions"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable { - protected matrixClient: MatrixClient | null = null; + private static instances: ReadyWatchingStore[] = []; + protected _matrixClient: MatrixClient | null = null; private dispatcherRef: string | null = null; + public static set matrixClient(client: MatrixClient) { + for (const instance of ReadyWatchingStore.instances) { + instance.start(client); + } + } + public constructor(protected readonly dispatcher: MatrixDispatcher) { super(); + + this.dispatcherRef = this.dispatcher.register(this.onAction); } - public async start(): Promise { - this.dispatcherRef = this.dispatcher.register(this.onAction); + public get matrixClient(): MatrixClient | null { + return this._matrixClient; + } - // MatrixClientPeg can be undefined in tests because of circular dependencies with other stores - const matrixClient = MatrixClientPeg?.get(); + public async start(matrixClient: MatrixClient | null): Promise { + const oldClient = this._matrixClient; + this._matrixClient = matrixClient; + + if (oldClient !== matrixClient) { + await this.onNotReady(); + } if (matrixClient) { - this.matrixClient = matrixClient; await this.onReady(); } } @@ -38,8 +51,10 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro return this.matrixClient; // for external readonly access } - public useUnitTestClient(cli: MatrixClient): void { - this.matrixClient = cli; + // XXX: This method is intended only for use in tests. + public async useUnitTestClient(cli: MatrixClient): Promise { + this._matrixClient = cli; + await this.onReady(); } public destroy(): void { @@ -74,13 +89,13 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro if (this.matrixClient) { await this.onNotReady(); } - this.matrixClient = payload.matrixClient; + this._matrixClient = payload.matrixClient; await this.onReady(); } } else if (payload.action === "on_client_not_viable" || payload.action === Action.OnLoggedOut) { if (this.matrixClient) { await this.onNotReady(); - this.matrixClient = null; + this._matrixClient = null; } } }; diff --git a/src/stores/VoiceRecordingStore.ts b/src/stores/VoiceRecordingStore.ts index 7cbb8ac120..7a9e3601cf 100644 --- a/src/stores/VoiceRecordingStore.ts +++ b/src/stores/VoiceRecordingStore.ts @@ -30,7 +30,6 @@ export class VoiceRecordingStore extends AsyncStoreWithClient { public static get instance(): VoiceRecordingStore { if (!this.internalInstance) { this.internalInstance = new VoiceRecordingStore(); - this.internalInstance.start(); } return this.internalInstance; } diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index cfb92360a0..071cf8bde9 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -45,7 +45,6 @@ interface IRoomWidgets { export default class WidgetStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { const instance = new WidgetStore(); - instance.start(); return instance; })(); diff --git a/src/stores/local-echo/EchoStore.ts b/src/stores/local-echo/EchoStore.ts index 956eacb929..41c92941ea 100644 --- a/src/stores/local-echo/EchoStore.ts +++ b/src/stores/local-echo/EchoStore.ts @@ -38,7 +38,6 @@ export class EchoStore extends AsyncStoreWithClient { public static get instance(): EchoStore { if (!this._instance) { this._instance = new EchoStore(); - this._instance.start(); } return this._instance; } diff --git a/src/stores/notifications/RoomNotificationStateStore.ts b/src/stores/notifications/RoomNotificationStateStore.ts index 87fb276c10..ebe9129989 100644 --- a/src/stores/notifications/RoomNotificationStateStore.ts +++ b/src/stores/notifications/RoomNotificationStateStore.ts @@ -26,7 +26,6 @@ export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator"); export class RoomNotificationStateStore extends AsyncStoreWithClient { private static readonly internalInstance = (() => { const instance = new RoomNotificationStateStore(); - instance.start(); return instance; })(); private roomMap = new Map(); diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 43a36e91b8..4415852758 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -403,7 +403,6 @@ export default class RightPanelStore extends ReadyWatchingStore { public static get instance(): RightPanelStore { if (!this.internalInstance) { this.internalInstance = new RightPanelStore(); - this.internalInstance.start(); } return this.internalInstance; } diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index e0e06ec980..1462826fe4 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -124,11 +124,7 @@ const mkMessagePreview = (text: string, event: MatrixEvent): MessagePreview => { }; export class MessagePreviewStore extends AsyncStoreWithClient { - private static readonly internalInstance = (() => { - const instance = new MessagePreviewStore(); - instance.start(); - return instance; - })(); + private static readonly internalInstance = (() => new MessagePreviewStore())(); /** * @internal Public for test only diff --git a/src/stores/room-list/RoomListLayoutStore.ts b/src/stores/room-list/RoomListLayoutStore.ts index ea85860554..d305bacffb 100644 --- a/src/stores/room-list/RoomListLayoutStore.ts +++ b/src/stores/room-list/RoomListLayoutStore.ts @@ -28,7 +28,6 @@ export default class RoomListLayoutStore extends AsyncStoreWithClient { public static get instance(): RoomListLayoutStore { if (!this.internalInstance) { this.internalInstance = new RoomListLayoutStore(); - this.internalInstance.start(); } return RoomListLayoutStore.internalInstance; } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 53377e0a01..bb7cf15e8e 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -643,11 +643,9 @@ export default class RoomListStore { if (SettingsStore.getValue("feature_sliding_sync")) { logger.info("using SlidingRoomListStoreClass"); const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance); - instance.start(); RoomListStore.internalInstance = instance; } else { const instance = new RoomListStoreClass(defaultDispatcher); - instance.start(); RoomListStore.internalInstance = instance; } } diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index 90358f3310..1c3afeaf85 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -17,6 +17,7 @@ import { MatrixEvent, ClientEvent, ISendEventResponse, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -1397,7 +1398,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { export default class SpaceStore { private static readonly internalInstance = (() => { const instance = new SpaceStoreClass(); - instance.start(); return instance; })(); @@ -1408,9 +1408,9 @@ export default class SpaceStore { /** * @internal for test only */ - public static testInstance(): SpaceStoreClass { + public static testInstance(client: MatrixClient): SpaceStoreClass { const store = new SpaceStoreClass(); - store.start(); + store.useUnitTestClient(client); return store; } } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index cefbee0f6b..00b27a1aa1 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -60,7 +60,6 @@ export class WidgetLayoutStore extends ReadyWatchingStore { public static get instance(): WidgetLayoutStore { if (!this.internalInstance) { this.internalInstance = new WidgetLayoutStore(); - this.internalInstance.start(); } return this.internalInstance; } diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index 0388aeacb4..404049ab36 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -27,7 +27,6 @@ export enum WidgetMessagingStoreEvent { export class WidgetMessagingStore extends AsyncStoreWithClient<{}> { private static readonly internalInstance = (() => { const instance = new WidgetMessagingStore(); - instance.start(); return instance; })(); diff --git a/test/stores/AutoRageshakeStore-test.ts b/test/stores/AutoRageshakeStore-test.ts index f321e909a4..7ed317fc81 100644 --- a/test/stores/AutoRageshakeStore-test.ts +++ b/test/stores/AutoRageshakeStore-test.ts @@ -47,7 +47,7 @@ describe("AutoRageshakeStore", () => { // @ts-ignore bypass private ctor for tests autoRageshakeStore = new AutoRageshakeStore(); - autoRageshakeStore.start(); + autoRageshakeStore.useUnitTestClient(client); utdEvent = mkEvent({ event: true, diff --git a/test/stores/OwnProfileStore-test.ts b/test/stores/OwnProfileStore-test.ts index 0f4f2d325a..628706372d 100644 --- a/test/stores/OwnProfileStore-test.ts +++ b/test/stores/OwnProfileStore-test.ts @@ -40,7 +40,7 @@ describe("OwnProfileStore", () => { displayname: "Display Name", avatar_url: "mxc://example.com/abc123", }); - await ownProfileStore.start(); + await ownProfileStore.useUnitTestClient(client); expect(onUpdate).toHaveBeenCalled(); expect(ownProfileStore.displayName).toBe("Display Name"); @@ -54,7 +54,7 @@ describe("OwnProfileStore", () => { errcode: "M_NOT_FOUND", }), ); - await ownProfileStore.start(); + await ownProfileStore.useUnitTestClient(client); expect(onUpdate).toHaveBeenCalled(); expect(ownProfileStore.displayName).toBe(client.getSafeUserId()); @@ -69,7 +69,7 @@ describe("OwnProfileStore", () => { }), ); try { - await ownProfileStore.start(); + await ownProfileStore.useUnitTestClient(client); } catch (ignore) {} expect(onUpdate).not.toHaveBeenCalled(); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 35ec275c49..53172f946a 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -1428,7 +1428,7 @@ describe("SpaceStore", () => { it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { // Given a store - const store = SpaceStore.testInstance(); + const store = SpaceStore.testInstance(client); // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); @@ -1478,7 +1478,7 @@ describe("SpaceStore", () => { it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { // Given a store - const store = SpaceStore.testInstance(); + const store = SpaceStore.testInstance(client); // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); diff --git a/test/stores/VoiceRecordingStore-test.ts b/test/stores/VoiceRecordingStore-test.ts index 6974e6063e..9af96914e8 100644 --- a/test/stores/VoiceRecordingStore-test.ts +++ b/test/stores/VoiceRecordingStore-test.ts @@ -31,7 +31,7 @@ describe("VoiceRecordingStore", () => { const mkStore = (): VoiceRecordingStore => { const store = new VoiceRecordingStore(); - store.start(); + store.useUnitTestClient(stubClient); return store; }; diff --git a/test/stores/WidgetLayoutStore-test.ts b/test/stores/WidgetLayoutStore-test.ts index 72418dda5f..4ac397a121 100644 --- a/test/stores/WidgetLayoutStore-test.ts +++ b/test/stores/WidgetLayoutStore-test.ts @@ -167,7 +167,7 @@ describe("WidgetLayoutStore", () => { it("should recalculate all rooms when the client is ready", async () => { mocked(client.getVisibleRooms).mockReturnValue([mockRoom]); - await store.start(); + await store.start(client); expect(roomUpdateListener).toHaveBeenCalled(); expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]); @@ -243,7 +243,7 @@ describe("WidgetLayoutStore", () => { }); it("should copy the layout to the room", async () => { - await store.start(); + await store.start(client); store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); store.copyLayoutToRoom(mockRoom); @@ -297,7 +297,7 @@ describe("WidgetLayoutStore", () => { mocked(client.getVisibleRooms).mockReturnValue([]); // @ts-ignore bypass private ctor for tests const store = new WidgetLayoutStore(); - await store.start(); + await store.start(client); expect(client.getVisibleRooms).toHaveBeenCalledWith(false); }); }); @@ -314,7 +314,7 @@ describe("WidgetLayoutStore", () => { mocked(client.getVisibleRooms).mockReturnValue([]); // @ts-ignore bypass private ctor for tests const store = new WidgetLayoutStore(); - await store.start(); + await store.start(client); expect(client.getVisibleRooms).toHaveBeenCalledWith(true); }); }); diff --git a/test/stores/room-list/MessagePreviewStore-test.ts b/test/stores/room-list/MessagePreviewStore-test.ts index 976c822253..603f4713e9 100644 --- a/test/stores/room-list/MessagePreviewStore-test.ts +++ b/test/stores/room-list/MessagePreviewStore-test.ts @@ -80,7 +80,7 @@ describe("MessagePreviewStore", () => { mocked(client.getRoom).mockReturnValue(room); store = MessagePreviewStore.testInstance(); - await store.start(); + await store.useUnitTestClient(client); await setupAsyncStoreWithClient(store, client); }); diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index fd5562753e..4fe59a6222 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -29,6 +29,9 @@ import DMRoomMap from "../../../src/utils/DMRoomMap"; import { flushPromises, stubClient, upsertRoomStateEvents, mkRoom } from "../../test-utils"; import { DEFAULT_PUSH_RULES, makePushRule } from "../../test-utils/pushRules"; +// Mock out the SpaceWatcher as it messes with the prefilterConditions +jest.mock("../../../src/stores/room-list/SpaceWatcher.ts"); + describe("RoomListStore", () => { const client = stubClient(); const newRoomId = "!roomid:example.com"; @@ -91,6 +94,10 @@ describe("RoomListStore", () => { await (RoomListStore.instance as RoomListStoreClass).makeReady(client); }); + beforeEach(() => { + DMRoomMap.makeShared(client); + }); + it.each(OrderedDefaultTagIDs)("defaults to importance ordering for %s=", (tagId) => { expect(RoomListStore.instance.getTagSorting(tagId)).toBe(SortAlgorithm.Recent); }); @@ -102,11 +109,11 @@ describe("RoomListStore", () => { function createStore(): { store: RoomListStoreClass; handleRoomUpdate: jest.Mock } { const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher; const store = new RoomListStoreClass(fakeDispatcher); - // @ts-ignore accessing private member to set client - store.readyStore.matrixClient = client; const handleRoomUpdate = jest.fn(); // @ts-ignore accessing private member to mock it store.algorithm.handleRoomUpdate = handleRoomUpdate; + // @ts-ignore accessing private member to set client + store.readyStore.useUnitTestClient(client); return { store, handleRoomUpdate }; } @@ -157,7 +164,6 @@ describe("RoomListStore", () => { room1.updateMyMembership(KnownMembership.Join); room2.updateMyMembership(KnownMembership.Join); room3.updateMyMembership(KnownMembership.Join); - DMRoomMap.makeShared(client); const { store } = createStore(); client.getVisibleRooms = jest.fn().mockReturnValue([room1, room2, room3]); @@ -269,7 +275,6 @@ describe("RoomListStore", () => { it("Passes the feature flag on to the client when asking for visible rooms", () => { // Given a store that we can ask for a room list - DMRoomMap.makeShared(client); const { store } = createStore(); client.getVisibleRooms = jest.fn().mockReturnValue([]); @@ -285,7 +290,7 @@ describe("RoomListStore", () => { describe("room updates", () => { const makeStore = async () => { const store = new RoomListStoreClass(defaultDispatcher); - await store.start(); + await store.useUnitTestClient(client); return store; }; diff --git a/test/stores/room-list/SlidingRoomListStore-test.ts b/test/stores/room-list/SlidingRoomListStore-test.ts index f667ef7dca..926d1fd2f3 100644 --- a/test/stores/room-list/SlidingRoomListStore-test.ts +++ b/test/stores/room-list/SlidingRoomListStore-test.ts @@ -65,7 +65,7 @@ describe("SlidingRoomListStore", () => { describe("spaces", () => { it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => { - await store.start(); // call onReady + await store.useUnitTestClient(context.client!); // call onReady const spaceRoomId = "!foo:bar"; const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { @@ -92,7 +92,7 @@ describe("SlidingRoomListStore", () => { }, ); activeSpace = MetaSpace.Home; - await store.start(); // call onReady + await store.useUnitTestClient(context.client!); // call onReady expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { filters: expect.objectContaining({ @@ -108,7 +108,7 @@ describe("SlidingRoomListStore", () => { const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { return listName === DefaultTagID.Untagged && !isLoading; }); - await store.start(); // call onReady + await store.useUnitTestClient(context.client!); // call onReady await p; expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith( DefaultTagID.Untagged, @@ -121,7 +121,7 @@ describe("SlidingRoomListStore", () => { }); it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => { - await store.start(); // call onReady + await store.useUnitTestClient(context.client!); // call onReady const spaceRoomId = "!foo:bar"; const subSpace1 = "!ss1:bar"; const subSpace2 = "!ss2:bar"; @@ -168,7 +168,7 @@ describe("SlidingRoomListStore", () => { }); it("getTagsForRoom gets the tags for the room", async () => { - await store.start(); + await store.useUnitTestClient(context.client!); const roomA = "!a:localhost"; const roomB = "!b:localhost"; const keyToListData: Record }> = { @@ -200,7 +200,7 @@ describe("SlidingRoomListStore", () => { }); it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => { - await store.start(); + await store.useUnitTestClient(context.client!); const roomA = "!a:localhost"; const roomB = "!b:localhost"; const roomC = "!c:localhost"; @@ -236,7 +236,7 @@ describe("SlidingRoomListStore", () => { }); it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => { - await store.start(); + await store.useUnitTestClient(context.client!); // seed the store with 3 rooms const roomIdA = "!a:localhost"; const roomIdB = "!b:localhost"; @@ -301,7 +301,7 @@ describe("SlidingRoomListStore", () => { }); it("gracefully handles unknown room IDs", async () => { - await store.start(); + await store.useUnitTestClient(context.client!); const roomIdA = "!a:localhost"; const roomIdB = "!b:localhost"; // does not exist const roomIdC = "!c:localhost";