diff --git a/.eslintrc.js b/.eslintrc.js index 892d7cdbb1..26865d55ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,10 @@ module.exports = { ["window.innerHeight", "window.innerWidth", "window.visualViewport"], "Use UIStore to access window dimensions instead.", ), + ...buildRestrictedPropertiesOptions( + ["React.forwardRef", "*.forwardRef", "forwardRef"], + "Use ref props instead.", + ), ...buildRestrictedPropertiesOptions( ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], "Use Media helper instead to centralise access for customisation.", @@ -55,6 +59,11 @@ module.exports = { "error", { paths: [ + { + name: "react", + importNames: ["forwardRef"], + message: "Use ref props instead.", + }, { name: "@testing-library/react", message: "Please use jest-matrix-react instead", diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index d094890467..61a4c59992 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -6,16 +6,9 @@ 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 PropsWithChildren } from "react"; - -import type React from "react"; +import { type ComponentType } from "react"; declare module "react" { - // Fix forwardRef types for Generic components - https://stackoverflow.com/a/58473012 - function forwardRef( - render: (props: PropsWithChildren

, ref: React.ForwardedRef) => React.ReactElement | null, - ): (props: P & React.RefAttributes) => React.ReactElement | null; - // Fix lazy types - https://stackoverflow.com/a/71017028 function lazy>(factory: () => Promise<{ default: T }>): T; diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 317ff8b936..5583292e48 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -6,18 +6,20 @@ 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, { forwardRef } from "react"; +import React, { type Ref, type JSX } from "react"; import { RovingTabIndexProvider } from "./RovingTabIndex"; import { getKeyBindingsManager } from "../KeyBindingsManager"; import { KeyBindingAction } from "./KeyboardShortcuts"; -interface IProps extends Omit, "onKeyDown"> {} +interface IProps extends Omit, "onKeyDown"> { + ref?: Ref; +} // This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines. // https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar // All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref` -const Toolbar = forwardRef(({ children, ...props }, ref) => { +const Toolbar = ({ children, ref, ...props }: IProps): JSX.Element => { const onKeyDown = (ev: React.KeyboardEvent): void => { const target = ev.target as HTMLElement; // Don't interfere with input default keydown behaviour @@ -55,6 +57,6 @@ const Toolbar = forwardRef(({ children, ...props }, ref) )} ); -}); +}; export default Toolbar; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 8096203d0e..df16d15821 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,7 +8,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, { forwardRef, type Ref } from "react"; +import React, { type Ref, type JSX } from "react"; import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton"; @@ -16,13 +16,19 @@ type Props = ButtonProps & { label?: string; // whether the context menu is currently open isExpanded: boolean; + ref?: Ref; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = forwardRef(function ( - { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, -) { +export const ContextMenuButton = function ({ + label, + isExpanded, + children, + onClick, + onContextMenu, + ref, + ...props +}: Props): JSX.Element { return ( ); -}); +}; diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index f58fbea171..b0d0156c0f 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -8,7 +8,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, { forwardRef, type Ref } from "react"; +import React, { type JSX } from "react"; import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton"; @@ -18,10 +18,14 @@ type Props = ButtonProps & { }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton = forwardRef(function ( - { isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, -) { +export const ContextMenuTooltipButton = function ({ + isExpanded, + children, + onClick, + onContextMenu, + ref, + ...props +}: Props): JSX.Element { return ( ); -}); +}; diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index ebbc1732e2..f1493d0c23 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.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, { forwardRef } from "react"; +import React, { type Ref, type JSX } from "react"; import classNames from "classnames"; /* These were earlier stateless functional components but had to be converted @@ -16,14 +16,24 @@ presumably wrap them in a

before rendering but I think this is the better */ interface ITextualCompletionProps { - title?: string; - subtitle?: string; - description?: string; - className?: string; + "title"?: string; + "subtitle"?: string; + "description"?: string; + "className"?: string; + "aria-selected"?: boolean; + "ref"?: Ref; } -export const TextualCompletion = forwardRef((props, ref) => { - const { title, subtitle, description, className, "aria-selected": ariaSelectedAttribute, ...restProps } = props; +export const TextualCompletion = (props: ITextualCompletionProps): JSX.Element => { + const { + title, + subtitle, + description, + className, + "aria-selected": ariaSelectedAttribute, + ref, + ...restProps + } = props; return (
((props {description}
); -}); +}; interface IPillCompletionProps extends ITextualCompletionProps { children?: React.ReactNode; } -export const PillCompletion = forwardRef((props, ref) => { +export const PillCompletion = (props: IPillCompletionProps): JSX.Element => { const { title, subtitle, @@ -51,6 +61,7 @@ export const PillCompletion = forwardRef((props, ref) className, children, "aria-selected": ariaSelectedAttribute, + ref, ...restProps } = props; return ( @@ -67,4 +78,4 @@ export const PillCompletion = forwardRef((props, ref) {description}
); -}); +}; diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 9cd5564795..39accdc8da 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -127,7 +127,7 @@ export default class UserProvider extends AutocompleteProvider { suffix: selection.beginning && range!.start === 0 ? ": " : " ", href: makeUserPermalink(user.userId), component: ( - + ), diff --git a/src/components/structures/MatrixClientContextProvider.tsx b/src/components/structures/MatrixClientContextProvider.tsx index e22d6bfcde..7d555f5809 100644 --- a/src/components/structures/MatrixClientContextProvider.tsx +++ b/src/components/structures/MatrixClientContextProvider.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, { type PropsWithChildren, useEffect, useState } from "react"; +import React, { type PropsWithChildren, useEffect, useState, type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +85,7 @@ interface Props { * A React component which exposes a {@link MatrixClientContext} and a {@link LocalDeviceVerificationStateContext} * to its children. */ -export function MatrixClientContextProvider(props: PropsWithChildren): React.JSX.Element { +export function MatrixClientContextProvider(props: PropsWithChildren): JSX.Element { const verificationState = useLocalVerificationState(props.client); return ( diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 4aadabd723..c4d4f30b05 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.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, { type JSX, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { type JSX, type Ref, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { type ISearchResults, type IThreadBundledRelationship, @@ -44,269 +44,275 @@ interface Props { resizeNotifier: ResizeNotifier; className: string; onUpdate(inProgress: boolean, results: ISearchResults | null, error: Error | null): void; + ref?: Ref; } // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? -export const RoomSearchView = forwardRef( - ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { - const client = useContext(MatrixClientContext); - const roomContext = useScopedRoomContext("showHiddenEvents"); - const [highlights, setHighlights] = useState(null); - const [results, setResults] = useState(null); - const aborted = useRef(false); - // A map from room ID to permalink creator - const permalinkCreators = useMemo(() => new Map(), []); - const innerRef = useRef(null); +export const RoomSearchView = ({ + term, + scope, + promise, + abortController, + resizeNotifier, + className, + onUpdate, + inProgress, + ref, +}: Props): JSX.Element => { + const client = useContext(MatrixClientContext); + const roomContext = useScopedRoomContext("showHiddenEvents"); + const [highlights, setHighlights] = useState(null); + const [results, setResults] = useState(null); + const aborted = useRef(false); + // A map from room ID to permalink creator + const permalinkCreators = useMemo(() => new Map(), []); + const innerRef = useRef(null); - useEffect(() => { - return () => { - permalinkCreators.forEach((pc) => pc.stop()); - permalinkCreators.clear(); - }; - }, [permalinkCreators]); + useEffect(() => { + return () => { + permalinkCreators.forEach((pc) => pc.stop()); + permalinkCreators.clear(); + }; + }, [permalinkCreators]); - const handleSearchResult = useCallback( - (searchPromise: Promise): Promise => { - onUpdate(true, null, null); + const handleSearchResult = useCallback( + (searchPromise: Promise): Promise => { + onUpdate(true, null, null); - return searchPromise.then( - async (results): Promise => { - debuglog("search complete"); - if (aborted.current) { - logger.error("Discarding stale search results"); - return false; - } + return searchPromise.then( + async (results): Promise => { + debuglog("search complete"); + if (aborted.current) { + logger.error("Discarding stale search results"); + return false; + } - // postgres on synapse returns us precise details of the strings - // which actually got matched for highlighting. - // - // In either case, we want to highlight the literal search term - // whether it was used by the search engine or not. + // postgres on synapse returns us precise details of the strings + // which actually got matched for highlighting. + // + // In either case, we want to highlight the literal search term + // whether it was used by the search engine or not. - let highlights = results.highlights; - if (!highlights.includes(term)) { - highlights = highlights.concat(term); - } + let highlights = results.highlights; + if (!highlights.includes(term)) { + highlights = highlights.concat(term); + } - // For overlapping highlights, - // favour longer (more specific) terms first - highlights = highlights.sort(function (a, b) { - return b.length - a.length; - }); + // For overlapping highlights, + // favour longer (more specific) terms first + highlights = highlights.sort(function (a, b) { + return b.length - a.length; + }); - for (const result of results.results) { - for (const event of result.context.getTimeline()) { - const bundledRelationship = - event.getServerAggregatedRelation( - THREAD_RELATION_TYPE.name, - ); - if (!bundledRelationship || event.getThread()) continue; - const room = client.getRoom(event.getRoomId()); - const thread = room?.findThreadForEvent(event); - if (thread) { - event.setThread(thread); - } else { - room?.createThread(event.getId()!, event, [], true); - } + for (const result of results.results) { + for (const event of result.context.getTimeline()) { + const bundledRelationship = event.getServerAggregatedRelation( + THREAD_RELATION_TYPE.name, + ); + if (!bundledRelationship || event.getThread()) continue; + const room = client.getRoom(event.getRoomId()); + const thread = room?.findThreadForEvent(event); + if (thread) { + event.setThread(thread); + } else { + room?.createThread(event.getId()!, event, [], true); } } + } - setHighlights(highlights); - setResults({ ...results }); // copy to force a refresh - onUpdate(false, results, null); + setHighlights(highlights); + setResults({ ...results }); // copy to force a refresh + onUpdate(false, results, null); + return false; + }, + (error) => { + if (aborted.current) { + logger.error("Discarding stale search results"); return false; - }, - (error) => { - if (aborted.current) { - logger.error("Discarding stale search results"); - return false; - } - logger.error("Search failed", error); - onUpdate(false, null, error); - return false; - }, - ); - }, - [client, term, onUpdate], - ); - - // Mount & unmount effect - useEffect(() => { - aborted.current = false; - handleSearchResult(promise); - return () => { - aborted.current = true; - abortController?.abort(); - }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - // show searching spinner - if (results === null) { - return ( -
+ } + logger.error("Search failed", error); + onUpdate(false, null, error); + return false; + }, ); - } + }, + [client, term, onUpdate], + ); - const onSearchResultsFillRequest = async (backwards: boolean): Promise => { - if (!backwards) { - return false; - } - - if (!results.next_batch) { - debuglog("no more search results"); - return false; - } - - debuglog("requesting more search results"); - const searchPromise = searchPagination(client, results); - return handleSearchResult(searchPromise); + // Mount & unmount effect + useEffect(() => { + aborted.current = false; + handleSearchResult(promise); + return () => { + aborted.current = true; + abortController?.abort(); }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps - const ret: JSX.Element[] = []; + // show searching spinner + if (results === null) { + return ( +
+ ); + } - if (inProgress) { - ret.push( -
  • - -
  • , - ); + const onSearchResultsFillRequest = async (backwards: boolean): Promise => { + if (!backwards) { + return false; } if (!results.next_batch) { - if (!results?.results?.length) { + debuglog("no more search results"); + return false; + } + + debuglog("requesting more search results"); + const searchPromise = searchPagination(client, results); + return handleSearchResult(searchPromise); + }; + + const ret: JSX.Element[] = []; + + if (inProgress) { + ret.push( +
  • + +
  • , + ); + } + + if (!results.next_batch) { + if (!results?.results?.length) { + ret.push( +
  • +

    {_t("common|no_results")}

    +
  • , + ); + } else { + ret.push( +
  • +

    {_t("no_more_results")}

    +
  • , + ); + } + } + + const onRef = (e: ScrollPanel | null): void => { + if (typeof ref === "function") { + ref(e); + } else if (!!ref) { + ref.current = e; + } + innerRef.current = e; + }; + + let lastRoomId: string | undefined; + let mergedTimeline: MatrixEvent[] = []; + let ourEventsIndexes: number[] = []; + + for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) { + const result = results.results[i]; + + const mxEv = result.context.getEvent(); + const roomId = mxEv.getRoomId()!; + const room = client.getRoom(roomId); + if (!room) { + // if we do not have the room in js-sdk stores then hide it as we cannot easily show it + // As per the spec, an all rooms search can create this condition, + // it happens with Seshat but not Synapse. + // It will make the result count not match the displayed count. + logger.log("Hiding search result from an unknown room", roomId); + continue; + } + + if (!haveRendererForEvent(mxEv, client, roomContext.showHiddenEvents)) { + // XXX: can this ever happen? It will make the result count + // not match the displayed count. + continue; + } + + if (scope === SearchScope.All) { + if (roomId !== lastRoomId) { ret.push( -
  • -

    {_t("common|no_results")}

    -
  • , - ); - } else { - ret.push( -
  • -

    {_t("no_more_results")}

    +
  • +

    + {_t("common|room")}: {room.name} +

  • , ); + lastRoomId = roomId; } } - const onRef = (e: ScrollPanel | null): void => { - if (typeof ref === "function") { - ref(e); - } else if (!!ref) { - ref.current = e; - } - innerRef.current = e; - }; + const resultLink = "#/room/" + roomId + "/" + mxEv.getId(); - let lastRoomId: string | undefined; - let mergedTimeline: MatrixEvent[] = []; - let ourEventsIndexes: number[] = []; - - for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) { - const result = results.results[i]; - - const mxEv = result.context.getEvent(); - const roomId = mxEv.getRoomId()!; - const room = client.getRoom(roomId); - if (!room) { - // if we do not have the room in js-sdk stores then hide it as we cannot easily show it - // As per the spec, an all rooms search can create this condition, - // it happens with Seshat but not Synapse. - // It will make the result count not match the displayed count. - logger.log("Hiding search result from an unknown room", roomId); - continue; - } - - if (!haveRendererForEvent(mxEv, client, roomContext.showHiddenEvents)) { - // XXX: can this ever happen? It will make the result count - // not match the displayed count. - continue; - } - - if (scope === SearchScope.All) { - if (roomId !== lastRoomId) { - ret.push( -
  • -

    - {_t("common|room")}: {room.name} -

    -
  • , - ); - lastRoomId = roomId; - } - } - - const resultLink = "#/room/" + roomId + "/" + mxEv.getId(); - - // merging two successive search result if the query is present in both of them - const currentTimeline = result.context.getTimeline(); - const nextTimeline = i > 0 ? results.results[i - 1].context.getTimeline() : []; - - if (i > 0 && currentTimeline[currentTimeline.length - 1].getId() == nextTimeline[0].getId()) { - // if this is the first searchResult we merge then add all values of the current searchResult - if (mergedTimeline.length == 0) { - for (let j = mergedTimeline.length == 0 ? 0 : 1; j < result.context.getTimeline().length; j++) { - mergedTimeline.push(currentTimeline[j]); - } - ourEventsIndexes.push(result.context.getOurEventIndex()); - } - - // merge the events of the next searchResult - for (let j = 1; j < nextTimeline.length; j++) { - mergedTimeline.push(nextTimeline[j]); - } - - // add the index of the matching event of the next searchResult - ourEventsIndexes.push( - ourEventsIndexes[ourEventsIndexes.length - 1] + - results.results[i - 1].context.getOurEventIndex() + - 1, - ); - - continue; - } + // merging two successive search result if the query is present in both of them + const currentTimeline = result.context.getTimeline(); + const nextTimeline = i > 0 ? results.results[i - 1].context.getTimeline() : []; + if (i > 0 && currentTimeline[currentTimeline.length - 1].getId() == nextTimeline[0].getId()) { + // if this is the first searchResult we merge then add all values of the current searchResult if (mergedTimeline.length == 0) { - mergedTimeline = result.context.getTimeline(); - ourEventsIndexes = []; + for (let j = mergedTimeline.length == 0 ? 0 : 1; j < result.context.getTimeline().length; j++) { + mergedTimeline.push(currentTimeline[j]); + } ourEventsIndexes.push(result.context.getOurEventIndex()); } - let permalinkCreator = permalinkCreators.get(roomId); - if (!permalinkCreator) { - permalinkCreator = new RoomPermalinkCreator(room); - permalinkCreator.start(); - permalinkCreators.set(roomId, permalinkCreator); + // merge the events of the next searchResult + for (let j = 1; j < nextTimeline.length; j++) { + mergedTimeline.push(nextTimeline[j]); } - ret.push( - , + // add the index of the matching event of the next searchResult + ourEventsIndexes.push( + ourEventsIndexes[ourEventsIndexes.length - 1] + results.results[i - 1].context.getOurEventIndex() + 1, ); - ourEventsIndexes = []; - mergedTimeline = []; + continue; } - return ( - -
  • - {ret} - + if (mergedTimeline.length == 0) { + mergedTimeline = result.context.getTimeline(); + ourEventsIndexes = []; + ourEventsIndexes.push(result.context.getOurEventIndex()); + } + + let permalinkCreator = permalinkCreators.get(roomId); + if (!permalinkCreator) { + permalinkCreator = new RoomPermalinkCreator(room); + permalinkCreator.start(); + permalinkCreators.set(roomId, permalinkCreator); + } + + ret.push( + , ); - }, -); + + ourEventsIndexes = []; + mergedTimeline = []; + } + + return ( + +
  • + {ret} + + ); +}; diff --git a/src/components/structures/auth/LoginSplashView.tsx b/src/components/structures/auth/LoginSplashView.tsx index 62e5734e19..aae7980185 100644 --- a/src/components/structures/auth/LoginSplashView.tsx +++ b/src/components/structures/auth/LoginSplashView.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 from "react"; +import React, { type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; @@ -43,13 +43,13 @@ type MigrationState = { /** * The view that is displayed after we have logged in, before the first /sync is completed. */ -export function LoginSplashView(props: Props): React.JSX.Element { +export function LoginSplashView(props: Props): JSX.Element { const migrationState = useTypedEventEmitterState( props.matrixClient, CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress?: number, total?: number): MigrationState => ({ progress: progress ?? -1, totalSteps: total ?? -1 }), ); - let errorBox: React.JSX.Element | undefined; + let errorBox: JSX.Element | undefined; if (props.syncError) { errorBox =
    {messageForSyncError(props.syncError)}
    ; } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 8aabf60149..0837bf1dc8 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.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 AriaRole, forwardRef, useCallback, useContext, useEffect, useState } from "react"; +import React, { type AriaRole, type JSX, type Ref, useCallback, useContext, useEffect, useState } from "react"; import classNames from "classnames"; import { ClientEvent, type SyncState } from "matrix-js-sdk/src/matrix"; import { Avatar } from "@vector-im/compound-web"; @@ -34,6 +34,7 @@ interface IProps { tabIndex?: number; altText?: string; role?: AriaRole; + ref?: Ref; } const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => { @@ -87,7 +88,7 @@ const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [ return [imageUrl, onError]; }; -const BaseAvatar = forwardRef((props, ref) => { +const BaseAvatar = (props: IProps): JSX.Element => { const { name, idName, @@ -99,6 +100,7 @@ const BaseAvatar = forwardRef((props, ref) => { className, type = "round", altText = _t("common|avatar"), + ref, ...otherProps } = props; @@ -134,7 +136,7 @@ const BaseAvatar = forwardRef((props, ref) => { data-testid="avatar-img" /> ); -}); +}; export default BaseAvatar; export type BaseAvatarType = React.FC; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index b1093e3901..a62594f038 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.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 JSX, forwardRef, type ReactNode, type Ref, useContext } from "react"; +import React, { type JSX, type ReactNode, type Ref, useContext } from "react"; import { type RoomMember, type ResizeMethod } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; @@ -33,21 +33,20 @@ interface IProps extends Omit, "name" | forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false. hideTitle?: boolean; children?: ReactNode; + ref?: Ref; } -function MemberAvatar( - { - size, - resizeMethod = "crop", - viewUserOnClick, - forceHistorical, - fallbackUserId, - hideTitle, - member: propsMember, - ...props - }: IProps, - ref: Ref, -): JSX.Element { +export default function MemberAvatar({ + size, + resizeMethod = "crop", + viewUserOnClick, + forceHistorical, + fallbackUserId, + hideTitle, + member: propsMember, + ref, + ...props +}: IProps): JSX.Element { const cli = useContext(MatrixClientContext); const card = useContext(CardContext); @@ -101,5 +100,3 @@ function MemberAvatar( /> ); } - -export default forwardRef(MemberAvatar); diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index b5d13bf8e7..a3e8a57c47 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -10,8 +10,6 @@ import React, { type JSX, type ComponentProps, type ComponentPropsWithoutRef, - forwardRef, - type FunctionComponent, type ReactElement, type KeyboardEvent, type Ref, @@ -100,6 +98,8 @@ type Props = { * Whether the tooltip should be disabled. */ disableTooltip?: TooltipProps["disabled"]; + + ref?: Ref; }; export type ButtonProps = Props & Omit, keyof Props>; @@ -119,28 +119,26 @@ type RenderedElementProps = React.InputHTMLAttributes( - { - element, - onClick, - children, - kind, - disabled, - className, - onKeyDown, - onKeyUp, - triggerOnMouseDown, - title, - caption, - placement = "right", - onTooltipOpenChange, - disableTooltip, - role = "button", - tabIndex = 0, - ...restProps - }: ButtonProps, - ref: Ref, -): JSX.Element { +const AccessibleButton = function AccessibleButton({ + element, + onClick, + children, + kind, + disabled, + className, + onKeyDown, + onKeyUp, + triggerOnMouseDown, + title, + caption, + placement = "right", + onTooltipOpenChange, + disableTooltip, + role = "button", + tabIndex = 0, + ref, + ...restProps +}: ButtonProps): JSX.Element { const newProps = { ...restProps, tabIndex, @@ -226,10 +224,7 @@ const AccessibleButton = forwardRef(function { ref?: Ref; diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx index 13faa8cb68..8502580c2e 100644 --- a/src/components/views/location/Marker.tsx +++ b/src/components/views/location/Marker.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, { type ReactNode, useState } from "react"; +import React, { type JSX, type ReactNode, type Ref, useState } from "react"; import classNames from "classnames"; import { type RoomMember } from "matrix-js-sdk/src/matrix"; import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; @@ -21,6 +21,7 @@ interface Props { // use member text color as background useMemberColor?: boolean; tooltip?: ReactNode; + ref?: Ref; } /** @@ -55,7 +56,7 @@ const OptionalTooltip: React.FC<{ /** * Generic location marker */ -const Marker = React.forwardRef(({ id, roomMember, useMemberColor, tooltip }, ref) => { +const Marker = ({ id, roomMember, useMemberColor, tooltip, ref }: Props): JSX.Element => { const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : ""; return (
    (({ id, roomMember, useMem
    ); -}); +}; export default Marker; diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index b3185921bb..f8efd8381e 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.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, { forwardRef, useCallback, useContext, useMemo } from "react"; +import React, { type Ref, useCallback, useContext, useMemo, type JSX } from "react"; import type { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { ConnectionState, type ElementCall } from "../../../models/Call"; @@ -37,60 +37,69 @@ interface ActiveCallEventProps { buttonKind: AccessibleButtonKind; buttonDisabledTooltip?: string; onButtonClick: ((ev: ButtonEvent) => void) | null; + ref?: Ref; } -const ActiveCallEvent = forwardRef( - ({ mxEvent, call, participatingMembers, buttonText, buttonKind, buttonDisabledTooltip, onButtonClick }, ref) => { - const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]); +const ActiveCallEvent = ({ + mxEvent, + call, + participatingMembers, + buttonText, + buttonKind, + buttonDisabledTooltip, + onButtonClick, + ref, +}: ActiveCallEventProps): JSX.Element => { + const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]); - const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]); - const facePileOverflow = participatingMembers.length > facePileMembers.length; + const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]); + const facePileOverflow = participatingMembers.length > facePileMembers.length; - return ( -
    -
    - -
    -
    - - {_t("timeline|m.call|video_call_started_text", { name: senderName })} - - - -
    - {call && } - - {buttonText} - + return ( +
    +
    + +
    +
    + + {_t("timeline|m.call|video_call_started_text", { name: senderName })} + + +
    + {call && } + + {buttonText} +
    - ); - }, -); +
    + ); +}; interface ActiveLoadedCallEventProps { mxEvent: MatrixEvent; call: ElementCall; + ref?: Ref; } -const ActiveLoadedCallEvent = forwardRef(({ mxEvent, call }, ref) => { +const ActiveLoadedCallEvent = ({ mxEvent, call, ref }: ActiveLoadedCallEventProps): JSX.Element => { const connectionState = useConnectionState(call); const participatingMembers = useParticipatingMembers(call); const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call); @@ -141,16 +150,17 @@ const ActiveLoadedCallEvent = forwardRef(({ mxE onButtonClick={onButtonClick} /> ); -}); +}; interface CallEventProps { mxEvent: MatrixEvent; + ref?: Ref; } /** * An event tile representing an active or historical Element call. */ -export const CallEvent = forwardRef(({ mxEvent }, ref) => { +export const CallEvent = ({ mxEvent, ref }: CallEventProps): JSX.Element => { const client = useContext(MatrixClientContext); const call = useCall(mxEvent.getRoomId()!); const latestEvent = client @@ -187,4 +197,4 @@ export const CallEvent = forwardRef(({ mxEvent }, ref) => { } return ; -}); +}; diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 1158749571..f75a7c48f8 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { forwardRef, type ForwardRefExoticComponent, useContext } from "react"; +import React, { type JSX, useContext } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -16,7 +16,7 @@ import { _t } from "../../../languageHandler"; import { type IBodyProps } from "./IBodyProps"; import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; -function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element { +function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | JSX.Element { switch (mxEvent.decryptionFailureReason) { case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: return _t("timeline|decryption_failure|blocked"); @@ -72,7 +72,7 @@ function errorClassName(mxEvent: MatrixEvent): string | null { } // A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = forwardRef(({ mxEvent }, ref): React.JSX.Element => { +export const DecryptionFailureBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { const verificationState = useContext(LocalDeviceVerificationStateContext); const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent)); @@ -81,4 +81,4 @@ export const DecryptionFailureBody = forwardRef(({ m {getErrorMessage(mxEvent, verificationState)}
    ); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index e612a5be4d..5c5f1f0dc2 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.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, { type JSX, forwardRef } from "react"; +import React, { type JSX, type Ref, type ReactNode } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; @@ -22,9 +22,10 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; interface IProps { mxEvent: MatrixEvent; timestamp?: JSX.Element; + ref?: Ref; } -const EncryptionEvent = forwardRef(({ mxEvent, timestamp }, ref) => { +const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => { const cli = useMatrixClientContext(); const roomId = mxEvent.getRoomId()!; const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined); @@ -80,6 +81,6 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp timestamp={timestamp} /> ); -}); +}; export default EncryptionEvent; diff --git a/src/components/views/messages/EventContentBody.tsx b/src/components/views/messages/EventContentBody.tsx index 66ff7915ea..fce5428e8c 100644 --- a/src/components/views/messages/EventContentBody.tsx +++ b/src/components/views/messages/EventContentBody.tsx @@ -5,7 +5,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, { memo, forwardRef, useContext, useMemo } from "react"; +import React, { memo, useContext, useMemo, type Ref } from "react"; import { type IContent, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix"; import parse from "html-react-parser"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; @@ -139,6 +139,7 @@ interface Props extends ReplacerOptions { * Whether to include the `dir="auto"` attribute on the rendered element */ includeDir?: boolean; + ref?: Ref; } /** @@ -148,52 +149,50 @@ interface Props extends ReplacerOptions { * Returns a div or span depending on `as`, the `dir` on a `div` is always set to `"auto"` but set by `includeDir` otherwise. */ const EventContentBody = memo( - forwardRef( - ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => { - const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); - const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId()); + ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => { + const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); + const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId()); - const replacer = useReplacer(content, mxEvent, options); - const linkifyOptions = useMemo( - () => ({ - render: replacerToRenderFunction(replacer), + const replacer = useReplacer(content, mxEvent, options); + const linkifyOptions = useMemo( + () => ({ + render: replacerToRenderFunction(replacer), + }), + [replacer], + ); + + const isEmote = content.msgtype === MsgType.Emote; + + const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo( + () => + bodyToNode(content, highlights, { + disableBigEmoji: isEmote || !enableBigEmoji, + // Part of Replies fallback support + stripReplyFallback: stripReply, + mediaIsVisible, }), - [replacer], - ); + [content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply], + ); - const isEmote = content.msgtype === MsgType.Emote; + if (as === "div") includeDir = true; // force dir="auto" on divs - const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo( - () => - bodyToNode(content, highlights, { - disableBigEmoji: isEmote || !enableBigEmoji, - // Part of Replies fallback support - stripReplyFallback: stripReply, - mediaIsVisible, - }), - [content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply], - ); + const As = as; + const body = formattedBody ? ( + + {parse(formattedBody, { + replace: replacer, + })} + + ) : ( + + {applyReplacerOnString(emojiBodyElements || strippedBody, replacer)} + + ); - if (as === "div") includeDir = true; // force dir="auto" on divs + if (!linkify) return body; - const As = as; - const body = formattedBody ? ( - - {parse(formattedBody, { - replace: replacer, - })} - - ) : ( - - {applyReplacerOnString(emojiBodyElements || strippedBody, replacer)} - - ); - - if (!linkify) return body; - - return {body}; - }, - ), + return {body}; + }, ); export default EventContentBody; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index 56fbc83b86..4569115c0d 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.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, { type JSX, forwardRef, type ReactNode } from "react"; +import React, { type JSX, type ReactNode, type Ref } from "react"; import classNames from "classnames"; interface IProps { @@ -15,19 +15,18 @@ interface IProps { timestamp?: JSX.Element; subtitle?: ReactNode; children?: JSX.Element; + ref?: Ref; } -const EventTileBubble = forwardRef( - ({ className, title, timestamp, subtitle, children }, ref) => { - return ( -
    -
    {title}
    - {subtitle &&
    {subtitle}
    } - {children} - {timestamp} -
    - ); - }, -); +const EventTileBubble = ({ className, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => { + return ( +
    +
    {title}
    + {subtitle &&
    {subtitle}
    } + {children} + {timestamp} +
    + ); +}; export default EventTileBubble; diff --git a/src/components/views/messages/HiddenBody.tsx b/src/components/views/messages/HiddenBody.tsx index 9cabbe43dc..20410017be 100644 --- a/src/components/views/messages/HiddenBody.tsx +++ b/src/components/views/messages/HiddenBody.tsx @@ -6,23 +6,18 @@ 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 from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { type JSX } from "react"; import { _t } from "../../../languageHandler"; import { type IBodyProps } from "./IBodyProps"; -interface IProps { - mxEvent: MatrixEvent; -} - /** * A message hidden from the user pending moderation. * * Note: This component must not be used when the user is the author of the message * or has a sufficient powerlevel to see the message. */ -const HiddenBody = React.forwardRef(({ mxEvent }, ref) => { +const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { let text; const visibility = mxEvent.messageVisibility(); switch (visibility.visible) { @@ -42,6 +37,6 @@ const HiddenBody = React.forwardRef(({ mxEvent }, ref) {text} ); -}); +}; export default HiddenBody; diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index 59f5030c75..37aae37de6 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -6,7 +6,6 @@ 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 LegacyRef } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type React from "react"; @@ -44,7 +43,7 @@ export interface IBodyProps { // helper function to access relations for this event getRelationsForEvent?: GetRelationsForEvent; - ref?: React.RefObject | LegacyRef; + ref?: React.RefObject; // Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order. // This may be useful when displaying a preview of the event. diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index 0b9528db50..48294296be 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.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, { type JSX, type ForwardRefExoticComponent, useCallback, useContext, useEffect, useState } from "react"; +import React, { type JSX, useCallback, useContext, useEffect, useState } from "react"; import { type Beacon, BeaconEvent, @@ -122,7 +122,7 @@ const useHandleBeaconRedaction = ( }, [event, onBeforeBeaconInfoRedaction]); }; -const MBeaconBody = React.forwardRef(({ mxEvent, getRelationsForEvent }, ref) => { +const MBeaconBody = ({ mxEvent, getRelationsForEvent, ref }: IBodyProps): JSX.Element => { const { beacon, isLive, latestLocationState, waitingToStart } = useBeaconState(mxEvent); const mapId = useUniqueId(mxEvent.getId()!); @@ -225,6 +225,6 @@ const MBeaconBody = React.forwardRef(({ mxEvent, get )}
    ); -}) as ForwardRefExoticComponent; +}; export default MBeaconBody; diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index a3a42a7843..95f8a53f2a 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.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, { useEffect, useState, useContext, type ForwardRefExoticComponent } from "react"; +import React, { useEffect, useState, useContext, type JSX } from "react"; import { MatrixEvent, M_TEXT } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +85,7 @@ const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent; return { pollStartEvent, isLoadingPollStartEvent }; }; -export const MPollEndBody = React.forwardRef(({ mxEvent, ...props }, ref) => { +export const MPollEndBody = ({ mxEvent, ref, ...props }: IBodyProps): JSX.Element => { const cli = useMatrixClientContext(); const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); @@ -105,4 +105,4 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro
    ); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index 9048d176ca..ddad4d2e4d 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.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, { type ForwardRefExoticComponent, useContext } from "react"; +import React, { useContext, type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; @@ -15,7 +15,7 @@ import { formatFullDate } from "../../../DateUtils"; import SettingsStore from "../../../settings/SettingsStore"; import { type IBodyProps } from "./IBodyProps"; -const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { +const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { const cli: MatrixClient = useContext(MatrixClientContext); let text = _t("timeline|self_redaction"); const unsigned = mxEvent.getUnsigned(); @@ -37,6 +37,6 @@ const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { {text} ); -}) as ForwardRefExoticComponent; +}; export default RedactedBody; diff --git a/src/components/views/messages/UnknownBody.tsx b/src/components/views/messages/UnknownBody.tsx index e72f8d2327..26e4ea5fca 100644 --- a/src/components/views/messages/UnknownBody.tsx +++ b/src/components/views/messages/UnknownBody.tsx @@ -7,16 +7,15 @@ 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, { forwardRef, type ForwardRefExoticComponent } from "react"; +import React, { type JSX } from "react"; import { type IBodyProps } from "./IBodyProps"; -export default forwardRef(({ mxEvent, children }, ref) => { +export default ({ mxEvent, ref }: IBodyProps): JSX.Element => { const text = mxEvent.getContent().body; return (
    {text} - {children}
    ); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 90534947a0..a564a60695 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.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, { forwardRef, type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react"; +import React, { type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react"; import classNames from "classnames"; import { IconButton, Text } from "@vector-im/compound-web"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; @@ -44,106 +44,102 @@ function closeRightPanel(ev: MouseEvent): void { RightPanelStore.instance.popCard(); } -const BaseCard: React.FC = forwardRef( - ( - { - closeLabel, - onClose, - onBack, - className, - id, - ariaLabelledBy, - role, - hideHeaderButtons, - header, - footer, - withoutScrollContainer, - children, - onKeyDown, - closeButtonRef, - }, - ref, - ) => { - let backButton; - const cardHistory = RightPanelStore.instance.roomPhaseHistory; - if (cardHistory.length > 1 && !hideHeaderButtons) { - const prevCard = cardHistory[cardHistory.length - 2]; - const onBackClick = (ev: MouseEvent): void => { - onBack?.(ev); - RightPanelStore.instance.popCard(); - }; - const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); - backButton = ( - - - - ); - } - - let closeButton; - if (!hideHeaderButtons) { - closeButton = ( - - - - ); - } - - if (!withoutScrollContainer) { - children = {children}; - } - - const shouldRenderHeader = header || !hideHeaderButtons; - - return ( - -
    - {shouldRenderHeader && ( -
    - {backButton} - {typeof header === "string" ? ( -
    - - {header} - -
    - ) : ( - (header ??
    ) - )} - {closeButton} -
    - )} - {children} - {footer &&
    {footer}
    } -
    - +const BaseCard: React.FC = ({ + closeLabel, + onClose, + onBack, + className, + id, + ariaLabelledBy, + role, + hideHeaderButtons, + header, + footer, + withoutScrollContainer, + children, + onKeyDown, + closeButtonRef, + ref, +}: IProps) => { + let backButton; + const cardHistory = RightPanelStore.instance.roomPhaseHistory; + if (cardHistory.length > 1 && !hideHeaderButtons) { + const prevCard = cardHistory[cardHistory.length - 2]; + const onBackClick = (ev: MouseEvent): void => { + onBack?.(ev); + RightPanelStore.instance.popCard(); + }; + const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); + backButton = ( + + + ); - }, -); + } + + let closeButton; + if (!hideHeaderButtons) { + closeButton = ( + + + + ); + } + + if (!withoutScrollContainer) { + children = {children}; + } + + const shouldRenderHeader = header || !hideHeaderButtons; + + return ( + +
    + {shouldRenderHeader && ( +
    + {backButton} + {typeof header === "string" ? ( +
    + + {header} + +
    + ) : ( + (header ??
    ) + )} + {closeButton} +
    + )} + {children} + {footer &&
    {footer}
    } +
    + + ); +}; export default BaseCard; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 7baa127f16..2c10d0afd9 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.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, { createRef, forwardRef, type JSX, type MouseEvent, type ReactNode } from "react"; +import React, { createRef, type JSX, type Ref, type MouseEvent, type ReactNode } from "react"; import classNames from "classnames"; import { EventStatus, @@ -228,6 +228,8 @@ export interface EventTileProps { // The following properties are used by EventTilePreview to disable tab indexes within the event tile hideTimestamp?: boolean; inhibitInteraction?: boolean; + + ref?: Ref; } interface IState { @@ -1482,15 +1484,13 @@ export class UnwrappedEventTile extends React.Component } // Wrap all event tiles with the tile error boundary so that any throws even during construction are captured -const SafeEventTile = forwardRef((props, ref) => { +const SafeEventTile = (props: EventTileProps): JSX.Element => { return ( - <> - - - - + + + ); -}); +}; export default SafeEventTile; function E2ePadlockUnencrypted(props: Omit): JSX.Element { diff --git a/src/components/views/rooms/MemberList/MemberListView.tsx b/src/components/views/rooms/MemberList/MemberListView.tsx index dd04a89209..0b5629685c 100644 --- a/src/components/views/rooms/MemberList/MemberListView.tsx +++ b/src/components/views/rooms/MemberList/MemberListView.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { Form } from "@vector-im/compound-web"; -import React from "react"; +import React, { type JSX } from "react"; import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List"; import { AutoSizer } from "react-virtualized"; @@ -33,7 +33,7 @@ const MemberListView: React.FC = (props: IProps) => { const totalRows = vm.members.length; - const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => { + const getRowComponent = (item: MemberWithSeparator): JSX.Element => { if (item === SEPARATOR) { return
    ; } else if (item.member) { @@ -64,7 +64,7 @@ const MemberListView: React.FC = (props: IProps) => { } }; - const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { + const rowRenderer = ({ key, index, style }: ListRowProps): JSX.Element => { if (index === totalRows) { // We've rendered all the members, // now we render an empty div to add some space to the end of the list. diff --git a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx index c25d40fc58..359bb74b23 100644 --- a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx @@ -5,7 +5,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 from "react"; +import React, { type JSX } from "react"; import { Tooltip } from "@vector-im/compound-web"; import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; @@ -14,7 +14,7 @@ import { _t } from "../../../../../../languageHandler"; import { E2EStatus } from "../../../../../../utils/ShieldUtils"; import { crossSigningUserTitles } from "../../../E2EIcon"; -function getIconFromStatus(status: E2EStatus): React.JSX.Element | undefined { +function getIconFromStatus(status: E2EStatus): JSX.Element | undefined { switch (status) { case E2EStatus.Normal: return undefined; diff --git a/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx index 855aecf626..ac31084adf 100644 --- a/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx @@ -5,7 +5,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 from "react"; +import React, { type JSX } from "react"; import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8"; import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8"; import DNDIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8"; @@ -19,7 +19,7 @@ interface Props { export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy"); -function getIconForPresenceState(state: string): React.JSX.Element { +function getIconForPresenceState(state: string): JSX.Element { switch (state) { case "online": return ; diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index c324e8ccbd..8e44a0a3b9 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.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, { forwardRef } from "react"; +import React, { type Ref, type JSX, type ReactNode } from "react"; import classNames from "classnames"; import { formatCount } from "../../../../utils/FormattingUtils"; @@ -26,6 +26,8 @@ interface Props { * for the difference between the two. */ forceDot?: boolean; + children?: ReactNode; + ref?: Ref; } interface ClickableProps extends Props { @@ -45,61 +47,66 @@ interface ClickableProps extends Props { * notifications in the room list, it may have a green badge with the number of unread notifications, * but somewhere else it may just have a green dot as a more compact representation of the same information. */ -export const StatelessNotificationBadge = forwardRef>( - ({ symbol, count, level, knocked, forceDot = false, ...props }, ref) => { - const hideBold = useSettingValue("feature_hidebold"); +export const StatelessNotificationBadge = ({ + symbol, + count, + level, + knocked, + forceDot = false, + ...props +}: XOR): JSX.Element => { + const hideBold = useSettingValue("feature_hidebold"); - // Don't show a badge if we don't need to - if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) { - return <>; - } + // Don't show a badge if we don't need to + if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) { + return <>; + } - const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol); + const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol); - const isEmptyBadge = symbol === null && count === 0; + const isEmptyBadge = symbol === null && count === 0; - if (symbol === null && count > 0) { - symbol = formatCount(count); - } + if (symbol === null && count > 0) { + symbol = formatCount(count); + } - // We show a dot if either: - // * The props force us to, or - // * It's just an activity-level notification or (in theory) lower and the room isn't knocked - const badgeType = - forceDot || (level <= NotificationLevel.Activity && !knocked) - ? "dot" - : !symbol || symbol.length < 3 - ? "badge_2char" - : "badge_3char"; + // We show a dot if either: + // * The props force us to, or + // * It's just an activity-level notification or (in theory) lower and the room isn't knocked + const badgeType = + forceDot || (level <= NotificationLevel.Activity && !knocked) + ? "dot" + : !symbol || symbol.length < 3 + ? "badge_2char" + : "badge_3char"; - const classes = classNames({ - "mx_NotificationBadge": true, - "mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount, - "mx_NotificationBadge_level_notification": level == NotificationLevel.Notification, - "mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight, - "mx_NotificationBadge_knocked": knocked, + const classes = classNames({ + "mx_NotificationBadge": true, + "mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount, + "mx_NotificationBadge_level_notification": level == NotificationLevel.Notification, + "mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight, + "mx_NotificationBadge_knocked": knocked, - // Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char - "mx_NotificationBadge_dot": badgeType === "dot", - "mx_NotificationBadge_2char": badgeType === "badge_2char", - "mx_NotificationBadge_3char": badgeType === "badge_3char", - // Badges with text should always use light colors - "cpd-theme-light": badgeType !== "dot", - }); - - if (props.onClick) { - return ( - - {symbol} - {props.children} - - ); - } + // Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char + "mx_NotificationBadge_dot": badgeType === "dot", + "mx_NotificationBadge_2char": badgeType === "badge_2char", + "mx_NotificationBadge_3char": badgeType === "badge_3char", + // Badges with text should always use light colors + "cpd-theme-light": badgeType !== "dot", + }); + if (props.onClick) { return ( -
    + {symbol} -
    + {props.children} + ); - }, -); + } + + return ( +
    + {symbol} +
    + ); +}; diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index 5f97e44378..984b43d489 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, forwardRef, type JSX, useState } from "react"; +import React, { type ComponentProps, type JSX, type Ref, useState } from "react"; import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web"; import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read"; import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread"; @@ -147,22 +147,22 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element ); } -interface MoreOptionsButtonProps extends ComponentProps {} +interface MoreOptionsButtonProps extends ComponentProps { + ref?: Ref; +} /** * A button to trigger the more options menu. */ -export const MoreOptionsButton = forwardRef( - function MoreOptionsButton(props, ref) { - return ( - - - - - - ); - }, -); +export const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element { + return ( + + + + + + ); +}; interface NotificationMenuProps { /** @@ -238,15 +238,17 @@ interface NotificationButtonProps extends ComponentProps { * Whether the room is muted. */ isRoomMuted: boolean; + ref?: Ref; } /** * A button to trigger the notification menu. */ -export const NotificationButton = forwardRef(function MoreOptionsButton( - { isRoomMuted, ...props }, +export const NotificationButton = function MoreOptionsButton({ + isRoomMuted, ref, -) { + ...props +}: NotificationButtonProps): JSX.Element { return ( @@ -254,4 +256,4 @@ export const NotificationButton = forwardRef ); -}); +}; diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 2ea87fc68e..c84f130f34 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.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, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react"; +import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react"; import classNames from "classnames"; import type EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -21,15 +21,13 @@ import { type ComposerFunctions } from "./types"; interface ContentProps { disabled?: boolean; composerFunctions: ComposerFunctions; + ref?: RefObject; } -const Content = forwardRef(function Content( - { disabled = false, composerFunctions }: ContentProps, - forwardRef: ForwardedRef, -) { - useWysiwygEditActionHandler(disabled, forwardRef as RefObject, composerFunctions); +const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode { + useWysiwygEditActionHandler(disabled, ref, composerFunctions); return null; -}); +}; interface EditWysiwygComposerProps { disabled?: boolean; diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index fb399dda28..9b21eef545 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.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, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react"; +import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react"; import { type IEventRelation } from "matrix-js-sdk/src/matrix"; import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler"; @@ -22,15 +22,13 @@ import { ComposerContext, getDefaultContextValue } from "./ComposerContext"; interface ContentProps { disabled?: boolean; composerFunctions: ComposerFunctions; + ref?: RefObject; } -const Content = forwardRef(function Content( - { disabled = false, composerFunctions }: ContentProps, - forwardRef: ForwardedRef, -) { - useWysiwygSendActionHandler(disabled, forwardRef as RefObject, composerFunctions); +const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode { + useWysiwygSendActionHandler(disabled, ref, composerFunctions); return null; -}); +}; export interface SendWysiwygComposerProps { initialContent?: string; diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index d46318af04..d418a0df19 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type CSSProperties, forwardRef, memo, type RefObject, type ReactNode } from "react"; +import React, { type CSSProperties, memo, type RefObject, type ReactNode } from "react"; import { useIsExpanded } from "../hooks/useIsExpanded"; import { useSelection } from "../hooks/useSelection"; @@ -19,44 +19,36 @@ interface EditorProps { placeholder?: string; leftComponent?: ReactNode; rightComponent?: ReactNode; + ref?: RefObject; } -export const Editor = memo( - forwardRef(function Editor( - { disabled, placeholder, leftComponent, rightComponent }: EditorProps, - ref, - ) { - const isExpanded = useIsExpanded(ref as RefObject, HEIGHT_BREAKING_POINT); - const { onFocus, onBlur, onInput } = useSelection(); +export const Editor = memo(function Editor({ disabled, placeholder, leftComponent, rightComponent, ref }: EditorProps) { + const isExpanded = useIsExpanded(ref, HEIGHT_BREAKING_POINT); + const { onFocus, onBlur, onInput } = useSelection(); - return ( -
    - {leftComponent} -
    -
    -
    - {rightComponent} + return ( +
    + {leftComponent} +
    +
    - ); - }), -); + {rightComponent} +
    + ); +}); diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 56ac579035..9446a32a7b 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.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, { type JSX, type ForwardedRef, forwardRef, type FunctionComponent } from "react"; +import React, { type JSX, type Ref, type FunctionComponent } from "react"; import { type FormattingFunctions, type MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { logger } from "matrix-js-sdk/src/logger"; @@ -39,6 +39,8 @@ interface WysiwygAutocompleteProps { * Handler purely for the at-room mentions special case */ handleAtRoomMention: FormattingFunctions["mentionAtRoom"]; + + ref?: Ref; } /** @@ -48,69 +50,70 @@ interface WysiwygAutocompleteProps { * * @param props.ref - the ref will be attached to the rendered `` component */ -const WysiwygAutocomplete = forwardRef( - ( - { suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps, - ref: ForwardedRef, - ): JSX.Element | null => { - const { room } = useScopedRoomContext("room"); - const client = useMatrixClientContext(); +const WysiwygAutocomplete = ({ + suggestion, + handleMention, + handleCommand, + handleAtRoomMention, + ref, +}: WysiwygAutocompleteProps): JSX.Element | null => { + const { room } = useScopedRoomContext("room"); + const client = useMatrixClientContext(); - function handleConfirm(completion: ICompletion): void { - if (client === undefined || room === undefined) { - return; - } - - switch (completion.type) { - case "command": { - // TODO determine if utils in SlashCommands.tsx are required. - // Trim the completion as some include trailing spaces, but we always insert a - // trailing space in the rust model anyway - handleCommand(completion.completion.trim()); - return; - } - case "at-room": { - handleAtRoomMention(getMentionAttributes(completion, client, room)); - return; - } - case "room": - case "user": { - if (typeof completion.href === "string") { - handleMention( - completion.href, - getMentionDisplayText(completion, client), - getMentionAttributes(completion, client, room), - ); - } - return; - } - // TODO - handle "community" type - default: - return; - } + function handleConfirm(completion: ICompletion): void { + if (client === undefined || room === undefined) { + return; } - if (!room) return null; + switch (completion.type) { + case "command": { + // TODO determine if utils in SlashCommands.tsx are required. + // Trim the completion as some include trailing spaces, but we always insert a + // trailing space in the rust model anyway + handleCommand(completion.completion.trim()); + return; + } + case "at-room": { + handleAtRoomMention(getMentionAttributes(completion, client, room)); + return; + } + case "room": + case "user": { + if (typeof completion.href === "string") { + handleMention( + completion.href, + getMentionDisplayText(completion, client), + getMentionAttributes(completion, client, room), + ); + } + return; + } + // TODO - handle "community" type + default: + return; + } + } - const autoCompleteQuery = buildQuery(suggestion); - // debug for https://github.com/vector-im/element-web/issues/26037 - logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`); + if (!room) return null; - // TODO - determine if we show all of the /command suggestions, there are some options in the - // list which don't seem to make sense in this context, specifically /html and /plain - return ( -
    - -
    - ); - }, -); + const autoCompleteQuery = buildQuery(suggestion); + // debug for https://github.com/vector-im/element-web/issues/26037 + logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`); + + // TODO - determine if we show all of the /command suggestions, there are some options in the + // list which don't seem to make sense in this context, specifically /html and /plain + return ( +
    + +
    + ); +}; (WysiwygAutocomplete as FunctionComponent).displayName = "WysiwygAutocomplete"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts index 0041cac557..d5f9850277 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import { type RefObject, useEffect, useState } from "react"; -export function useIsExpanded(ref: RefObject, breakingPoint: number): boolean { +export function useIsExpanded(ref: RefObject | undefined, breakingPoint: number): boolean { const [isExpanded, setIsExpanded] = useState(false); useEffect(() => { - if (ref.current) { + if (ref?.current) { const editor = ref.current; const resizeObserver = new ResizeObserver((entries) => { requestAnimationFrame(() => { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index 68d38eebcd..c105996a05 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext. export function useWysiwygEditActionHandler( disabled: boolean, - composerElement: RefObject, + composerElement: RefObject | undefined, composerFunctions: ComposerFunctions, ): void { const roomContext = useScopedRoomContext("timelineRenderingType"); @@ -33,7 +33,7 @@ export function useWysiwygEditActionHandler( (payload: ActionPayload) => { // don't let the user into the composer if it is disabled - all of these branches lead // to the cursor being in the composer - if (disabled || !composerElement.current) return; + if (disabled || !composerElement?.current) return; const context = payload.context ?? TimelineRenderingType.Room; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 89379db1ad..fcab8f4547 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext. export function useWysiwygSendActionHandler( disabled: boolean, - composerElement: RefObject, + composerElement: RefObject | undefined, composerFunctions: ComposerFunctions, ): void { const roomContext = useScopedRoomContext("timelineRenderingType"); diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index 507fc31425..dfa188cf69 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -15,7 +15,7 @@ import AccessibleButton, { type ButtonProps } from "../../elements/AccessibleBut type Props = Omit< ButtonProps, - "aria-label" | "title" | "kind" | "className" | "element" + "aria-label" | "title" | "kind" | "className" | "element" | "ref" > & { isExpanded: boolean; }; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index c9a266e4b9..fd30091106 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.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, { type ForwardedRef, forwardRef } from "react"; +import React, { type JSX, type Ref } from "react"; import { type IPusher, PUSHER_DEVICE_ID, type LocalNotificationSettings } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; @@ -47,6 +47,7 @@ interface Props { * Changes sign out button to be a manage button */ delegatedAuthAccountUrl?: string; + ref?: Ref; } const isDeviceSelected = ( @@ -237,152 +238,148 @@ const DeviceListItem: React.FC<{ * Filtered list of devices * Sorted by latest activity descending */ -export const FilteredDeviceList = forwardRef( - ( - { - devices, - pushers, - localNotificationSettings, - filter, - expandedDeviceIds, - signingOutDeviceIds, - selectedDeviceIds, - onFilterChange, - onDeviceExpandToggle, - saveDeviceName, - onSignOutDevices, - onRequestDeviceVerification, - setPushNotifications, - setSelectedDeviceIds, - supportsMSC3881, - delegatedAuthAccountUrl, - }: Props, - ref: ForwardedRef, - ) => { - const sortedDevices = getFilteredSortedDevices(devices, filter); +export const FilteredDeviceList = ({ + devices, + pushers, + localNotificationSettings, + filter, + expandedDeviceIds, + signingOutDeviceIds, + selectedDeviceIds, + onFilterChange, + onDeviceExpandToggle, + saveDeviceName, + onSignOutDevices, + onRequestDeviceVerification, + setPushNotifications, + setSelectedDeviceIds, + supportsMSC3881, + delegatedAuthAccountUrl, + ref, +}: Props): JSX.Element => { + const sortedDevices = getFilteredSortedDevices(devices, filter); - function getPusherForDevice(device: ExtendedDevice): IPusher | undefined { - return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id); + function getPusherForDevice(device: ExtendedDevice): IPusher | undefined { + return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id); + } + + const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => { + if (isDeviceSelected(deviceId, selectedDeviceIds)) { + // remove from selection + setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId)); + } else { + setSelectedDeviceIds([...selectedDeviceIds, deviceId]); } + }; - const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => { - if (isDeviceSelected(deviceId, selectedDeviceIds)) { - // remove from selection - setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId)); - } else { - setSelectedDeviceIds([...selectedDeviceIds, deviceId]); - } - }; + const options: FilterDropdownOption[] = [ + { id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") }, + { + id: DeviceSecurityVariation.Verified, + label: _t("common|verified"), + description: _t("settings|sessions|filter_verified_description"), + }, + { + id: DeviceSecurityVariation.Unverified, + label: _t("common|unverified"), + description: _t("settings|sessions|filter_unverified_description"), + }, + { + id: DeviceSecurityVariation.Inactive, + label: _t("settings|sessions|filter_inactive"), + description: _t("settings|sessions|filter_inactive_description", { + inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS, + }), + }, + ]; - const options: FilterDropdownOption[] = [ - { id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") }, - { - id: DeviceSecurityVariation.Verified, - label: _t("common|verified"), - description: _t("settings|sessions|filter_verified_description"), - }, - { - id: DeviceSecurityVariation.Unverified, - label: _t("common|unverified"), - description: _t("settings|sessions|filter_unverified_description"), - }, - { - id: DeviceSecurityVariation.Inactive, - label: _t("settings|sessions|filter_inactive"), - description: _t("settings|sessions|filter_inactive_description", { - inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS, - }), - }, - ]; + const onFilterOptionChange = (filterId: DeviceFilterKey): void => { + onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation)); + }; - const onFilterOptionChange = (filterId: DeviceFilterKey): void => { - onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation)); - }; + const isAllSelected = selectedDeviceIds.length >= sortedDevices.length; + const toggleSelectAll = (): void => { + if (isAllSelected) { + setSelectedDeviceIds([]); + } else { + setSelectedDeviceIds(sortedDevices.map((device) => device.device_id)); + } + }; - const isAllSelected = selectedDeviceIds.length >= sortedDevices.length; - const toggleSelectAll = (): void => { - if (isAllSelected) { - setSelectedDeviceIds([]); - } else { - setSelectedDeviceIds(sortedDevices.map((device) => device.device_id)); - } - }; + const isSigningOut = !!signingOutDeviceIds.length; - const isSigningOut = !!signingOutDeviceIds.length; - - return ( -
    - - {selectedDeviceIds.length ? ( - <> - onSignOutDevices(selectedDeviceIds)} - className="mx_FilteredDeviceList_headerButton" - > - {isSigningOut && } - {_t("action|sign_out")} - - setSelectedDeviceIds([])} - className="mx_FilteredDeviceList_headerButton" - > - {_t("action|cancel")} - - - ) : ( - - id="device-list-filter" - label={_t("settings|sessions|filter_label")} - value={filter || ALL_FILTER_ID} - onOptionChange={onFilterOptionChange} - options={options} - selectedLabel={_t("action|show")} - /> - )} - - {!!sortedDevices.length ? ( - + return ( +
    + + {selectedDeviceIds.length ? ( + <> + onSignOutDevices(selectedDeviceIds)} + className="mx_FilteredDeviceList_headerButton" + > + {isSigningOut && } + {_t("action|sign_out")} + + setSelectedDeviceIds([])} + className="mx_FilteredDeviceList_headerButton" + > + {_t("action|cancel")} + + ) : ( - onFilterChange(undefined)} /> + + id="device-list-filter" + label={_t("settings|sessions|filter_label")} + value={filter || ALL_FILTER_ID} + onOptionChange={onFilterOptionChange} + options={options} + selectedLabel={_t("action|show")} + /> )} -
      - {sortedDevices.map((device) => ( - onDeviceExpandToggle(device.device_id)} - onSignOutDevice={() => onSignOutDevices([device.device_id])} - saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)} - onRequestDeviceVerification={ - onRequestDeviceVerification - ? () => onRequestDeviceVerification(device.device_id) - : undefined - } - setPushNotifications={setPushNotifications} - toggleSelected={() => toggleSelection(device.device_id)} - supportsMSC3881={supportsMSC3881} - delegatedAuthAccountUrl={delegatedAuthAccountUrl} - /> - ))} -
    -
    - ); - }, -); + + {!!sortedDevices.length ? ( + + ) : ( + onFilterChange(undefined)} /> + )} +
      + {sortedDevices.map((device) => ( + onDeviceExpandToggle(device.device_id)} + onSignOutDevice={() => onSignOutDevices([device.device_id])} + saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)} + onRequestDeviceVerification={ + onRequestDeviceVerification + ? () => onRequestDeviceVerification(device.device_id) + : undefined + } + setPushNotifications={setPushNotifications} + toggleSelected={() => toggleSelection(device.device_id)} + supportsMSC3881={supportsMSC3881} + delegatedAuthAccountUrl={delegatedAuthAccountUrl} + /> + ))} +
    +
    + ); +}; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 28f56a072c..d03ac8a1e2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -45,7 +45,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; type ButtonProps = Omit< AccessibleButtonProps, - "title" | "onClick" | "size" | "element" + "title" | "onClick" | "size" | "element" | "ref" > & { space?: Room; spaceKey?: SpaceKey; diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx index bb04df275b..9d43655c60 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, forwardRef } from "react"; +import React, { type ComponentProps, type Ref, type JSX } from "react"; import ThreadsSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid"; import classNames from "classnames"; import { IconButton, Text, Tooltip } from "@vector-im/compound-web"; @@ -28,44 +28,46 @@ interface ThreadsActivityCentreButtonProps extends ComponentProps; } /** * A button to open the thread activity centre. */ -export const ThreadsActivityCentreButton = forwardRef( - function ThreadsActivityCentreButton( - { displayLabel, notificationLevel, disableTooltip, ...props }, - ref, - ): React.JSX.Element { - // Disable tooltip when the label is displayed - const openTooltip = disableTooltip || displayLabel ? false : undefined; +export const ThreadsActivityCentreButton = function ThreadsActivityCentreButton({ + displayLabel, + notificationLevel, + disableTooltip, + ref, + ...props +}: ThreadsActivityCentreButtonProps): JSX.Element { + // Disable tooltip when the label is displayed + const openTooltip = disableTooltip || displayLabel ? false : undefined; - return ( - - - <> - - {/* This is dirty, but we need to add the label to the indicator icon */} - {displayLabel && ( - - {_t("common|threads")} - - )} - - - - ); - }, -); + return ( + + + <> + + {/* This is dirty, but we need to add the label to the indicator icon */} + {displayLabel && ( + + {_t("common|threads")} + + )} + + + + ); +}; diff --git a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx index d9fc85c943..0e81b5278e 100644 --- a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx +++ b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx @@ -8,7 +8,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, useState, forwardRef } from "react"; +import React, { createRef, useState, type Ref, type FC } from "react"; import classNames from "classnames"; import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; @@ -41,31 +41,40 @@ type ButtonProps = Omit, "title" | "element"> & { offLabel?: string; forceHide?: boolean; onHover?: (hovering: boolean) => void; + ref?: Ref; }; -const LegacyCallViewToggleButton = forwardRef( - ({ children, state: isOn, className, onLabel, offLabel, forceHide, onHover, ...props }, ref) => { - const classes = classNames("mx_LegacyCallViewButtons_button", className, { - mx_LegacyCallViewButtons_button_on: isOn, - mx_LegacyCallViewButtons_button_off: !isOn, - }); +const LegacyCallViewToggleButton: FC = ({ + children, + state: isOn, + className, + onLabel, + offLabel, + forceHide, + onHover, + ref, + ...props +}) => { + const classes = classNames("mx_LegacyCallViewButtons_button", className, { + mx_LegacyCallViewButtons_button_on: isOn, + mx_LegacyCallViewButtons_button_off: !isOn, + }); - const title = forceHide ? undefined : isOn ? onLabel : offLabel; + const title = forceHide ? undefined : isOn ? onLabel : offLabel; - return ( - - {children} - - ); - }, -); + return ( + + {children} + + ); +}; interface IDropdownButtonProps extends ButtonProps { deviceKinds: MediaDeviceKindEnum[]; diff --git a/src/contexts/MatrixClientContext.tsx b/src/contexts/MatrixClientContext.tsx index 0810465789..1783c9a12f 100644 --- a/src/contexts/MatrixClientContext.tsx +++ b/src/contexts/MatrixClientContext.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, { type ComponentClass, createContext, forwardRef, useContext } from "react"; +import React, { type ComponentClass, createContext, useContext } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; // This context is available to components under LoggedInView, @@ -24,22 +24,16 @@ export function useMatrixClientContext(): MatrixClient { return useContext(MatrixClientContext); } -const matrixHOC = ( - ComposedComponent: ComponentClass, -): (( - props: Omit & React.RefAttributes>, -) => React.ReactElement | null) => { - type ComposedComponentInstance = InstanceType; - - // eslint-disable-next-line react-hooks/rules-of-hooks - - const TypedComponent = ComposedComponent; - - return forwardRef>((props, ref) => { +const matrixHOC = + ( + ComposedComponent: ComponentClass, + ): (( + props: Omit & React.RefAttributes>, + ) => React.ReactElement | null) => + (props) => { const client = useContext(MatrixClientContext); // @ts-ignore - return ; - }); -}; + return ; + }; export const withMatrixClientHOC = matrixHOC; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 12e9f869f3..f1fc224471 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -69,7 +69,7 @@ export interface EventTileTypeProps } type FactoryProps = Omit; -type Factory = (ref: Optional>, props: X) => JSX.Element; +type Factory = (ref: React.RefObject | undefined, props: X) => JSX.Element; export const MessageEventFactory: Factory = (ref, props) => ; const LegacyCallEventFactory: Factory = (ref, props) => ( diff --git a/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx index 16b9cdaf89..b4ab471cc0 100644 --- a/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx +++ b/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx @@ -11,8 +11,8 @@ import { act, fireEvent, screen, waitFor } from "jest-matrix-react"; import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { mocked } from "jest-mock"; +import { type JSX } from "react"; -import type React from "react"; import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { type Rendered, renderMemberList } from "./common"; @@ -21,7 +21,7 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({ shouldShowComponent: jest.fn(), })); -type Children = (args: { height: number; width: number }) => React.JSX.Element; +type Children = (args: { height: number; width: number }) => JSX.Element; jest.mock("react-virtualized", () => { const ReactVirtualized = jest.requireActual("react-virtualized"); return { diff --git a/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx index 5877416dae..240ad8a502 100644 --- a/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx +++ b/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx @@ -10,8 +10,8 @@ Please see LICENSE files in the repository root for full details. import { act } from "react"; import { waitFor } from "jest-matrix-react"; import { type Room, type RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { type JSX } from "react"; -import type React from "react"; import { filterConsole } from "../../../../../test-utils"; import { type Rendered, renderMemberList } from "./common"; @@ -19,7 +19,7 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({ shouldShowComponent: jest.fn(), })); -type Children = (args: { height: number; width: number }) => React.JSX.Element; +type Children = (args: { height: number; width: number }) => JSX.Element; jest.mock("react-virtualized", () => { const ReactVirtualized = jest.requireActual("react-virtualized"); return {