diff --git a/package.json b/package.json index fced3c505d..83f5761aec 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.5.0", "@matrix-org/matrix-wysiwyg": "^2.3.0", - "@matrix-org/react-sdk-module-api": "^0.0.5", + "@matrix-org/react-sdk-module-api": "^0.0.6", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/src/@types/common.ts b/src/@types/common.ts index 4aeea8a643..4ea9cff802 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { JSXElementConstructor } from "react"; +import { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = { [P in Exclude]?: never }; @@ -22,7 +22,6 @@ export type XOR = T | U extends object ? (Without & U) | (Without = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; -export type ReactAnyComponent = React.Component | React.ExoticComponent; // Utility type for string dot notation for accessing nested object properties // Based on https://stackoverflow.com/a/58436959 diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 3433560a17..e4fc4dee8e 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -46,7 +46,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; -import { ICollapseConfig } from "../../resizer/distributors/collapse"; +import { CollapseItem, ICollapseConfig } from "../../resizer/distributors/collapse"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { IOpts } from "../../createRoom"; import SpacePanel from "../views/spaces/SpacePanel"; @@ -134,7 +134,7 @@ class LoggedInView extends React.Component { protected layoutWatcherRef?: string; protected compactLayoutWatcherRef?: string; protected backgroundImageWatcherRef?: string; - protected resizer?: Resizer; + protected resizer?: Resizer; public constructor(props: IProps) { super(props); @@ -230,7 +230,7 @@ class LoggedInView extends React.Component { return this._roomView.current.canResetTimeline(); }; - private createResizer(): Resizer { + private createResizer(): Resizer { let panelSize: number | null; let panelCollapsed: boolean; const collapseConfig: ICollapseConfig = { diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index d0f2c362be..3218ff360a 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef, RefObject, useCallback, useContext, useEffect, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react"; import { ISearchResults } from "matrix-js-sdk/src/@types/search"; import { IThreadBundledRelationship } from "matrix-js-sdk/src/models/event"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; @@ -57,10 +57,7 @@ interface Props { // 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 }: Props, - ref: RefObject, - ) => { + ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props, ref) => { const client = useContext(MatrixClientContext); const roomContext = useContext(RoomContext); const [inProgress, setInProgress] = useState(true); @@ -69,6 +66,7 @@ export const RoomSearchView = forwardRef( const aborted = useRef(false); // A map from room ID to permalink creator const permalinkCreators = useRef(new Map()).current; + const innerRef = useRef(); useEffect(() => { return () => { @@ -214,8 +212,16 @@ export const RoomSearchView = forwardRef( // once dynamic content in the search results load, make the scrollPanel check // the scroll offsets. const onHeightChanged = (): void => { - const scrollPanel = ref.current; - scrollPanel?.checkScroll(); + innerRef.current?.checkScroll(); + }; + + 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; @@ -317,7 +323,7 @@ export const RoomSearchView = forwardRef( return ( { return this.divScroll; } - private collectScroll = (divScroll: HTMLDivElement): void => { + private collectScroll = (divScroll: HTMLDivElement | null): void => { this.divScroll = divScroll; }; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 94180f40eb..0c4d11d709 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -91,6 +91,9 @@ interface IAuthEntryProps { onPhaseChange: (phase: number) => void; submitAuthDict: (auth: IAuthDict) => void; requestEmailToken?: () => Promise; + fail: (error: Error) => void; + clientSecret: string; + showContinue: boolean; } interface IPasswordAuthEntryState { @@ -248,7 +251,6 @@ interface ITermsAuthEntryProps extends IAuthEntryProps { stageParams?: { policies?: Policies; }; - showContinue: boolean; } interface LocalisedPolicyWithId extends LocalisedPolicy { @@ -416,7 +418,7 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps { emailAddress?: string; }; stageState?: { - emailSid: string; + emailSid?: string; }; } @@ -540,12 +542,10 @@ export class EmailIdentityAuthEntry extends React.Component< } interface IMsisdnAuthEntryProps extends IAuthEntryProps { - inputs: { - phoneCountry: string; - phoneNumber: string; + inputs?: { + phoneCountry?: string; + phoneNumber?: string; }; - clientSecret: string; - fail: (error: Error) => void; } interface IMsisdnAuthEntryState { @@ -590,8 +590,8 @@ export class MsisdnAuthEntry extends React.Component { return this.props.matrixClient .requestRegisterMsisdnToken( - this.props.inputs.phoneCountry, - this.props.inputs.phoneNumber, + this.props.inputs?.phoneCountry ?? "", + this.props.inputs?.phoneNumber ?? "", this.props.clientSecret, 1, // TODO: Multiple send attempts? ) @@ -982,14 +982,11 @@ export class FallbackAuthEntry extends React.Component { } export interface IStageComponentProps extends IAuthEntryProps { - clientSecret?: string; stageParams?: Record; inputs?: IInputs; stageState?: IStageStatus; - showContinue?: boolean; continueText?: string; continueKind?: string; - fail?(e: Error): void; setEmailSid?(sid: string): void; onCancel?(): void; requestEmailToken?(): Promise; diff --git a/src/components/views/dialogs/ModuleUiDialog.tsx b/src/components/views/dialogs/ModuleUiDialog.tsx index d4f36f8674..acf9d1eda4 100644 --- a/src/components/views/dialogs/ModuleUiDialog.tsx +++ b/src/components/views/dialogs/ModuleUiDialog.tsx @@ -21,21 +21,24 @@ import { logger } from "matrix-js-sdk/src/logger"; import ScrollableBaseModal, { IScrollableBaseState } from "./ScrollableBaseModal"; import { _t } from "../../../languageHandler"; -interface IProps { - contentFactory: (props: DialogProps, ref: React.Ref) => React.ReactNode; - contentProps: DialogProps; +interface IProps

> { + contentFactory: (props: P, ref: React.RefObject) => React.ReactNode; + contentProps: P; title: string; - onFinished(ok?: boolean, model?: Awaited>): void; + onFinished(ok?: boolean, model?: Awaited["trySubmit"]>>): void; } interface IState extends IScrollableBaseState { // nothing special } -export class ModuleUiDialog extends ScrollableBaseModal { - private contentRef = createRef(); +export class ModuleUiDialog

> extends ScrollableBaseModal< + IProps, + IState +> { + private contentRef = createRef(); - public constructor(props: IProps) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 028671f584..c3b615ba89 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef } from "react"; +import React, { forwardRef, ForwardRefExoticComponent } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; @@ -27,12 +27,10 @@ function getErrorMessage(mxEvent?: MatrixEvent): string { } // A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = forwardRef>( - ({ mxEvent }, ref): JSX.Element => { - return ( -

- {getErrorMessage(mxEvent)} -
- ); - }, -); +export const DecryptionFailureBody = forwardRef(({ mxEvent }, ref): JSX.Element => { + return ( +
+ {getErrorMessage(mxEvent)} +
+ ); +}) as ForwardRefExoticComponent; diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index 17981be54c..b9ec41df8e 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -39,9 +39,9 @@ export interface IBodyProps { maxImageHeight?: number; replacingEventId?: string; editState?: EditorStateTransfer; - onMessageAllowed: () => void; // TODO: Docs + onMessageAllowed?: () => void; // TODO: Docs permalinkCreator?: RoomPermalinkCreator; - mediaEventHelper: MediaEventHelper; + mediaEventHelper?: MediaEventHelper; /* If present and `true`, the message has been marked as hidden pending moderation diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 86736d4e48..4a74dc676e 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -51,7 +51,7 @@ export default class MAudioBody extends React.PureComponent try { try { - const blob = await this.props.mediaEventHelper.sourceBlob.value; + const blob = await this.props.mediaEventHelper!.sourceBlob.value; buffer = await blob.arrayBuffer(); } catch (e) { this.setState({ error: e }); diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index 20cfaf1411..c8e7f3b17e 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useEffect, useState } from "react"; +import React, { ForwardRefExoticComponent, useCallback, useContext, useEffect, useState } from "react"; import { Beacon, BeaconEvent, @@ -234,6 +234,6 @@ const MBeaconBody = React.forwardRef(({ mxEvent, get )} ); -}); +}) as ForwardRefExoticComponent; export default MBeaconBody; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 4e85652217..a7159049a1 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -168,7 +168,7 @@ export default class MFileBody extends React.Component { try { this.userDidClick = true; this.setState({ - decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, + decryptedBlob: await this.props.mediaEventHelper!.sourceBlob.value, }); } catch (err) { logger.warn("Unable to decrypt attachment: ", err); @@ -188,7 +188,7 @@ export default class MFileBody extends React.Component { // As a button we're missing the `download` attribute for styling reasons, so // download with the file downloader. this.fileDownloader.download({ - blob: await mediaHelper.sourceBlob.value, + blob: await mediaHelper!.sourceBlob.value, name: this.fileName, }); } @@ -322,7 +322,7 @@ export default class MFileBody extends React.Component { // Start a fetch for the download // Based upon https://stackoverflow.com/a/49500465 - this.props.mediaEventHelper.sourceBlob.value.then((blob) => { + this.props.mediaEventHelper?.sourceBlob.value.then((blob) => { const blobUrl = URL.createObjectURL(blob); // We have to create an anchor to download the file diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 3f62c72fae..d2f9fde030 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -261,7 +261,7 @@ export default class MImageBody extends React.Component { let thumbUrl: string | null; let contentUrl: string | null; - if (this.props.mediaEventHelper.media.isEncrypted) { + if (this.props.mediaEventHelper?.media.isEncrypted) { try { [contentUrl, thumbUrl] = await Promise.all([ this.props.mediaEventHelper.sourceUrl.value, @@ -311,7 +311,7 @@ export default class MImageBody extends React.Component { } try { - const blob = await this.props.mediaEventHelper.sourceBlob.value; + const blob = await this.props.mediaEventHelper!.sourceBlob.value; if (!(await blobIsAnimated(content.info?.mimetype, blob))) { isAnimated = false; } diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index b133866d4a..091a69be94 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useState, useContext } from "react"; +import React, { useEffect, useState, useContext, ForwardRefExoticComponent } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events"; import { logger } from "matrix-js-sdk/src/logger"; @@ -109,9 +109,9 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro } return ( -
+
{_t("Ended a poll")}
); -}); +}) as ForwardRefExoticComponent; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 5fdbc42d36..2d34c909d3 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -143,7 +143,7 @@ export default class MVideoBody extends React.PureComponent logger.error("Failed to load blurhash", e); } - if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { + if (this.props.mediaEventHelper?.media.isEncrypted && this.state.decryptedUrl === null) { try { const autoplay = SettingsStore.getValue("autoplayVideo") as boolean; const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; @@ -199,7 +199,7 @@ export default class MVideoBody extends React.PureComponent // To stop subsequent download attempts fetchingData: true, }); - if (!this.props.mediaEventHelper.media.isEncrypted) { + if (!this.props.mediaEventHelper!.media.isEncrypted) { this.setState({ error: "No file given in content", }); @@ -207,8 +207,8 @@ export default class MVideoBody extends React.PureComponent } this.setState( { - decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, - decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, + decryptedUrl: await this.props.mediaEventHelper!.sourceUrl.value, + decryptedBlob: await this.props.mediaEventHelper!.sourceBlob.value, fetchingData: false, }, () => { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 3eb1cfbb60..9e974a02a3 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -27,7 +27,6 @@ import RedactedBody from "./RedactedBody"; import UnknownBody from "./UnknownBody"; import { IMediaBody } from "./IMediaBody"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; -import { ReactAnyComponent } from "../../../@types/common"; import { IBodyProps } from "./IBodyProps"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import TextualBody from "./TextualBody"; @@ -70,7 +69,7 @@ const baseBodyTypes = new Map([ [MsgType.Audio, MVoiceOrAudioBody], [MsgType.Video, MVideoBody], ]); -const baseEvTypes = new Map>>([ +const baseEvTypes = new Map>([ [EventType.Sticker, MStickerBody], [M_POLL_START.name, MPollBody], [M_POLL_START.altName, MPollBody], @@ -84,7 +83,7 @@ export default class MessageEvent extends React.Component implements IMe private body: React.RefObject = createRef(); private mediaHelper?: MediaEventHelper; private bodyTypes = new Map(baseBodyTypes.entries()); - private evTypes = new Map>>(baseEvTypes.entries()); + private evTypes = new Map>(baseEvTypes.entries()); public static contextType = MatrixClientContext; public context!: React.ContextType; @@ -123,7 +122,7 @@ export default class MessageEvent extends React.Component implements IMe this.bodyTypes.set(bodyType, bodyComponent); } - this.evTypes = new Map>>(baseEvTypes.entries()); + this.evTypes = new Map>(baseEvTypes.entries()); for (const [evType, evComponent] of Object.entries(this.props.overrideEventTypes ?? {})) { this.evTypes.set(evType, evComponent); } @@ -153,7 +152,7 @@ export default class MessageEvent extends React.Component implements IMe const content = this.props.mxEvent.getContent(); const type = this.props.mxEvent.getType(); const msgtype = content.msgtype; - let BodyType: React.ComponentType> | ReactAnyComponent = RedactedBody; + let BodyType: React.ComponentType = RedactedBody; if (!this.props.mxEvent.isRedacted()) { // only resolve BodyType if event is not redacted if (this.props.mxEvent.isDecryptionFailure()) { @@ -195,7 +194,6 @@ export default class MessageEvent extends React.Component implements IMe } } - // @ts-ignore - this is a dynamic react component return BodyType ? ( void; -} - -export default class MjolnirBody extends React.Component { +export default class MjolnirBody extends React.Component { private onAllowClick = (e: ButtonEvent): void => { e.preventDefault(); e.stopPropagation(); const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; localStorage.setItem(key, "true"); - this.props.onMessageAllowed(); + this.props.onMessageAllowed?.(); }; public render(): React.ReactNode { diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index 7b8ddf07a4..1486465203 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.tsx @@ -14,20 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext } from "react"; +import React, { ForwardRefExoticComponent, useContext } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { formatFullDate } from "../../../DateUtils"; import SettingsStore from "../../../settings/SettingsStore"; import { IBodyProps } from "./IBodyProps"; -interface IProps { - mxEvent: MatrixEvent; -} -const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { +const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { const cli: MatrixClient = useContext(MatrixClientContext); let text = _t("Message deleted"); const unsigned = mxEvent.getUnsigned(); @@ -49,6 +45,6 @@ const RedactedBody = React.forwardRef(({ mxEvent }, re {text} ); -}); +}) as ForwardRefExoticComponent; export default RedactedBody; diff --git a/src/components/views/messages/UnknownBody.tsx b/src/components/views/messages/UnknownBody.tsx index ec32449e92..dd8b2e4949 100644 --- a/src/components/views/messages/UnknownBody.tsx +++ b/src/components/views/messages/UnknownBody.tsx @@ -15,15 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { forwardRef, ForwardRefExoticComponent } from "react"; -interface IProps { - mxEvent: MatrixEvent; - children?: React.ReactNode; -} +import { IBodyProps } from "./IBodyProps"; -export default forwardRef(({ mxEvent, children }, ref) => { +export default forwardRef(({ mxEvent, children }, ref) => { const text = mxEvent.getContent().body; return (
@@ -31,4 +27,4 @@ export default forwardRef(({ mxEvent, children }, ref) = {children}
); -}); +}) as ForwardRefExoticComponent; diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index d68a606c9d..70e4f78023 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -28,7 +28,7 @@ import WidgetUtils from "../../../utils/WidgetUtils"; import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeHandle from "../elements/ResizeHandle"; -import Resizer from "../../../resizer/resizer"; +import Resizer, { IConfig } from "../../../resizer/resizer"; import PercentageDistributor from "../../../resizer/distributors/percentage"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers"; @@ -58,7 +58,7 @@ interface IState { export default class AppsDrawer extends React.Component { private unmounted = false; private resizeContainer?: HTMLDivElement; - private resizer: Resizer; + private resizer: Resizer; private dispatcherRef?: string; public static defaultProps: Partial = { showApps: true, @@ -104,7 +104,7 @@ export default class AppsDrawer extends React.Component { } }; - private createResizer(): Resizer { + private createResizer(): Resizer { // This is the horizontal one, changing the distribution of the width between the app tiles // (ie. a vertical resize handle because, the handle itself is vertical...) const classNames = { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index c3ca861fad..9e7b7bf489 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -243,7 +243,7 @@ const CreateSpaceButton: React.FC {contextMenu} diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 671a15b2c3..72ed355ffa 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -21,7 +21,6 @@ import React, { createRef, InputHTMLAttributes, LegacyRef, - forwardRef, RefObject, } from "react"; import classNames from "classnames"; @@ -59,124 +58,121 @@ interface IButtonProps extends Omit; ContextMenuComponent?: ComponentType>; onClick?(ev?: ButtonEvent): void; } -export const SpaceButton = forwardRef( - ( - { - space, - spaceKey, - className, - selected, - label, - contextMenuTooltip, - notificationState, - avatarSize, - isNarrow, - children, - ContextMenuComponent, - ...props - }, - ref: RefObject, - ) => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(ref); - const [onFocus, isActive] = useRovingTabIndex(handle); - const tabIndex = isActive ? 0 : -1; +export const SpaceButton: React.FC = ({ + space, + spaceKey, + className, + selected, + label, + contextMenuTooltip, + notificationState, + avatarSize, + isNarrow, + children, + innerRef, + ContextMenuComponent, + ...props +}) => { + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); + const [onFocus, isActive] = useRovingTabIndex(handle); + const tabIndex = isActive ? 0 : -1; - let avatar = ( -
-
+ let avatar = ( +
+
+
+ ); + if (space) { + avatar = ; + } + + let notifBadge; + if (space && notificationState) { + let ariaLabel = _t("Jump to first unread room."); + if (space.getMyMembership() === "invite") { + ariaLabel = _t("Jump to first invite."); + } + + const jumpToNotification = (ev: MouseEvent): void => { + ev.stopPropagation(); + ev.preventDefault(); + SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId); + }; + + notifBadge = ( +
+
); - if (space) { - avatar = ; - } + } - let notifBadge; - if (space && notificationState) { - let ariaLabel = _t("Jump to first unread room."); - if (space.getMyMembership() === "invite") { - ariaLabel = _t("Jump to first invite."); - } - - const jumpToNotification = (ev: MouseEvent): void => { - ev.stopPropagation(); - ev.preventDefault(); - SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId); - }; - - notifBadge = ( -
- -
- ); - } - - let contextMenu: JSX.Element | undefined; - if (space && menuDisplayed && handle.current && ContextMenuComponent) { - contextMenu = ( - - ); - } - - const viewSpaceHome = (): void => - // space is set here because of the assignment condition of onClick - defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space!.roomId }); - const activateSpace = (): void => SpaceStore.instance.setActiveSpace(spaceKey ?? space?.roomId ?? ""); - const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace); - - return ( - - {children} -
-
- {avatar} - {notifBadge} -
- {!isNarrow && {label}} - - {ContextMenuComponent && ( - - )} - - {contextMenu} -
-
+ let contextMenu: JSX.Element | undefined; + if (space && menuDisplayed && handle.current && ContextMenuComponent) { + contextMenu = ( + ); - }, -); + } + + const viewSpaceHome = (): void => + // space is set here because of the assignment condition of onClick + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space!.roomId }); + const activateSpace = (): void => SpaceStore.instance.setActiveSpace(spaceKey ?? space?.roomId ?? ""); + const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace); + + return ( + + {children} +
+
+ {avatar} + {notifBadge} +
+ {!isNarrow && {label}} + + {ContextMenuComponent && ( + + )} + + {contextMenu} +
+
+ ); +}; interface IItemProps extends InputHTMLAttributes { space: Room; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index c0a662bf43..bf92d5f73e 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -105,7 +105,7 @@ const EVENT_TILE_TYPES = new Map([ [M_POLL_END.altName, MessageEventFactory], [EventType.KeyVerificationCancel, KeyVerificationConclFactory], [EventType.KeyVerificationDone, KeyVerificationConclFactory], - [EventType.CallInvite, LegacyCallEventFactory], // note that this requires a special factory type + [EventType.CallInvite, LegacyCallEventFactory as Factory], // note that this requires a special factory type ]); const STATE_EVENT_TILE_TYPES = new Map([ diff --git a/src/modules/ProxiedModuleApi.ts b/src/modules/ProxiedModuleApi.ts index e66d3cbe10..0c2b75e71f 100644 --- a/src/modules/ProxiedModuleApi.ts +++ b/src/modules/ProxiedModuleApi.ts @@ -17,7 +17,7 @@ limitations under the License. import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi"; import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations"; import { Optional } from "matrix-events-sdk"; -import { DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent"; +import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent"; import React from "react"; import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo"; import { PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations"; @@ -81,23 +81,22 @@ export class ProxiedModuleApi implements ModuleApi { /** * @override */ - public openDialog< - M extends object, - P extends DialogProps = DialogProps, - C extends React.Component = React.Component, - >( + public openDialog>( title: string, body: (props: P, ref: React.RefObject) => React.ReactNode, + props?: Omit, ): Promise<{ didOkOrSubmit: boolean; model: M }> { return new Promise<{ didOkOrSubmit: boolean; model: M }>((resolve) => { Modal.createDialog( - ModuleUiDialog, + ModuleUiDialog, { title: title, contentFactory: body, - contentProps: { + // Typescript isn't very happy understanding that `props` satisfies `Omit` + contentProps: { + ...props, moduleApi: this, - }, + } as unknown as P, }, "mx_CompoundDialog", ).finished.then(([didOkOrSubmit, model]) => { diff --git a/src/resizer/distributors/collapse.ts b/src/resizer/distributors/collapse.ts index 507b62e903..6685efea92 100644 --- a/src/resizer/distributors/collapse.ts +++ b/src/resizer/distributors/collapse.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import FixedDistributor from "./fixed"; +import { BaseDistributor } from "./fixed"; import ResizeItem from "../item"; import Resizer, { IConfig } from "../resizer"; import Sizer from "../sizer"; @@ -25,7 +25,7 @@ export interface ICollapseConfig extends IConfig { isItemCollapsed(element: HTMLElement): boolean; } -class CollapseItem extends ResizeItem { +export class CollapseItem extends ResizeItem { public notifyCollapsed(collapsed: boolean): void { this.resizer.config?.onCollapsed?.(collapsed, this.id, this.domNode); } @@ -35,10 +35,10 @@ class CollapseItem extends ResizeItem { } } -export default class CollapseDistributor extends FixedDistributor { +export default class CollapseDistributor extends BaseDistributor { public static createItem( resizeHandle: HTMLDivElement, - resizer: Resizer, + resizer: Resizer, sizer: Sizer, container?: HTMLElement, ): CollapseItem { diff --git a/src/resizer/distributors/fixed.ts b/src/resizer/distributors/fixed.ts index f1ca5198b2..609b0fdbce 100644 --- a/src/resizer/distributors/fixed.ts +++ b/src/resizer/distributors/fixed.ts @@ -18,21 +18,7 @@ import ResizeItem from "../item"; import Sizer from "../sizer"; import Resizer, { IConfig } from "../resizer"; -/** -distributors translate a moving cursor into -CSS/DOM changes by calling the sizer - -they have two methods: - `resize` receives then new item size - `resizeFromContainerOffset` receives resize handle location - within the container bounding box. For internal use. - This method usually ends up calling `resize` once the start offset is subtracted. -*/ -export default class FixedDistributor = ResizeItem> { - public static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem { - return new ResizeItem(resizeHandle, resizer, sizer); - } - +export abstract class BaseDistributor = ResizeItem> { public static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer { return new Sizer(containerElement, vertical, reverse); } @@ -67,3 +53,22 @@ export default class FixedDistributor = ResizeItem, +> extends BaseDistributor { + public static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem { + return new ResizeItem(resizeHandle, resizer, sizer); + } +} diff --git a/src/resizer/item.ts b/src/resizer/item.ts index 6c979225d6..3372f3ada7 100644 --- a/src/resizer/item.ts +++ b/src/resizer/item.ts @@ -17,14 +17,14 @@ limitations under the License. import Resizer, { IConfig } from "./resizer"; import Sizer from "./sizer"; -export default class ResizeItem { +export default class ResizeItem { public readonly domNode: HTMLElement; protected readonly id: string | null; protected reverse: boolean; public constructor( handle: HTMLElement, - public readonly resizer: Resizer, + public readonly resizer: Resizer, public readonly sizer: Sizer, public readonly container?: HTMLElement, ) { @@ -37,12 +37,17 @@ export default class ResizeItem { this.id = handle.getAttribute("data-id"); } - private copyWith(handle: HTMLElement, resizer: Resizer, sizer: Sizer, container?: HTMLElement): ResizeItem { + private copyWith( + handle: HTMLElement, + resizer: Resizer, + sizer: Sizer, + container?: HTMLElement, + ): ResizeItem { const Ctor = this.constructor as typeof ResizeItem; return new Ctor(handle, resizer, sizer, container); } - private advance(forwards: boolean): ResizeItem | undefined { + private advance(forwards: boolean): ResizeItem | undefined { // opposite direction from fromResizeHandle to get back to handle let handle: Element | null | undefined = this.reverse ? this.domNode.previousElementSibling @@ -64,11 +69,11 @@ export default class ResizeItem { } } - public next(): ResizeItem | undefined { + public next(): ResizeItem | undefined { return this.advance(true); } - public previous(): ResizeItem | undefined { + public previous(): ResizeItem | undefined { return this.advance(false); } @@ -106,7 +111,7 @@ export default class ResizeItem { this.resizer.config?.onResized?.(null, this.id, this.domNode); } - public first(): ResizeItem | undefined { + public first(): ResizeItem | undefined { if (!this.domNode.parentElement?.children) { return; } @@ -118,7 +123,7 @@ export default class ResizeItem { } } - public last(): ResizeItem | undefined { + public last(): ResizeItem | undefined { if (!this.domNode.parentElement?.children) { return; } diff --git a/src/resizer/resizer.ts b/src/resizer/resizer.ts index 07e74337a7..d78c30b444 100644 --- a/src/resizer/resizer.ts +++ b/src/resizer/resizer.ts @@ -38,7 +38,7 @@ export interface IConfig { handler?: HTMLDivElement; } -export default class Resizer { +export default class Resizer = ResizeItem> { private classNames: IClassNames; // TODO move vertical/horizontal to config option/container class @@ -46,13 +46,8 @@ export default class Resizer { public constructor( public container: HTMLElement | null, private readonly distributorCtor: { - new (item: ResizeItem): FixedDistributor; - createItem( - resizeHandle: HTMLDivElement, - resizer: Resizer, - sizer: Sizer, - container?: HTMLElement, - ): ResizeItem; + new (item: I): FixedDistributor; + createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer, container?: HTMLElement): I; createSizer(containerElement: HTMLElement | null, vertical: boolean, reverse: boolean): Sizer; }, public readonly config?: C, @@ -87,7 +82,7 @@ export default class Resizer { @param {number} handleIndex the index of the resize handle in the container @return {FixedDistributor} a new distributor for the given handle */ - public forHandleAt(handleIndex: number): FixedDistributor | undefined { + public forHandleAt(handleIndex: number): FixedDistributor | undefined { const handles = this.getResizeHandles(); const handle = handles[handleIndex]; if (handle) { @@ -96,7 +91,7 @@ export default class Resizer { } } - public forHandleWithId(id: string): FixedDistributor | undefined { + public forHandleWithId(id: string): FixedDistributor | undefined { const handles = this.getResizeHandles(); const handle = handles.find((h) => h.getAttribute("data-id") === id); if (handle) { @@ -178,7 +173,7 @@ export default class Resizer { { trailing: true, leading: true }, ); - public getDistributors = (): FixedDistributor>[] => { + public getDistributors = (): FixedDistributor[] => { return this.getResizeHandles().map((handle) => { const { distributor } = this.createSizerAndDistributor(handle); return distributor; @@ -187,7 +182,7 @@ export default class Resizer { private createSizerAndDistributor(resizeHandle: HTMLDivElement): { sizer: Sizer; - distributor: FixedDistributor; + distributor: FixedDistributor; } { const vertical = resizeHandle.classList.contains(this.classNames.vertical!); const reverse = this.isReverseResizeHandle(resizeHandle); diff --git a/tsconfig.json b/tsconfig.json index 40b5091a82..556a11782d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,6 @@ "jsx": "react", "lib": ["es2020", "dom", "dom.iterable"], "strict": true, - "strictFunctionTypes": false, "useUnknownInCatchVariables": false }, "include": [ diff --git a/yarn.lock b/yarn.lock index d558aec96d..14ea484eeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1619,10 +1619,10 @@ version "3.2.14" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" -"@matrix-org/react-sdk-module-api@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.5.tgz#78bd80f42b918394978965ef3e08496e97948c7a" - integrity sha512-QhH1T1E6Q6csCUitQzm32SRnX49Ox73TF5BZ4p5TOGFpPD3QuYc5/dDC1Yh3xUljgqOS2C6H24qaskw6olCtfQ== +"@matrix-org/react-sdk-module-api@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.6.tgz#941872ed081acdca9d247ccd6e146265aa24010b" + integrity sha512-FydbJYSMecpDIGk4fVQ9djjckQdbJPV9bH3px78TQ+MX/WHmzPmjEpMPTeP3uDSeg0EWmfoIFdNypJglMqAHpw== dependencies: "@babel/runtime" "^7.17.9"