Enable strictFunctionTypes (#11201)

pull/28217/head
Michael Telatynski 2023-07-07 13:37:26 +01:00 committed by GitHub
parent 40de66424d
commit 4207d182cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 245 additions and 258 deletions

View File

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

View File

@ -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<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
@ -22,7 +22,6 @@ export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U,
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
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

View File

@ -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<IProps, IState> {
protected layoutWatcherRef?: string;
protected compactLayoutWatcherRef?: string;
protected backgroundImageWatcherRef?: string;
protected resizer?: Resizer;
protected resizer?: Resizer<ICollapseConfig, CollapseItem>;
public constructor(props: IProps) {
super(props);
@ -230,7 +230,7 @@ class LoggedInView extends React.Component<IProps, IState> {
return this._roomView.current.canResetTimeline();
};
private createResizer(): Resizer {
private createResizer(): Resizer<ICollapseConfig, CollapseItem> {
let panelSize: number | null;
let panelCollapsed: boolean;
const collapseConfig: ICollapseConfig = {

View File

@ -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<ScrollPanel, Props>(
(
{ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props,
ref: RefObject<ScrollPanel>,
) => {
({ 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<ScrollPanel, Props>(
const aborted = useRef(false);
// A map from room ID to permalink creator
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current;
const innerRef = useRef<ScrollPanel | null>();
useEffect(() => {
return () => {
@ -214,8 +212,16 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
// 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<ScrollPanel, Props>(
return (
<ScrollPanel
ref={ref}
ref={onRef}
className={"mx_RoomView_searchResultsPanel " + className}
onFillRequest={onSearchResultsFillRequest}
resizeNotifier={resizeNotifier}

View File

@ -854,7 +854,7 @@ export default class ScrollPanel extends React.Component<IProps> {
return this.divScroll;
}
private collectScroll = (divScroll: HTMLDivElement): void => {
private collectScroll = (divScroll: HTMLDivElement | null): void => {
this.divScroll = divScroll;
};

View File

@ -91,6 +91,9 @@ interface IAuthEntryProps {
onPhaseChange: (phase: number) => void;
submitAuthDict: (auth: IAuthDict) => void;
requestEmailToken?: () => Promise<void>;
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<IMsisdnAuthEntryProps, IMsi
private requestMsisdnToken(): Promise<void> {
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<IAuthEntryProps> {
}
export interface IStageComponentProps extends IAuthEntryProps {
clientSecret?: string;
stageParams?: Record<string, any>;
inputs?: IInputs;
stageState?: IStageStatus;
showContinue?: boolean;
continueText?: string;
continueKind?: string;
fail?(e: Error): void;
setEmailSid?(sid: string): void;
onCancel?(): void;
requestEmailToken?(): Promise<void>;

View File

@ -21,21 +21,24 @@ import { logger } from "matrix-js-sdk/src/logger";
import ScrollableBaseModal, { IScrollableBaseState } from "./ScrollableBaseModal";
import { _t } from "../../../languageHandler";
interface IProps<C extends React.Component = React.Component> {
contentFactory: (props: DialogProps, ref: React.Ref<C>) => React.ReactNode;
contentProps: DialogProps;
interface IProps<P extends DialogProps, C extends DialogContent<P>> {
contentFactory: (props: P, ref: React.RefObject<C>) => React.ReactNode;
contentProps: P;
title: string;
onFinished(ok?: boolean, model?: Awaited<ReturnType<DialogContent["trySubmit"]>>): void;
onFinished(ok?: boolean, model?: Awaited<ReturnType<DialogContent<P>["trySubmit"]>>): void;
}
interface IState extends IScrollableBaseState {
// nothing special
}
export class ModuleUiDialog extends ScrollableBaseModal<IProps, IState> {
private contentRef = createRef<DialogContent>();
export class ModuleUiDialog<P extends DialogProps, C extends DialogContent<P>> extends ScrollableBaseModal<
IProps<P, C>,
IState
> {
private contentRef = createRef<C>();
public constructor(props: IProps) {
public constructor(props: IProps<P, C>) {
super(props);
this.state = {

View File

@ -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<HTMLDivElement, Partial<IBodyProps>>(
({ mxEvent }, ref): JSX.Element => {
return (
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
{getErrorMessage(mxEvent)}
</div>
);
},
);
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): JSX.Element => {
return (
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
{getErrorMessage(mxEvent)}
</div>
);
}) as ForwardRefExoticComponent<IBodyProps>;

View File

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

View File

@ -51,7 +51,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
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 });

View File

@ -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<HTMLDivElement, IBodyProps>(({ mxEvent, get
)}
</div>
);
});
}) as ForwardRefExoticComponent<IBodyProps>;
export default MBeaconBody;

View File

@ -168,7 +168,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
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<IProps, IState> {
// 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<IProps, IState> {
// 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

View File

@ -261,7 +261,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
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<IBodyProps, IState> {
}
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;
}

View File

@ -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<any, IBodyProps>(({ mxEvent, ...pro
}
return (
<div>
<div ref={ref}>
<Caption>{_t("Ended a poll")}</Caption>
<MPollBody mxEvent={pollStartEvent} {...props} />
</div>
);
});
}) as ForwardRefExoticComponent<IBodyProps>;

View File

@ -143,7 +143,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
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<IBodyProps, IState>
// 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<IBodyProps, IState>
}
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,
},
() => {

View File

@ -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<string, typeof React.Component>([
[MsgType.Audio, MVoiceOrAudioBody],
[MsgType.Video, MVideoBody],
]);
const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
[EventType.Sticker, MStickerBody],
[M_POLL_START.name, MPollBody],
[M_POLL_START.altName, MPollBody],
@ -84,7 +83,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private mediaHelper?: MediaEventHelper;
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
private evTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>(baseEvTypes.entries());
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
@ -123,7 +122,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
this.bodyTypes.set(bodyType, bodyComponent);
}
this.evTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>(baseEvTypes.entries());
this.evTypes = new Map<string, React.ComponentType<IBodyProps>>(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<IProps> implements IMe
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
const msgtype = content.msgtype;
let BodyType: React.ComponentType<Partial<IBodyProps>> | ReactAnyComponent = RedactedBody;
let BodyType: React.ComponentType<IBodyProps> = 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<IProps> implements IMe
}
}
// @ts-ignore - this is a dynamic react component
return BodyType ? (
<BodyType
ref={this.body}

View File

@ -15,24 +15,19 @@ limitations under the License.
*/
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { IBodyProps } from "./IBodyProps";
interface IProps {
mxEvent: MatrixEvent;
onMessageAllowed: () => void;
}
export default class MjolnirBody extends React.Component<IProps> {
export default class MjolnirBody extends React.Component<IBodyProps> {
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 {

View File

@ -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<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
const cli: MatrixClient = useContext(MatrixClientContext);
let text = _t("Message deleted");
const unsigned = mxEvent.getUnsigned();
@ -49,6 +45,6 @@ const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, re
{text}
</span>
);
});
}) as ForwardRefExoticComponent<IBodyProps>;
export default RedactedBody;

View File

@ -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<HTMLDivElement, IProps>(({ mxEvent, children }, ref) => {
export default forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, children }, ref) => {
const text = mxEvent.getContent().body;
return (
<div className="mx_UnknownBody" ref={ref}>
@ -31,4 +27,4 @@ export default forwardRef<HTMLDivElement, IProps>(({ mxEvent, children }, ref) =
{children}
</div>
);
});
}) as ForwardRefExoticComponent<IBodyProps>;

View File

@ -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<IProps, IState> {
private unmounted = false;
private resizeContainer?: HTMLDivElement;
private resizer: Resizer;
private resizer: Resizer<IConfig>;
private dispatcherRef?: string;
public static defaultProps: Partial<IProps> = {
showApps: true,
@ -104,7 +104,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
}
};
private createResizer(): Resizer {
private createResizer(): Resizer<IConfig> {
// 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 = {

View File

@ -243,7 +243,7 @@ const CreateSpaceButton: React.FC<Pick<IInnerSpacePanelProps, "isPanelCollapsed"
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={onNewClick}
isNarrow={isPanelCollapsed}
ref={handle}
innerRef={handle}
/>
{contextMenu}

View File

@ -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<ComponentProps<typeof AccessibleTooltipButto
notificationState?: NotificationState;
isNarrow?: boolean;
avatarSize?: number;
innerRef?: RefObject<HTMLElement>;
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
onClick?(ev?: ButtonEvent): void;
}
export const SpaceButton = forwardRef<HTMLElement, IButtonProps>(
(
{
space,
spaceKey,
className,
selected,
label,
contextMenuTooltip,
notificationState,
avatarSize,
isNarrow,
children,
ContextMenuComponent,
...props
},
ref: RefObject<HTMLElement>,
) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(ref);
const [onFocus, isActive] = useRovingTabIndex(handle);
const tabIndex = isActive ? 0 : -1;
export const SpaceButton: React.FC<IButtonProps> = ({
space,
spaceKey,
className,
selected,
label,
contextMenuTooltip,
notificationState,
avatarSize,
isNarrow,
children,
innerRef,
ContextMenuComponent,
...props
}) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(innerRef);
const [onFocus, isActive] = useRovingTabIndex(handle);
const tabIndex = isActive ? 0 : -1;
let avatar = (
<div className="mx_SpaceButton_avatarPlaceholder">
<div className="mx_SpaceButton_icon" />
let avatar = (
<div className="mx_SpaceButton_avatarPlaceholder">
<div className="mx_SpaceButton_icon" />
</div>
);
if (space) {
avatar = <RoomAvatar width={avatarSize} height={avatarSize} room={space} />;
}
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 = (
<div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={jumpToNotification}
forceCount={false}
notification={notificationState}
aria-label={ariaLabel}
tabIndex={tabIndex}
showUnsentTooltip={true}
/>
</div>
);
if (space) {
avatar = <RoomAvatar width={avatarSize} height={avatarSize} room={space} />;
}
}
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 = (
<div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={jumpToNotification}
forceCount={false}
notification={notificationState}
aria-label={ariaLabel}
tabIndex={tabIndex}
showUnsentTooltip={true}
/>
</div>
);
}
let contextMenu: JSX.Element | undefined;
if (space && menuDisplayed && handle.current && ContextMenuComponent) {
contextMenu = (
<ContextMenuComponent
{...toRightOf(handle.current.getBoundingClientRect(), 0)}
space={space}
onFinished={closeMenu}
/>
);
}
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 (
<AccessibleTooltipButton
{...props}
className={classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected,
mx_SpaceButton_hasMenuOpen: menuDisplayed,
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
onClick={onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
inputRef={handle}
tabIndex={tabIndex}
onFocus={onFocus}
>
{children}
<div className="mx_SpaceButton_selectionWrapper">
<div className="mx_SpaceButton_avatarWrapper">
{avatar}
{notifBadge}
</div>
{!isNarrow && <span className="mx_SpaceButton_name">{label}</span>}
{ContextMenuComponent && (
<ContextMenuTooltipButton
className="mx_SpaceButton_menuButton"
onClick={openMenu}
title={contextMenuTooltip}
isExpanded={menuDisplayed}
/>
)}
{contextMenu}
</div>
</AccessibleTooltipButton>
let contextMenu: JSX.Element | undefined;
if (space && menuDisplayed && handle.current && ContextMenuComponent) {
contextMenu = (
<ContextMenuComponent
{...toRightOf(handle.current.getBoundingClientRect(), 0)}
space={space}
onFinished={closeMenu}
/>
);
},
);
}
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 (
<AccessibleTooltipButton
{...props}
className={classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected,
mx_SpaceButton_hasMenuOpen: menuDisplayed,
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
onClick={onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
inputRef={handle}
tabIndex={tabIndex}
onFocus={onFocus}
>
{children}
<div className="mx_SpaceButton_selectionWrapper">
<div className="mx_SpaceButton_avatarWrapper">
{avatar}
{notifBadge}
</div>
{!isNarrow && <span className="mx_SpaceButton_name">{label}</span>}
{ContextMenuComponent && (
<ContextMenuTooltipButton
className="mx_SpaceButton_menuButton"
onClick={openMenu}
title={contextMenuTooltip}
isExpanded={menuDisplayed}
/>
)}
{contextMenu}
</div>
</AccessibleTooltipButton>
);
};
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space: Room;

View File

@ -105,7 +105,7 @@ const EVENT_TILE_TYPES = new Map<string, Factory>([
[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<string, Factory>([

View File

@ -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<M extends object, P extends DialogProps, C extends DialogContent<P>>(
title: string,
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
props?: Omit<P, keyof DialogProps>,
): Promise<{ didOkOrSubmit: boolean; model: M }> {
return new Promise<{ didOkOrSubmit: boolean; model: M }>((resolve) => {
Modal.createDialog(
ModuleUiDialog,
ModuleUiDialog<P, C>,
{
title: title,
contentFactory: body,
contentProps: <DialogProps>{
// Typescript isn't very happy understanding that `props` satisfies `Omit<P, keyof DialogProps>`
contentProps: {
...props,
moduleApi: this,
},
} as unknown as P,
},
"mx_CompoundDialog",
).finished.then(([didOkOrSubmit, model]) => {

View File

@ -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<ICollapseConfig> {
export class CollapseItem extends ResizeItem<ICollapseConfig> {
public notifyCollapsed(collapsed: boolean): void {
this.resizer.config?.onCollapsed?.(collapsed, this.id, this.domNode);
}
@ -35,10 +35,10 @@ class CollapseItem extends ResizeItem<ICollapseConfig> {
}
}
export default class CollapseDistributor extends FixedDistributor<ICollapseConfig, CollapseItem> {
export default class CollapseDistributor extends BaseDistributor<ICollapseConfig, CollapseItem> {
public static createItem(
resizeHandle: HTMLDivElement,
resizer: Resizer<ICollapseConfig>,
resizer: Resizer<ICollapseConfig, CollapseItem>,
sizer: Sizer,
container?: HTMLElement,
): CollapseItem {

View File

@ -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<C extends IConfig, I extends ResizeItem<any> = ResizeItem<C>> {
public static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem {
return new ResizeItem(resizeHandle, resizer, sizer);
}
export abstract class BaseDistributor<C extends IConfig, I extends ResizeItem<C> = ResizeItem<C>> {
public static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
return new Sizer(containerElement, vertical, reverse);
}
@ -67,3 +53,22 @@ export default class FixedDistributor<C extends IConfig, I extends ResizeItem<an
this.item.finish();
}
}
/**
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<
C extends IConfig,
I extends ResizeItem<C> = ResizeItem<C>,
> extends BaseDistributor<C, I> {
public static createItem(resizeHandle: HTMLDivElement, resizer: Resizer<any>, sizer: Sizer): ResizeItem<any> {
return new ResizeItem(resizeHandle, resizer, sizer);
}
}

View File

@ -17,14 +17,14 @@ limitations under the License.
import Resizer, { IConfig } from "./resizer";
import Sizer from "./sizer";
export default class ResizeItem<C extends IConfig = IConfig> {
export default class ResizeItem<C extends IConfig> {
public readonly domNode: HTMLElement;
protected readonly id: string | null;
protected reverse: boolean;
public constructor(
handle: HTMLElement,
public readonly resizer: Resizer<C>,
public readonly resizer: Resizer<C, any>,
public readonly sizer: Sizer,
public readonly container?: HTMLElement,
) {
@ -37,12 +37,17 @@ export default class ResizeItem<C extends IConfig = IConfig> {
this.id = handle.getAttribute("data-id");
}
private copyWith(handle: HTMLElement, resizer: Resizer, sizer: Sizer, container?: HTMLElement): ResizeItem {
private copyWith(
handle: HTMLElement,
resizer: Resizer<C, any>,
sizer: Sizer,
container?: HTMLElement,
): ResizeItem<C> {
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<C> | 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<C extends IConfig = IConfig> {
}
}
public next(): ResizeItem | undefined {
public next(): ResizeItem<C> | undefined {
return this.advance(true);
}
public previous(): ResizeItem | undefined {
public previous(): ResizeItem<C> | undefined {
return this.advance(false);
}
@ -106,7 +111,7 @@ export default class ResizeItem<C extends IConfig = IConfig> {
this.resizer.config?.onResized?.(null, this.id, this.domNode);
}
public first(): ResizeItem | undefined {
public first(): ResizeItem<C> | undefined {
if (!this.domNode.parentElement?.children) {
return;
}
@ -118,7 +123,7 @@ export default class ResizeItem<C extends IConfig = IConfig> {
}
}
public last(): ResizeItem | undefined {
public last(): ResizeItem<C> | undefined {
if (!this.domNode.parentElement?.children) {
return;
}

View File

@ -38,7 +38,7 @@ export interface IConfig {
handler?: HTMLDivElement;
}
export default class Resizer<C extends IConfig = IConfig> {
export default class Resizer<C extends IConfig, I extends ResizeItem<C> = ResizeItem<C>> {
private classNames: IClassNames;
// TODO move vertical/horizontal to config option/container class
@ -46,13 +46,8 @@ export default class Resizer<C extends IConfig = IConfig> {
public constructor(
public container: HTMLElement | null,
private readonly distributorCtor: {
new (item: ResizeItem): FixedDistributor<C, any>;
createItem(
resizeHandle: HTMLDivElement,
resizer: Resizer,
sizer: Sizer,
container?: HTMLElement,
): ResizeItem;
new (item: I): FixedDistributor<C, I>;
createItem(resizeHandle: HTMLDivElement, resizer: Resizer<C, I>, 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<C extends IConfig = IConfig> {
@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<C> | undefined {
public forHandleAt(handleIndex: number): FixedDistributor<C, I> | undefined {
const handles = this.getResizeHandles();
const handle = handles[handleIndex];
if (handle) {
@ -96,7 +91,7 @@ export default class Resizer<C extends IConfig = IConfig> {
}
}
public forHandleWithId(id: string): FixedDistributor<C> | undefined {
public forHandleWithId(id: string): FixedDistributor<C, I> | 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<C extends IConfig = IConfig> {
{ trailing: true, leading: true },
);
public getDistributors = (): FixedDistributor<any, ResizeItem<any>>[] => {
public getDistributors = (): FixedDistributor<C, I>[] => {
return this.getResizeHandles().map((handle) => {
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
@ -187,7 +182,7 @@ export default class Resizer<C extends IConfig = IConfig> {
private createSizerAndDistributor(resizeHandle: HTMLDivElement): {
sizer: Sizer;
distributor: FixedDistributor<any>;
distributor: FixedDistributor<C, I>;
} {
const vertical = resizeHandle.classList.contains(this.classNames.vertical!);
const reverse = this.isReverseResizeHandle(resizeHandle);

View File

@ -14,7 +14,6 @@
"jsx": "react",
"lib": ["es2020", "dom", "dom.iterable"],
"strict": true,
"strictFunctionTypes": false,
"useUnknownInCatchVariables": false
},
"include": [

View File

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