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>
This commit is contained in:
renovate[bot]
2025-04-09 19:03:09 +00:00
committed by GitHub
parent f54fbf7231
commit e1b2e3a101
44 changed files with 300 additions and 290 deletions

View File

@@ -18,4 +18,9 @@ declare module "react" {
// Fix lazy types - https://stackoverflow.com/a/71017028
function lazy<T extends ComponentType<any>>(factory: () => Promise<{ default: T }>): T;
// Standardize defaultProps for FunctionComponent so we can write generics assuming `defaultProps` exists on ComponentType
interface FunctionComponent {
defaultProps?: unknown;
}
}

View File

@@ -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<string> {
const emojiToHtmlSpan = (emoji: string): string =>
`<span class='mx_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => (
const emojiToJsxSpan = (emoji: string, key: Key): JSX.Element => (
<span key={key} className="mx_Emoji" title={unicodeToShortcode(emoji)}>
{emoji}
</span>

View File

@@ -321,7 +321,7 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean>
} 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<void> {
function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void {
Modal.createDialog(ErrorDialog, {
title: _t("auth|oidc|error_title"),
description,

View File

@@ -212,7 +212,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
scrollIntoView,
onKeyDown,
}) => {
const [state, dispatch] = useReducer<Reducer<IState, Action>>(reducer, {
const [state, dispatch] = useReducer<IState, [Action]>(reducer, {
nodes: [],
});

View File

@@ -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<RefAttributes<HTMLElement> & HTMLAttributes<HTMLElement>>;
range: ISelectionRange;
command?: string;
suffix?: string;

View File

@@ -21,8 +21,6 @@ export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollb
// scroll horizontally rather than vertically. This should only be used on components
// with no vertical scroll opportunity.
verticalScrollsHorizontally?: boolean;
children: React.ReactNode;
};
interface IState {

View File

@@ -165,12 +165,6 @@ interface IProps {
initialScreenAfterLogin?: IScreen;
// displayname, if any, to set on the device when logging in/registering.
defaultDeviceDisplayName?: string;
// Used by tests, this function is called when session initialisation starts
// with a promise that resolves or rejects once the initialiation process
// has finished, so that tests can wait for this to avoid them executing over
// each other.
initPromiseCallback?: (p: Promise<void>) => void;
}
interface IState {
@@ -291,9 +285,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
*/
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<IProps, IState> {
// 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<IProps, IState> {
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<IProps, IState> {
* (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<void>();
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

View File

@@ -27,14 +27,14 @@ export class Tab<T extends string> {
* @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,
) {}
}

View File

@@ -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<any, any>> = R extends Reducer<any, infer A> ? A : never;
interface AuthHeaderContextType {
state: ReducerState<AuthHeaderReducer>;
dispatch: Dispatch<ReducerAction<AuthHeaderReducer>>;

View File

@@ -25,7 +25,7 @@ interface AuthHeaderAction {
export type AuthHeaderReducer = Reducer<ComponentProps<typeof AuthHeaderModifier>[], AuthHeaderAction>;
export function AuthHeaderProvider({ children }: PropsWithChildren): JSX.Element {
const [state, dispatch] = useReducer<AuthHeaderReducer>(
const [state, dispatch] = useReducer<ComponentProps<typeof AuthHeaderModifier>[], [AuthHeaderAction]>(
(state: ComponentProps<typeof AuthHeaderModifier>[], action: AuthHeaderAction) => {
switch (action.type) {
case AuthHeaderActionType.Add:

View File

@@ -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<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember | null;
@@ -47,6 +48,7 @@ function MemberAvatar(
}: IProps,
ref: Ref<HTMLElement>,
): 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,

View File

@@ -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<P extends DialogProps, C extends DialogContent<P>> extends ScrollableBaseModal<
IProps<P, C>,
IState
> {
export class ModuleUiDialog<
P extends DialogProps = DialogProps,
C extends DialogContent<P> = DialogContent<P>,
> extends ScrollableBaseModal<IProps<P, C>, IState> {
private contentRef = createRef<C>();
public constructor(props: IProps<P, C>) {
@@ -74,6 +74,11 @@ export class ModuleUiDialog<P extends DialogProps, C extends DialogContent<P>> e
...dialogProps,
} as unknown as P;
return <div className="mx_ModuleUiDialog">{this.props.contentFactory(contentProps, this.contentRef)}</div>;
// XXX: we have to fudge the types here a little as the react-sdk-module-api lacks React 19 support
return (
<div className="mx_ModuleUiDialog">
{this.props.contentFactory(contentProps, this.contentRef as RefObject<C>)}
</div>
);
}
}

View File

@@ -135,12 +135,19 @@ const AccessibleButton = forwardRef(function <T extends ElementType = typeof def
placement = "right",
onTooltipOpenChange,
disableTooltip,
role = "button",
tabIndex = 0,
...restProps
}: ButtonProps<T>,
ref: Ref<HTMLElementTagNameMap[T]>,
): JSX.Element {
const newProps = restProps as RenderedElementProps<T>;
newProps["aria-label"] = newProps["aria-label"] ?? title;
const newProps = {
...restProps,
tabIndex,
role,
"aria-label": restProps["aria-label"] ?? title,
} as RenderedElementProps<T>;
if (disabled) {
newProps["aria-disabled"] = true;
newProps["disabled"] = true;
@@ -222,10 +229,6 @@ const AccessibleButton = forwardRef(function <T extends ElementType = typeof def
});
// Type assertion required due to forwardRef type workaround in react.d.ts
(AccessibleButton as FunctionComponent).defaultProps = {
role: "button",
tabIndex: 0,
};
(AccessibleButton as FunctionComponent).displayName = "AccessibleButton";
interface RefProp<T extends ElementType> {

View File

@@ -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<IProps, IState> {
});
};
private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>): void => {
private onLocalAliasesToggled = (event: ToggleEvent<HTMLDetailsElement>): void => {
// expanded
if (event.currentTarget.open) {
// if local aliases haven't been preloaded yet at component mount

View File

@@ -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";

View File

@@ -198,7 +198,7 @@ export const options: Opts = {
rel: "noreferrer noopener",
},
ignoreTags: ["pre", "code"],
ignoreTags: ["a", "pre", "code"],
className: "linkified",

View File

@@ -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"], " ") -> <span>hello world</span>
* 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 <span>{newArray}</span>;
return (
<>
{array.map((element, index) => (
<React.Fragment key={index}>
{element}
{index === array.length - 1 ? null : joiner}
</React.Fragment>
))}
</>
);
}

View File

@@ -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;
}