From e1b2e3a1011523d080d7ff2dea2a1d64be911e3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:03:09 +0000 Subject: [PATCH] Update react monorepo to v19 (major) (#28914) * Update react monorepo to v19 * Import JSX explicitly for React 19 compatibility Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update usages of refs for React 19 compatibility Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update react imports Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Avoid legacy contexts as much as possible Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Avoid deprecated React symbols Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Stash Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update usages of refs for React 19 compatibility Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch pillify to use a html-react-parser approach rather than DOM muddling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate react html parsing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate react html parsing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate html parsing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Memoize the EventContentBody component Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate html parsing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Discard changes to src/Linkify.tsx * Discard changes to src/components/views/messages/TextualBody.tsx * Discard changes to src/settings/handlers/AbstractLocalStorageSettingsHandler.ts * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Prepare for React 19 upgrade Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove stale comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 13 +-- patches/@types+react+18.3.18.patch | 76 --------------- patches/@types+react+19.0.10.patch | 31 ++++++ patches/react-blurhash+0.3.0.patch | 22 +++++ .../pills-click-in-app.spec.ts | 1 + src/@types/react.d.ts | 5 + src/HtmlUtils.tsx | 4 +- src/Lifecycle.ts | 4 +- src/accessibility/RovingTabIndex.tsx | 2 +- src/autocomplete/Autocompleter.ts | 4 +- .../structures/IndicatorScrollbar.tsx | 2 - src/components/structures/MatrixChat.tsx | 30 +++--- src/components/structures/TabbedView.tsx | 4 +- .../auth/header/AuthHeaderContext.tsx | 4 +- .../auth/header/AuthHeaderProvider.tsx | 2 +- src/components/views/avatars/MemberAvatar.tsx | 4 +- .../views/dialogs/ModuleUiDialog.tsx | 17 ++-- .../views/elements/AccessibleButton.tsx | 15 +-- .../views/room_settings/AliasSettings.tsx | 11 ++- .../tabs/user/PreferencesUserSettingsTab.tsx | 2 +- src/linkify-matrix.ts | 2 +- src/utils/ReactUtils.tsx | 17 ++-- src/widgets/ManagedHybrid.ts | 3 +- test/test-utils/client.ts | 2 +- .../__snapshots__/TextForEvent-test.ts.snap | 2 - .../autocomplete/EmojiProvider-test.ts | 14 ++- .../structures/AutocompleteInput-test.tsx | 8 +- .../components/structures/MatrixChat-test.tsx | 58 +++++------- .../structures/PipContainer-test.tsx | 7 +- .../__snapshots__/MessagePanel-test.tsx.snap | 4 +- .../views/messages/TextualBody-test.tsx | 2 +- .../__snapshots__/TextualBody-test.tsx.snap | 4 +- .../__snapshots__/PollHistory-test.tsx.snap | 4 +- .../views/right_panel/UserInfo-test.tsx | 2 +- .../__snapshots__/UserInfo-test.tsx.snap | 24 ++--- .../__snapshots__/RoomHeader-test.tsx.snap | 8 +- .../settings/AddPrivilegedUsers-test.tsx | 4 +- .../tabs/user/SessionManagerTab-test.tsx | 5 +- .../ThreadsActivityCentre-test.tsx.snap | 2 +- .../FormattingUtils-test.tsx.snap | 94 ++++++++++++------- test/unit-tests/utils/export-test.tsx | 5 +- .../__snapshots__/HTMLExport-test.ts.snap | 2 +- .../utils/permalinks/Permalinks-test.ts | 2 +- yarn.lock | 62 ++++++------ 44 files changed, 300 insertions(+), 290 deletions(-) delete mode 100644 patches/@types+react+18.3.18.patch create mode 100644 patches/@types+react+19.0.10.patch create mode 100644 patches/react-blurhash+0.3.0.patch diff --git a/package.json b/package.json index 1463cc9948..ab117e8e76 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,10 @@ "postinstall": "patch-package" }, "resolutions": { + "**/pretty-format/react-is": "19.0.0", "@playwright/test": "1.51.1", - "@types/react": "18.3.18", - "@types/react-dom": "18.3.5", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", "oidc-client-ts": "3.2.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001707", @@ -141,10 +142,10 @@ "posthog-js": "1.157.2", "qrcode": "1.5.4", "re-resizable": "6.11.2", - "react": "^18.3.1", + "react": "^19.0.0", "react-beautiful-dnd": "^13.1.0", "react-blurhash": "^0.3.0", - "react-dom": "^18.3.1", + "react-dom": "^19.0.0", "react-focus-lock": "^2.5.1", "react-string-replace": "^1.1.1", "react-transition-group": "^4.4.1", @@ -211,9 +212,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "18.3.18", + "@types/react": "19.0.10", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "18.3.5", + "@types/react-dom": "19.0.4", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.15.0", "@types/semver": "^7.5.8", diff --git a/patches/@types+react+18.3.18.patch b/patches/@types+react+18.3.18.patch deleted file mode 100644 index 18a1e64524..0000000000 --- a/patches/@types+react+18.3.18.patch +++ /dev/null @@ -1,76 +0,0 @@ -diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts -index 6ea73ef..cb51757 100644 ---- a/node_modules/@types/react/index.d.ts -+++ b/node_modules/@types/react/index.d.ts -@@ -151,7 +151,7 @@ declare namespace React { - /** - * The current value of the ref. - */ -- readonly current: T | null; -+ current: T; - } - - interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES { -@@ -186,7 +186,7 @@ declare namespace React { - * @see {@link RefObject} - */ - -- type Ref = RefCallback | RefObject | null; -+ type Ref = RefCallback | RefObject | null; - /** - * A legacy implementation of refs where you can pass a string to a ref prop. - * -@@ -300,7 +300,7 @@ declare namespace React { - * - * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs} - */ -- ref?: LegacyRef | undefined; -+ ref?: LegacyRef | undefined; - } - - /** -@@ -1234,7 +1234,7 @@ declare namespace React { - * - * @see {@link ForwardRefRenderFunction} - */ -- type ForwardedRef = ((instance: T | null) => void) | MutableRefObject | null; -+ type ForwardedRef = ((instance: T | null) => void) | RefObject | null; - - /** - * The type of the function passed to {@link forwardRef}. This is considered different -@@ -1565,7 +1565,7 @@ declare namespace React { - [propertyName: string]: any; - } - -- function createRef(): RefObject; -+ function createRef(): RefObject; - - /** - * The type of the component returned from {@link forwardRef}. -@@ -1989,7 +1989,7 @@ declare namespace React { - * @version 16.8.0 - * @see {@link https://react.dev/reference/react/useRef} - */ -- function useRef(initialValue: T): MutableRefObject; -+ function useRef(initialValue: T): RefObject; - // convenience overload for refs given as a ref prop as they typically start with a null value - /** - * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument -@@ -2004,7 +2004,7 @@ declare namespace React { - * @version 16.8.0 - * @see {@link https://react.dev/reference/react/useRef} - */ -- function useRef(initialValue: T | null): RefObject; -+ function useRef(initialValue: T | null): RefObject; - // convenience overload for potentially undefined initialValue / call with 0 arguments - // has a default to stop it from defaulting to {} instead - /** -@@ -2017,7 +2017,7 @@ declare namespace React { - * @version 16.8.0 - * @see {@link https://react.dev/reference/react/useRef} - */ -- function useRef(initialValue?: undefined): MutableRefObject; -+ function useRef(initialValue: T | undefined): RefObject; - /** - * The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations. - * Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside diff --git a/patches/@types+react+19.0.10.patch b/patches/@types+react+19.0.10.patch new file mode 100644 index 0000000000..6f54d0b382 --- /dev/null +++ b/patches/@types+react+19.0.10.patch @@ -0,0 +1,31 @@ +diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts +index 2272032..18bd20a 100644 +--- a/node_modules/@types/react/index.d.ts ++++ b/node_modules/@types/react/index.d.ts +@@ -134,7 +134,7 @@ declare namespace React { + props: P, + ) => ReactNode | Promise) + // constructor signature must match React.Component +- | (new(props: P) => Component); ++ | (new(props: P, context?: any) => Component); + + /** + * Created by {@link createRef}, or {@link useRef} when passed `null`. +@@ -941,7 +941,7 @@ declare namespace React { + context: unknown; + + // Keep in sync with constructor signature of JSXElementConstructor and ComponentClass. +- constructor(props: P); ++ constructor(props: P, context?: unknown); + + // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. + // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 +@@ -1113,7 +1113,7 @@ declare namespace React { + */ + interface ComponentClass

extends StaticLifecycle { + // constructor signature must match React.Component +- new(props: P): Component; ++ new(props: P, context?: any): Component; + /** + * Ignored by React. + * @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release. diff --git a/patches/react-blurhash+0.3.0.patch b/patches/react-blurhash+0.3.0.patch new file mode 100644 index 0000000000..93e4b05d6e --- /dev/null +++ b/patches/react-blurhash+0.3.0.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts +index 3adbd0a..32e8c13 100644 +--- a/node_modules/react-blurhash/dist/index.d.ts ++++ b/node_modules/react-blurhash/dist/index.d.ts +@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent { + resolutionY: number; + }; + componentDidUpdate(): void; +- render(): JSX.Element; ++ render(): React.JSX.Element; + } + + declare type Props = React.CanvasHTMLAttributes & { +@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent { + componentDidUpdate(): void; + handleRef: (canvas: HTMLCanvasElement) => void; + draw: () => void; +- render(): JSX.Element; ++ render(): React.JSX.Element; + } + + export { Blurhash, BlurhashCanvas }; diff --git a/playwright/e2e/regression-tests/pills-click-in-app.spec.ts b/playwright/e2e/regression-tests/pills-click-in-app.spec.ts index 7486af7033..3670e64308 100644 --- a/playwright/e2e/regression-tests/pills-click-in-app.spec.ts +++ b/playwright/e2e/regression-tests/pills-click-in-app.spec.ts @@ -43,6 +43,7 @@ test.describe("Pills", () => { // go back to the message room and try to click on the pill text, as a user would await app.viewRoomByName(messageRoom); + await expect(page).toHaveURL(new RegExp(`/#/room/${messageRoomId}`)); const pillText = page.locator(".mx_EventTile_body .mx_Pill .mx_Pill_text"); await expect(pillText).toHaveCSS("pointer-events", "none"); await pillText.click({ force: true }); // force is to ensure we bypass pointer-events diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index d66c22e56f..d094890467 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -18,4 +18,9 @@ declare module "react" { // Fix lazy types - https://stackoverflow.com/a/71017028 function lazy>(factory: () => Promise<{ default: T }>): T; + + // Standardize defaultProps for FunctionComponent so we can write generics assuming `defaultProps` exists on ComponentType + interface FunctionComponent { + defaultProps?: unknown; + } } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5bc20095f3..f704657c32 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type LegacyRef, type ReactNode } from "react"; +import React, { type JSX, type Key, type LegacyRef, type ReactNode } from "react"; import sanitizeHtml, { type IOptions } from "sanitize-html"; import classNames from "classnames"; import katex from "katex"; @@ -239,7 +239,7 @@ class HtmlHighlighter extends BaseHighlighter { const emojiToHtmlSpan = (emoji: string): string => `${emoji}`; -const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => ( +const emojiToJsxSpan = (emoji: string, key: Key): JSX.Element => ( {emoji} diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 8ac235f78a..273b2eeb10 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -321,7 +321,7 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise } catch (error) { logger.error("Failed to login via OIDC", error); - await onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); + onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); return false; } } @@ -468,7 +468,7 @@ type TryAgainFunction = () => void; * @param description error description * @param tryAgain OPTIONAL function to call on try again button from error dialog */ -async function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): Promise { +function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void { Modal.createDialog(ErrorDialog, { title: _t("auth|oidc|error_title"), description, diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 7d4e291e5b..9942b4398c 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -212,7 +212,7 @@ export const RovingTabIndexProvider: React.FC = ({ scrollIntoView, onKeyDown, }) => { - const [state, dispatch] = useReducer>(reducer, { + const [state, dispatch] = useReducer(reducer, { nodes: [], }); diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index b3ab0ade46..2653de1034 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type ReactElement } from "react"; +import { type ReactElement, type RefAttributes, type HTMLAttributes } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import CommandProvider from "./CommandProvider"; @@ -31,7 +31,7 @@ export interface ICompletion { type?: "at-room" | "command" | "community" | "room" | "user"; completion: string; completionId?: string; - component: ReactElement; + component: ReactElement & HTMLAttributes>; range: ISelectionRange; command?: string; suffix?: string; diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 1667d73336..f566e2d256 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -21,8 +21,6 @@ export type IProps = Omit) => void; } interface IState { @@ -291,9 +285,6 @@ export default class MatrixChat extends React.PureComponent { */ private startInitSession = (): void => { const initProm = this.initSession(); - if (this.props.initPromiseCallback) { - this.props.initPromiseCallback(initProm); - } initProm.catch((err) => { // TODO: show an error screen, rather than a spinner of doom @@ -1002,10 +993,6 @@ export default class MatrixChat extends React.PureComponent { // Wait for the first sync to complete so that if a room does have an alias, // it would have been retrieved. if (!this.firstSyncComplete) { - if (!this.firstSyncPromise) { - logger.warn("Cannot view a room before first sync. room_id:", roomInfo.room_id); - return; - } await this.firstSyncPromise.promise; } @@ -1116,8 +1103,7 @@ export default class MatrixChat extends React.PureComponent { private viewUser(userId: string, subAction: string): void { // Wait for the first sync so that `getRoom` gives us a room object if it's // in the sync response - const waitForSync = this.firstSyncPromise ? this.firstSyncPromise.promise : Promise.resolve(); - waitForSync.then(() => { + this.firstSyncPromise.promise.then(() => { if (subAction === "chat") { this.chatCreateOrReuse(userId); return; @@ -1480,11 +1466,17 @@ export default class MatrixChat extends React.PureComponent { * (useful for setting listeners) */ private onWillStartClient(): void { - // reset the 'have completed first sync' flag, - // since we're about to start the client and therefore about - // to do the first sync + // Reset the 'have completed first sync' flag, + // since we're about to start the client and therefore about to do the first sync + // We resolve the existing promise with the new one to update any existing listeners + if (!this.firstSyncComplete) { + const firstSyncPromise = defer(); + this.firstSyncPromise.resolve(firstSyncPromise.promise); + this.firstSyncPromise = firstSyncPromise; + } else { + this.firstSyncPromise = defer(); + } this.firstSyncComplete = false; - this.firstSyncPromise = defer(); const cli = MatrixClientPeg.safeGet(); // Allow the JS SDK to reap timeline events. This reduces the amount of diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 4b842eaff1..b01f160551 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -27,14 +27,14 @@ export class Tab { * @param {string} id The tab's ID. * @param {string} label The untranslated tab label. * @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask. - * @param {React.ReactNode} body The JSX for the tab container. + * @param {JSX.Element} body The JSX for the tab container. * @param {string} screenName The screen name to report to Posthog. */ public constructor( public readonly id: T, public readonly label: TranslationKey, public readonly icon: string | JSX.Element | null, - public readonly body: React.ReactNode, + public readonly body: JSX.Element, public readonly screenName?: ScreenName, ) {} } diff --git a/src/components/structures/auth/header/AuthHeaderContext.tsx b/src/components/structures/auth/header/AuthHeaderContext.tsx index 768060b654..a45233564b 100644 --- a/src/components/structures/auth/header/AuthHeaderContext.tsx +++ b/src/components/structures/auth/header/AuthHeaderContext.tsx @@ -6,10 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { createContext, type Dispatch, type ReducerAction, type ReducerState } from "react"; +import { createContext, type Dispatch, type Reducer, type ReducerState } from "react"; import type { AuthHeaderReducer } from "./AuthHeaderProvider"; +type ReducerAction> = R extends Reducer ? A : never; + interface AuthHeaderContextType { state: ReducerState; dispatch: Dispatch>; diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx index ad1ff5b0cc..50a53b95c3 100644 --- a/src/components/structures/auth/header/AuthHeaderProvider.tsx +++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx @@ -25,7 +25,7 @@ interface AuthHeaderAction { export type AuthHeaderReducer = Reducer[], AuthHeaderAction>; export function AuthHeaderProvider({ children }: PropsWithChildren): JSX.Element { - const [state, dispatch] = useReducer( + const [state, dispatch] = useReducer[], [AuthHeaderAction]>( (state: ComponentProps[], action: AuthHeaderAction) => { switch (action.type) { case AuthHeaderActionType.Add: diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index aed1e61180..b1093e3901 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -18,6 +18,7 @@ import { CardContext } from "../right_panel/context"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile"; import { _t } from "../../../languageHandler"; +import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember | null; @@ -47,6 +48,7 @@ function MemberAvatar( }: IProps, ref: Ref, ): JSX.Element { + const cli = useContext(MatrixClientContext); const card = useContext(CardContext); const member = useRoomMemberProfile({ @@ -60,7 +62,7 @@ function MemberAvatar( let imageUrl: string | null | undefined; if (member?.name) { if (member.getMxcAvatarUrl()) { - imageUrl = mediaFromMxc(member.getMxcAvatarUrl() ?? "").getThumbnailOfSourceHttp( + imageUrl = mediaFromMxc(member.getMxcAvatarUrl() ?? "", cli).getThumbnailOfSourceHttp( parseInt(size, 10), parseInt(size, 10), resizeMethod, diff --git a/src/components/views/dialogs/ModuleUiDialog.tsx b/src/components/views/dialogs/ModuleUiDialog.tsx index 65c36bd8c4..533b24306f 100644 --- a/src/components/views/dialogs/ModuleUiDialog.tsx +++ b/src/components/views/dialogs/ModuleUiDialog.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { createRef, type RefObject } from "react"; import { type DialogContent, type DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent"; import { logger } from "matrix-js-sdk/src/logger"; import { type ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi"; @@ -27,10 +27,10 @@ interface IState extends IScrollableBaseState { // nothing special } -export class ModuleUiDialog

> extends ScrollableBaseModal< - IProps, - IState -> { +export class ModuleUiDialog< + P extends DialogProps = DialogProps, + C extends DialogContent

= DialogContent

, +> extends ScrollableBaseModal, IState> { private contentRef = createRef(); public constructor(props: IProps) { @@ -74,6 +74,11 @@ export class ModuleUiDialog

> e ...dialogProps, } as unknown as P; - return

{this.props.contentFactory(contentProps, this.contentRef)}
; + // XXX: we have to fudge the types here a little as the react-sdk-module-api lacks React 19 support + return ( +
+ {this.props.contentFactory(contentProps, this.contentRef as RefObject)} +
+ ); } } diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 378c2dfdc1..b5d13bf8e7 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -135,12 +135,19 @@ const AccessibleButton = forwardRef(function , ref: Ref, ): JSX.Element { - const newProps = restProps as RenderedElementProps; - newProps["aria-label"] = newProps["aria-label"] ?? title; + const newProps = { + ...restProps, + tabIndex, + role, + "aria-label": restProps["aria-label"] ?? title, + } as RenderedElementProps; + if (disabled) { newProps["aria-disabled"] = true; newProps["disabled"] = true; @@ -222,10 +229,6 @@ const AccessibleButton = forwardRef(function { diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 5c9792a296..c7efbb6166 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -6,7 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ChangeEvent, type ContextType, createRef, type SyntheticEvent } from "react"; +import React, { + type JSX, + type ToggleEvent, + type ChangeEvent, + type ContextType, + createRef, + type SyntheticEvent, +} from "react"; import { type MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { type RoomCanonicalAliasEventContent } from "matrix-js-sdk/src/types"; @@ -278,7 +285,7 @@ export default class AliasSettings extends React.Component { }); }; - private onLocalAliasesToggled = (event: ChangeEvent): void => { + private onLocalAliasesToggled = (event: ToggleEvent): void => { // expanded if (event.currentTarget.open) { // if local aliases haven't been preloaded yet at component mount diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 11b889304a..e7929348d6 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactElement, useCallback, useEffect, useState } from "react"; +import React, { type JSX, type ReactElement, useCallback, useEffect, useState } from "react"; import { type NonEmptyArray } from "../../../../../@types/common"; import { _t, getCurrentLanguage } from "../../../../../languageHandler"; diff --git a/src/linkify-matrix.ts b/src/linkify-matrix.ts index c22445bb96..0a96e088e9 100644 --- a/src/linkify-matrix.ts +++ b/src/linkify-matrix.ts @@ -198,7 +198,7 @@ export const options: Opts = { rel: "noreferrer noopener", }, - ignoreTags: ["pre", "code"], + ignoreTags: ["a", "pre", "code"], className: "linkified", diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx index a5a2aa9e0e..40217b1adb 100644 --- a/src/utils/ReactUtils.tsx +++ b/src/utils/ReactUtils.tsx @@ -9,15 +9,20 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, type ReactNode } from "react"; /** - * Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> hello world + * Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <>hello world * @param array the array of element to join * @param joiner the string/JSX.Element to join with * @returns the joined array */ export function jsxJoin(array: ReactNode[], joiner?: string | JSX.Element): JSX.Element { - const newArray: ReactNode[] = []; - array.forEach((element, index) => { - newArray.push(element, index === array.length - 1 ? null : joiner); - }); - return {newArray}; + return ( + <> + {array.map((element, index) => ( + + {element} + {index === array.length - 1 ? null : joiner} + + ))} + + ); } diff --git a/src/widgets/ManagedHybrid.ts b/src/widgets/ManagedHybrid.ts index f89420ae2a..b1a67db4fb 100644 --- a/src/widgets/ManagedHybrid.ts +++ b/src/widgets/ManagedHybrid.ts @@ -10,7 +10,6 @@ import { type IWidget } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; import { type Room } from "matrix-js-sdk/src/matrix"; -import { MatrixClientPeg } from "../MatrixClientPeg"; import { getCallBehaviourWellKnown } from "../utils/WellKnownUtils"; import WidgetUtils from "../utils/WidgetUtils"; import { type IStoredLayout, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; @@ -37,7 +36,7 @@ function getWidgetBuildUrl(room: Room): string | undefined { return SdkConfig.get().widget_build_url; } - const wellKnown = getCallBehaviourWellKnown(MatrixClientPeg.safeGet()); + const wellKnown = getCallBehaviourWellKnown(room.client); if (isDm && wellKnown?.ignore_dm) { return undefined; } diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 40aacb748e..cdda7b7dea 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -153,7 +153,7 @@ export const mockClientMethodsCrypto = (): Partial< > => ({ isKeyBackupKeyStored: jest.fn(), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), - secretStorage: { hasKey: jest.fn() }, + secretStorage: { hasKey: jest.fn(), isStored: jest.fn().mockResolvedValue(null) }, getCrypto: jest.fn().mockReturnValue({ getUserDeviceInfo: jest.fn(), getCrossSigningStatus: jest.fn().mockResolvedValue({ diff --git a/test/unit-tests/__snapshots__/TextForEvent-test.ts.snap b/test/unit-tests/__snapshots__/TextForEvent-test.ts.snap index 1ab32ffd04..6713c3d449 100644 --- a/test/unit-tests/__snapshots__/TextForEvent-test.ts.snap +++ b/test/unit-tests/__snapshots__/TextForEvent-test.ts.snap @@ -7,8 +7,6 @@ exports[`TextForEvent textForJoinRulesEvent() returns correct JSX message when r View settings diff --git a/test/unit-tests/autocomplete/EmojiProvider-test.ts b/test/unit-tests/autocomplete/EmojiProvider-test.ts index 81fd832e64..c2c42691b7 100644 --- a/test/unit-tests/autocomplete/EmojiProvider-test.ts +++ b/test/unit-tests/autocomplete/EmojiProvider-test.ts @@ -33,6 +33,10 @@ const EMOJI_SHORTCODES = [ // to simply assert that the final completion with the colon is the exact emoji. const TOO_SHORT_EMOJI_SHORTCODE = [{ emojiShortcode: ":o", expectedEmoji: "⭕️" }]; +interface CompletionComponentProps { + title: string; +} + describe("EmojiProvider", function () { const testRoom = mkStubRoom(undefined, undefined, undefined); stubClient(); @@ -69,8 +73,8 @@ describe("EmojiProvider", function () { const ep = new EmojiProvider(testRoom); const completionsList = await ep.getCompletions(":heart", { beginning: true, start: 0, end: 6 }); - expect(completionsList[0]?.component?.props.title).toEqual(":heartpulse:"); - expect(completionsList[1]?.component?.props.title).toEqual(":heart_eyes:"); + expect((completionsList[0]?.component?.props as CompletionComponentProps).title).toEqual(":heartpulse:"); + expect((completionsList[1]?.component?.props as CompletionComponentProps).title).toEqual(":heart_eyes:"); }); it("Exact match in recently used takes the lead", async function () { @@ -83,8 +87,8 @@ describe("EmojiProvider", function () { const ep = new EmojiProvider(testRoom); const completionsList = await ep.getCompletions(":heart", { beginning: true, start: 0, end: 6 }); - expect(completionsList[0]?.component?.props.title).toEqual(":heart:"); - expect(completionsList[1]?.component?.props.title).toEqual(":heartpulse:"); - expect(completionsList[2]?.component?.props.title).toEqual(":heart_eyes:"); + expect((completionsList[0]?.component?.props as CompletionComponentProps).title).toEqual(":heart:"); + expect((completionsList[1]?.component?.props as CompletionComponentProps).title).toEqual(":heartpulse:"); + expect((completionsList[2]?.component?.props as CompletionComponentProps).title).toEqual(":heart_eyes:"); }); }); diff --git a/test/unit-tests/components/structures/AutocompleteInput-test.tsx b/test/unit-tests/components/structures/AutocompleteInput-test.tsx index 901764f43b..17d443564c 100644 --- a/test/unit-tests/components/structures/AutocompleteInput-test.tsx +++ b/test/unit-tests/components/structures/AutocompleteInput-test.tsx @@ -63,12 +63,12 @@ describe("AutocompleteInput", () => { const input = getEditorInput(); - act(() => { + await act(async () => { fireEvent.focus(input); fireEvent.change(input, { target: { value: "user" } }); + await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1)); }); - await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1)); expect(screen.getByTestId("autocomplete-matches").childNodes).toHaveLength(mockCompletion.length); }); @@ -152,12 +152,12 @@ describe("AutocompleteInput", () => { const input = getEditorInput(); - act(() => { + await act(async () => { fireEvent.focus(input); fireEvent.change(input, { target: { value: "user" } }); + await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1)); }); - await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1)); expect(screen.getAllByTestId("custom-suggestion-element")).toHaveLength(mockCompletion.length); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 3b10a40bb8..709fa9da92 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -118,7 +118,7 @@ describe("", () => { startup: jest.fn(), }, login: jest.fn(), - loginFlows: jest.fn(), + loginFlows: jest.fn().mockResolvedValue({ flows: [] }), isGuest: jest.fn().mockReturnValue(false), clearStores: jest.fn(), setGuest: jest.fn(), @@ -165,12 +165,9 @@ describe("", () => { isNameResolvable: true, warning: "", }; - let initPromise: Promise | undefined; let defaultProps: ComponentProps; const getComponent = (props: Partial> = {}) => { - // MatrixChat does many questionable things which bomb tests in modern React mode, - // we'll want to refactor and break up MatrixChat before turning off legacyRoot mode - return render(, { legacyRoot: true }); + return render(); }; // make test results readable @@ -220,6 +217,8 @@ describe("", () => { }; beforeEach(async () => { + localStorage.clear(); + jest.restoreAllMocks(); defaultProps = { config: { brand: "Test", @@ -235,10 +234,8 @@ describe("", () => { onNewScreen: jest.fn(), onTokenLoginCompleted: jest.fn(), realQueryParams: {}, - initPromiseCallback: (p: Promise) => (initPromise = p), }; - initPromise = undefined; mockClient = getMockClientWithEventEmitter(getMockClientMethods()); jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); @@ -257,18 +254,9 @@ describe("", () => { }); afterEach(async () => { - // Wait for the promise that MatrixChat gives us to complete so that we know - // it's finished running its login code. We either need to do this or make the - // login code abort halfway through once the test finishes testing whatever it - // needs to test. If we do nothing, the login code will just continue running - // and interfere with the subsequent tests. - await initPromise; - // @ts-ignore DMRoomMap.setShared(null); - jest.restoreAllMocks(); - // emit a loggedOut event so that all of the Store singletons forget about their references to the mock client // (must be sync otherwise the next test will start before it happens) act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true)); @@ -313,7 +301,6 @@ describe("", () => { state: state, }; - const userId = "@alice:server.org"; const deviceId = "test-device-id"; const accessToken = "test-access-token-from-oidc"; @@ -335,8 +322,6 @@ describe("", () => { const dialog = await screen.findByRole("dialog"); expect(within(dialog).getByText(errorMessage)).toBeInTheDocument(); - // just check we're back on welcome page - await expect(screen.findByTestId("mx_welcome_screen")).resolves.toBeInTheDocument(); }; beforeEach(() => { @@ -518,6 +503,9 @@ describe("", () => { it("should set logged in and start MatrixClient", async () => { getComponent({ realQueryParams }); + defaultDispatcher.dispatch({ + action: "will_start_client", + }); // client successfully started await waitFor(() => expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" }), @@ -597,9 +585,7 @@ describe("", () => { // wait for logged in view to load await screen.findByLabelText("User menu"); - expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); - const h1Element = screen.getByRole("heading", { level: 1 }); - expect(h1Element).toHaveTextContent(`Welcome Ernie`); + await screen.findByRole("heading", { level: 1, name: "Welcome Ernie" }); }); describe("clean up drafts", () => { @@ -612,9 +598,6 @@ describe("", () => { localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content"); mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null); }); - afterEach(() => { - jest.restoreAllMocks(); - }); it("should clean up drafts", async () => { Date.now = jest.fn(() => timestamp); localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content"); @@ -1113,8 +1096,6 @@ describe("", () => { "org.matrix.e2e_cross_signing", ); - await flushPromises(); - // logged in await screen.findByLabelText("User menu"); }); @@ -1210,7 +1191,6 @@ describe("", () => { }; let loginClient!: ReturnType; - const userId = "@alice:server.org"; const deviceId = "test-device-id"; const accessToken = "test-access-token"; const clientLoginResponse = { @@ -1304,6 +1284,7 @@ describe("", () => { }, ); }); + it("should clear storage", async () => { const localStorageClearSpy = jest.spyOn(localStorage.__proto__, "clear"); @@ -1349,6 +1330,9 @@ describe("", () => { it("should continue to post login setup when no session is found in local storage", async () => { getComponent({ realQueryParams }); + defaultDispatcher.dispatch({ + action: "will_start_client", + }); // logged in but waiting for sync screen await screen.findByText("Logout"); @@ -1604,8 +1588,13 @@ describe("", () => { }); await flushPromises(); mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); - await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); - expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + _payload: expect.objectContaining({ _result: expect.objectContaining({ __test: true }) }), + }), + ), + ); }); it("should show the recovery method removed dialog", async () => { @@ -1622,8 +1611,13 @@ describe("", () => { }); await flushPromises(); mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); - await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); - expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + _payload: expect.objectContaining({ _result: expect.objectContaining({ __test: true }) }), + }), + ), + ); }); }); }); diff --git a/test/unit-tests/components/structures/PipContainer-test.tsx b/test/unit-tests/components/structures/PipContainer-test.tsx index 74b6577444..c401853c2f 100644 --- a/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/test/unit-tests/components/structures/PipContainer-test.tsx @@ -148,6 +148,8 @@ describe("PipContainer", () => { WidgetStore.instance.addVirtualWidget(call.widget, room.roomId); WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, { stop: () => {}, + hasCapability: jest.fn(), + feedStateUpdate: jest.fn().mockResolvedValue(undefined), } as unknown as ClientWidgetApi); await call.start(); @@ -271,7 +273,10 @@ describe("PipContainer", () => { Parameters >() .mockResolvedValue({}); - const mockMessaging = { transport: { send: sendSpy }, stop: () => {} } as unknown as ClientWidgetApi; + const mockMessaging = { + transport: { send: sendSpy }, + stop: () => {}, + } as unknown as ClientWidgetApi; WidgetMessagingStore.instance.storeMessaging(new Widget(widget), room.roomId, mockMessaging); await user.click(screen.getByRole("button", { name: "Leave" })); expect(sendSpy).toHaveBeenCalledWith(ElementWidgetActions.HangupCall, {}); diff --git a/test/unit-tests/components/structures/__snapshots__/MessagePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MessagePanel-test.tsx.snap index f7f825afcd..bb748e820a 100644 --- a/test/unit-tests/components/structures/__snapshots__/MessagePanel-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MessagePanel-test.tsx.snap @@ -108,9 +108,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = ` - - @user:id made no changes 100 times - + @user:id made no changes 100 times diff --git a/test/unit-tests/components/views/messages/TextualBody-test.tsx b/test/unit-tests/components/views/messages/TextualBody-test.tsx index 5534ae6911..cb3d755f8b 100644 --- a/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -197,7 +197,7 @@ describe("", () => { const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( - `"Chat with Member"`, + `"Chat with Member"`, ); }); diff --git a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap index 8a725eee61..ae1c638a90 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap @@ -510,9 +510,9 @@ exports[` renders plain-text m.text correctly linkification get a `; -exports[` renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit Message from Member"`; +exports[` renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit Message from Member"`; -exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; +exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; exports[` renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = `
renders a list of active polls when there are polls in tabindex="0" >
@@ -116,7 +116,7 @@ exports[` renders a list of active polls when there are polls in tabindex="0" >
diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index a70fdabafd..71465fddbd 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -467,7 +467,7 @@ describe("", () => { await expect(screen.findByRole("button", { name: "Deactivate user" })).resolves.toBeInTheDocument(); if (screen.queryAllByRole("progressbar").length) { - await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar")); + await act(() => waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"))); } expect(container).toMatchSnapshot(); }); diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap index db0252ebe9..8084634a98 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap @@ -19,7 +19,7 @@ exports[` with crypto enabled renders 1`] = `

- - a - - and - - b - - + + + + a + + + + and + + + + b + + + `; exports[`FormattingUtils formatList should return expected sentence in ReactNode when given more React children 1`] = ` - - - a - - , - - b - - , - - c - - and - - d - - + + + + a + + + + , + + + + b + + + + , + + + + c + + + + and + + + + d + + + `; exports[`FormattingUtils formatList should return expected sentence in ReactNode when using itemLimit 1`] = ` - - - a - - , - - b - - + + + + a + + , + + + + b + + + and 2 others `; diff --git a/test/unit-tests/utils/export-test.tsx b/test/unit-tests/utils/export-test.tsx index bd0c13e05c..a5e0063f3b 100644 --- a/test/unit-tests/utils/export-test.tsx +++ b/test/unit-tests/utils/export-test.tsx @@ -253,8 +253,9 @@ describe("export", function () { }, setProgressText, ); - const imageRegex = //; - expect(imageRegex.test(renderToString(exporter.getEventTile(mkImageEvent(), true)))).toBeTruthy(); + expect(renderToString(exporter.getEventTile(mkImageEvent(), true))).toMatch( + //, + ); }); const invalidExportOptions: [string, IExportOptions][] = [ diff --git a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index 83ffc13e78..56cc1d0f27 100644 --- a/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/unit-tests/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -57,7 +57,7 @@ exports[`HTMLExport should export 1`] = `

-
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • +
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • diff --git a/test/unit-tests/utils/permalinks/Permalinks-test.ts b/test/unit-tests/utils/permalinks/Permalinks-test.ts index 5effb8ed43..fcb7d3257f 100644 --- a/test/unit-tests/utils/permalinks/Permalinks-test.ts +++ b/test/unit-tests/utils/permalinks/Permalinks-test.ts @@ -116,7 +116,7 @@ describe("Permalinks", function () { it("should gracefully handle invalid MXIDs", () => { const roomId = "!fake:example.org"; - const alice50 = makeMemberWithPL(roomId, "@alice:pl_50:org", 50); + const alice50 = makeMemberWithPL(roomId, "@alice:pl-50:org", 50); const room = mockRoom(roomId, [alice50]); const creator = new RoomPermalinkCreator(room); creator.load(); diff --git a/yarn.lock b/yarn.lock index 311151d411..e39506bca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3376,10 +3376,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@18.3.5": - version "18.3.5" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716" - integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== +"@types/react-dom@19.0.4": + version "19.0.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89" + integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg== "@types/react-redux@^7.1.20": version "7.1.34" @@ -3404,12 +3404,11 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@18.3.18": - version "18.3.18" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" - integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== +"@types/react@*", "@types/react@19.0.10": + version "19.0.10" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb" + integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== dependencies: - "@types/prop-types" "*" csstype "^3.0.2" "@types/retry@0.12.0": @@ -8936,7 +8935,7 @@ long@^5.2.0: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -10856,13 +10855,12 @@ react-clientside-effect@^1.2.7: dependencies: "@babel/runtime" "^7.12.13" -react-dom@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== +react-dom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" + integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "^0.25.0" react-focus-lock@^2.5.1: version "2.13.6" @@ -10876,21 +10874,21 @@ react-focus-lock@^2.5.1: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" +react-is@19.0.0, react-is@^17.0.1, react-is@^18.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.0.0.tgz#d6669fd389ff022a9684f708cf6fa4962d1fea7a" + integrity sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1, react-is@^17.0.2: +react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -10968,12 +10966,10 @@ react-virtualized@^9.22.5: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" -react@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" +react@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" + integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== read-cache@^1.0.0: version "1.0.0" @@ -11411,12 +11407,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== schema-utils@^3.0.0: version "3.3.0"