mirror of https://github.com/vector-im/riot-web
Enable strictFunctionTypes (#11201)
parent
40de66424d
commit
4207d182cd
|
@ -62,7 +62,7 @@
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.5.0",
|
"@matrix-org/analytics-events": "^0.5.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^2.3.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/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { JSXElementConstructor } from "react";
|
import { JSXElementConstructor } from "react";
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
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 Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
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
|
// Utility type for string dot notation for accessing nested object properties
|
||||||
// Based on https://stackoverflow.com/a/58436959
|
// Based on https://stackoverflow.com/a/58436959
|
||||||
|
|
|
@ -46,7 +46,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
import { CollapseItem, ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
|
@ -134,7 +134,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
protected layoutWatcherRef?: string;
|
protected layoutWatcherRef?: string;
|
||||||
protected compactLayoutWatcherRef?: string;
|
protected compactLayoutWatcherRef?: string;
|
||||||
protected backgroundImageWatcherRef?: string;
|
protected backgroundImageWatcherRef?: string;
|
||||||
protected resizer?: Resizer;
|
protected resizer?: Resizer<ICollapseConfig, CollapseItem>;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -230,7 +230,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
return this._roomView.current.canResetTimeline();
|
return this._roomView.current.canResetTimeline();
|
||||||
};
|
};
|
||||||
|
|
||||||
private createResizer(): Resizer {
|
private createResizer(): Resizer<ICollapseConfig, CollapseItem> {
|
||||||
let panelSize: number | null;
|
let panelSize: number | null;
|
||||||
let panelCollapsed: boolean;
|
let panelCollapsed: boolean;
|
||||||
const collapseConfig: ICollapseConfig = {
|
const collapseConfig: ICollapseConfig = {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||||
import { IThreadBundledRelationship } from "matrix-js-sdk/src/models/event";
|
import { IThreadBundledRelationship } from "matrix-js-sdk/src/models/event";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
@ -57,10 +57,7 @@ interface Props {
|
||||||
// XXX: todo: merge overlapping results somehow?
|
// XXX: todo: merge overlapping results somehow?
|
||||||
// XXX: why doesn't searching on name work?
|
// XXX: why doesn't searching on name work?
|
||||||
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
(
|
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props, ref) => {
|
||||||
{ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props,
|
|
||||||
ref: RefObject<ScrollPanel>,
|
|
||||||
) => {
|
|
||||||
const client = useContext(MatrixClientContext);
|
const client = useContext(MatrixClientContext);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const [inProgress, setInProgress] = useState(true);
|
const [inProgress, setInProgress] = useState(true);
|
||||||
|
@ -69,6 +66,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
const aborted = useRef(false);
|
const aborted = useRef(false);
|
||||||
// A map from room ID to permalink creator
|
// A map from room ID to permalink creator
|
||||||
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current;
|
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current;
|
||||||
|
const innerRef = useRef<ScrollPanel | null>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -214,8 +212,16 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
// once dynamic content in the search results load, make the scrollPanel check
|
// once dynamic content in the search results load, make the scrollPanel check
|
||||||
// the scroll offsets.
|
// the scroll offsets.
|
||||||
const onHeightChanged = (): void => {
|
const onHeightChanged = (): void => {
|
||||||
const scrollPanel = ref.current;
|
innerRef.current?.checkScroll();
|
||||||
scrollPanel?.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;
|
let lastRoomId: string | undefined;
|
||||||
|
@ -317,7 +323,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollPanel
|
<ScrollPanel
|
||||||
ref={ref}
|
ref={onRef}
|
||||||
className={"mx_RoomView_searchResultsPanel " + className}
|
className={"mx_RoomView_searchResultsPanel " + className}
|
||||||
onFillRequest={onSearchResultsFillRequest}
|
onFillRequest={onSearchResultsFillRequest}
|
||||||
resizeNotifier={resizeNotifier}
|
resizeNotifier={resizeNotifier}
|
||||||
|
|
|
@ -854,7 +854,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
return this.divScroll;
|
return this.divScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectScroll = (divScroll: HTMLDivElement): void => {
|
private collectScroll = (divScroll: HTMLDivElement | null): void => {
|
||||||
this.divScroll = divScroll;
|
this.divScroll = divScroll;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,9 @@ interface IAuthEntryProps {
|
||||||
onPhaseChange: (phase: number) => void;
|
onPhaseChange: (phase: number) => void;
|
||||||
submitAuthDict: (auth: IAuthDict) => void;
|
submitAuthDict: (auth: IAuthDict) => void;
|
||||||
requestEmailToken?: () => Promise<void>;
|
requestEmailToken?: () => Promise<void>;
|
||||||
|
fail: (error: Error) => void;
|
||||||
|
clientSecret: string;
|
||||||
|
showContinue: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPasswordAuthEntryState {
|
interface IPasswordAuthEntryState {
|
||||||
|
@ -248,7 +251,6 @@ interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||||
stageParams?: {
|
stageParams?: {
|
||||||
policies?: Policies;
|
policies?: Policies;
|
||||||
};
|
};
|
||||||
showContinue: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||||
|
@ -416,7 +418,7 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
||||||
emailAddress?: string;
|
emailAddress?: string;
|
||||||
};
|
};
|
||||||
stageState?: {
|
stageState?: {
|
||||||
emailSid: string;
|
emailSid?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,12 +542,10 @@ export class EmailIdentityAuthEntry extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
||||||
inputs: {
|
inputs?: {
|
||||||
phoneCountry: string;
|
phoneCountry?: string;
|
||||||
phoneNumber: string;
|
phoneNumber?: string;
|
||||||
};
|
};
|
||||||
clientSecret: string;
|
|
||||||
fail: (error: Error) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMsisdnAuthEntryState {
|
interface IMsisdnAuthEntryState {
|
||||||
|
@ -590,8 +590,8 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
||||||
private requestMsisdnToken(): Promise<void> {
|
private requestMsisdnToken(): Promise<void> {
|
||||||
return this.props.matrixClient
|
return this.props.matrixClient
|
||||||
.requestRegisterMsisdnToken(
|
.requestRegisterMsisdnToken(
|
||||||
this.props.inputs.phoneCountry,
|
this.props.inputs?.phoneCountry ?? "",
|
||||||
this.props.inputs.phoneNumber,
|
this.props.inputs?.phoneNumber ?? "",
|
||||||
this.props.clientSecret,
|
this.props.clientSecret,
|
||||||
1, // TODO: Multiple send attempts?
|
1, // TODO: Multiple send attempts?
|
||||||
)
|
)
|
||||||
|
@ -982,14 +982,11 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStageComponentProps extends IAuthEntryProps {
|
export interface IStageComponentProps extends IAuthEntryProps {
|
||||||
clientSecret?: string;
|
|
||||||
stageParams?: Record<string, any>;
|
stageParams?: Record<string, any>;
|
||||||
inputs?: IInputs;
|
inputs?: IInputs;
|
||||||
stageState?: IStageStatus;
|
stageState?: IStageStatus;
|
||||||
showContinue?: boolean;
|
|
||||||
continueText?: string;
|
continueText?: string;
|
||||||
continueKind?: string;
|
continueKind?: string;
|
||||||
fail?(e: Error): void;
|
|
||||||
setEmailSid?(sid: string): void;
|
setEmailSid?(sid: string): void;
|
||||||
onCancel?(): void;
|
onCancel?(): void;
|
||||||
requestEmailToken?(): Promise<void>;
|
requestEmailToken?(): Promise<void>;
|
||||||
|
|
|
@ -21,21 +21,24 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import ScrollableBaseModal, { IScrollableBaseState } from "./ScrollableBaseModal";
|
import ScrollableBaseModal, { IScrollableBaseState } from "./ScrollableBaseModal";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
interface IProps<C extends React.Component = React.Component> {
|
interface IProps<P extends DialogProps, C extends DialogContent<P>> {
|
||||||
contentFactory: (props: DialogProps, ref: React.Ref<C>) => React.ReactNode;
|
contentFactory: (props: P, ref: React.RefObject<C>) => React.ReactNode;
|
||||||
contentProps: DialogProps;
|
contentProps: P;
|
||||||
title: string;
|
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 {
|
interface IState extends IScrollableBaseState {
|
||||||
// nothing special
|
// nothing special
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModuleUiDialog extends ScrollableBaseModal<IProps, IState> {
|
export class ModuleUiDialog<P extends DialogProps, C extends DialogContent<P>> extends ScrollableBaseModal<
|
||||||
private contentRef = createRef<DialogContent>();
|
IProps<P, C>,
|
||||||
|
IState
|
||||||
|
> {
|
||||||
|
private contentRef = createRef<C>();
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps<P, C>) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef, ForwardRefExoticComponent } from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -27,12 +27,10 @@ function getErrorMessage(mxEvent?: MatrixEvent): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A placeholder element for messages that could not be decrypted
|
// A placeholder element for messages that could not be decrypted
|
||||||
export const DecryptionFailureBody = forwardRef<HTMLDivElement, Partial<IBodyProps>>(
|
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): JSX.Element => {
|
||||||
({ mxEvent }, ref): JSX.Element => {
|
return (
|
||||||
return (
|
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
|
||||||
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
|
{getErrorMessage(mxEvent)}
|
||||||
{getErrorMessage(mxEvent)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
|
@ -39,9 +39,9 @@ export interface IBodyProps {
|
||||||
maxImageHeight?: number;
|
maxImageHeight?: number;
|
||||||
replacingEventId?: string;
|
replacingEventId?: string;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
onMessageAllowed: () => void; // TODO: Docs
|
onMessageAllowed?: () => void; // TODO: Docs
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
mediaEventHelper: MediaEventHelper;
|
mediaEventHelper?: MediaEventHelper;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
If present and `true`, the message has been marked as hidden pending moderation
|
If present and `true`, the message has been marked as hidden pending moderation
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
|
||||||
buffer = await blob.arrayBuffer();
|
buffer = await blob.arrayBuffer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
import React, { ForwardRefExoticComponent, useCallback, useContext, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Beacon,
|
Beacon,
|
||||||
BeaconEvent,
|
BeaconEvent,
|
||||||
|
@ -234,6 +234,6 @@ const MBeaconBody = React.forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, get
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||||
|
|
||||||
export default MBeaconBody;
|
export default MBeaconBody;
|
||||||
|
|
|
@ -168,7 +168,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
try {
|
try {
|
||||||
this.userDidClick = true;
|
this.userDidClick = true;
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
|
decryptedBlob: await this.props.mediaEventHelper!.sourceBlob.value,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn("Unable to decrypt attachment: ", 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
|
// As a button we're missing the `download` attribute for styling reasons, so
|
||||||
// download with the file downloader.
|
// download with the file downloader.
|
||||||
this.fileDownloader.download({
|
this.fileDownloader.download({
|
||||||
blob: await mediaHelper.sourceBlob.value,
|
blob: await mediaHelper!.sourceBlob.value,
|
||||||
name: this.fileName,
|
name: this.fileName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -322,7 +322,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Start a fetch for the download
|
// Start a fetch for the download
|
||||||
// Based upon https://stackoverflow.com/a/49500465
|
// 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);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// We have to create an anchor to download the file
|
// We have to create an anchor to download the file
|
||||||
|
|
|
@ -261,7 +261,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
let thumbUrl: string | null;
|
let thumbUrl: string | null;
|
||||||
let contentUrl: string | null;
|
let contentUrl: string | null;
|
||||||
if (this.props.mediaEventHelper.media.isEncrypted) {
|
if (this.props.mediaEventHelper?.media.isEncrypted) {
|
||||||
try {
|
try {
|
||||||
[contentUrl, thumbUrl] = await Promise.all([
|
[contentUrl, thumbUrl] = await Promise.all([
|
||||||
this.props.mediaEventHelper.sourceUrl.value,
|
this.props.mediaEventHelper.sourceUrl.value,
|
||||||
|
@ -311,7 +311,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
|
||||||
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
|
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
|
||||||
isAnimated = false;
|
isAnimated = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events";
|
import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
@ -109,9 +109,9 @@ export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...pro
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={ref}>
|
||||||
<Caption>{_t("Ended a poll")}</Caption>
|
<Caption>{_t("Ended a poll")}</Caption>
|
||||||
<MPollBody mxEvent={pollStartEvent} {...props} />
|
<MPollBody mxEvent={pollStartEvent} {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||||
|
|
|
@ -143,7 +143,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
logger.error("Failed to load blurhash", e);
|
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 {
|
try {
|
||||||
const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
|
const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
|
||||||
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
|
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
|
// To stop subsequent download attempts
|
||||||
fetchingData: true,
|
fetchingData: true,
|
||||||
});
|
});
|
||||||
if (!this.props.mediaEventHelper.media.isEncrypted) {
|
if (!this.props.mediaEventHelper!.media.isEncrypted) {
|
||||||
this.setState({
|
this.setState({
|
||||||
error: "No file given in content",
|
error: "No file given in content",
|
||||||
});
|
});
|
||||||
|
@ -207,8 +207,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
}
|
}
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
|
decryptedUrl: await this.props.mediaEventHelper!.sourceUrl.value,
|
||||||
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
|
decryptedBlob: await this.props.mediaEventHelper!.sourceBlob.value,
|
||||||
fetchingData: false,
|
fetchingData: false,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import RedactedBody from "./RedactedBody";
|
||||||
import UnknownBody from "./UnknownBody";
|
import UnknownBody from "./UnknownBody";
|
||||||
import { IMediaBody } from "./IMediaBody";
|
import { IMediaBody } from "./IMediaBody";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import { ReactAnyComponent } from "../../../@types/common";
|
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import TextualBody from "./TextualBody";
|
import TextualBody from "./TextualBody";
|
||||||
|
@ -70,7 +69,7 @@ const baseBodyTypes = new Map<string, typeof React.Component>([
|
||||||
[MsgType.Audio, MVoiceOrAudioBody],
|
[MsgType.Audio, MVoiceOrAudioBody],
|
||||||
[MsgType.Video, MVideoBody],
|
[MsgType.Video, MVideoBody],
|
||||||
]);
|
]);
|
||||||
const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
|
const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
|
||||||
[EventType.Sticker, MStickerBody],
|
[EventType.Sticker, MStickerBody],
|
||||||
[M_POLL_START.name, MPollBody],
|
[M_POLL_START.name, MPollBody],
|
||||||
[M_POLL_START.altName, 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 body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
||||||
private mediaHelper?: MediaEventHelper;
|
private mediaHelper?: MediaEventHelper;
|
||||||
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
|
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 static contextType = MatrixClientContext;
|
||||||
public context!: React.ContextType<typeof 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.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 ?? {})) {
|
for (const [evType, evComponent] of Object.entries(this.props.overrideEventTypes ?? {})) {
|
||||||
this.evTypes.set(evType, evComponent);
|
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 content = this.props.mxEvent.getContent();
|
||||||
const type = this.props.mxEvent.getType();
|
const type = this.props.mxEvent.getType();
|
||||||
const msgtype = content.msgtype;
|
const msgtype = content.msgtype;
|
||||||
let BodyType: React.ComponentType<Partial<IBodyProps>> | ReactAnyComponent = RedactedBody;
|
let BodyType: React.ComponentType<IBodyProps> = RedactedBody;
|
||||||
if (!this.props.mxEvent.isRedacted()) {
|
if (!this.props.mxEvent.isRedacted()) {
|
||||||
// only resolve BodyType if event is not redacted
|
// only resolve BodyType if event is not redacted
|
||||||
if (this.props.mxEvent.isDecryptionFailure()) {
|
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 ? (
|
return BodyType ? (
|
||||||
<BodyType
|
<BodyType
|
||||||
ref={this.body}
|
ref={this.body}
|
||||||
|
|
|
@ -15,24 +15,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
import { IBodyProps } from "./IBodyProps";
|
||||||
|
|
||||||
interface IProps {
|
export default class MjolnirBody extends React.Component<IBodyProps> {
|
||||||
mxEvent: MatrixEvent;
|
|
||||||
onMessageAllowed: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MjolnirBody extends React.Component<IProps> {
|
|
||||||
private onAllowClick = (e: ButtonEvent): void => {
|
private onAllowClick = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
|
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
|
||||||
localStorage.setItem(key, "true");
|
localStorage.setItem(key, "true");
|
||||||
this.props.onMessageAllowed();
|
this.props.onMessageAllowed?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
|
|
|
@ -14,20 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from "react";
|
import React, { ForwardRefExoticComponent, useContext } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { formatFullDate } from "../../../DateUtils";
|
import { formatFullDate } from "../../../DateUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { IBodyProps } from "./IBodyProps";
|
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);
|
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||||
let text = _t("Message deleted");
|
let text = _t("Message deleted");
|
||||||
const unsigned = mxEvent.getUnsigned();
|
const unsigned = mxEvent.getUnsigned();
|
||||||
|
@ -49,6 +45,6 @@ const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, re
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||||
|
|
||||||
export default RedactedBody;
|
export default RedactedBody;
|
||||||
|
|
|
@ -15,15 +15,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef, ForwardRefExoticComponent } from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
interface IProps {
|
import { IBodyProps } from "./IBodyProps";
|
||||||
mxEvent: MatrixEvent;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default forwardRef<HTMLDivElement, IProps>(({ mxEvent, children }, ref) => {
|
export default forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, children }, ref) => {
|
||||||
const text = mxEvent.getContent().body;
|
const text = mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<div className="mx_UnknownBody" ref={ref}>
|
<div className="mx_UnknownBody" ref={ref}>
|
||||||
|
@ -31,4 +27,4 @@ export default forwardRef<HTMLDivElement, IProps>(({ mxEvent, children }, ref) =
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||||
|
|
|
@ -28,7 +28,7 @@ import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import ResizeHandle from "../elements/ResizeHandle";
|
import ResizeHandle from "../elements/ResizeHandle";
|
||||||
import Resizer from "../../../resizer/resizer";
|
import Resizer, { IConfig } from "../../../resizer/resizer";
|
||||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
||||||
|
@ -58,7 +58,7 @@ interface IState {
|
||||||
export default class AppsDrawer extends React.Component<IProps, IState> {
|
export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private resizeContainer?: HTMLDivElement;
|
private resizeContainer?: HTMLDivElement;
|
||||||
private resizer: Resizer;
|
private resizer: Resizer<IConfig>;
|
||||||
private dispatcherRef?: string;
|
private dispatcherRef?: string;
|
||||||
public static defaultProps: Partial<IProps> = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
showApps: true,
|
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
|
// 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...)
|
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
||||||
const classNames = {
|
const classNames = {
|
||||||
|
|
|
@ -243,7 +243,7 @@ const CreateSpaceButton: React.FC<Pick<IInnerSpacePanelProps, "isPanelCollapsed"
|
||||||
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||||
onClick={onNewClick}
|
onClick={onNewClick}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
ref={handle}
|
innerRef={handle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import React, {
|
||||||
createRef,
|
createRef,
|
||||||
InputHTMLAttributes,
|
InputHTMLAttributes,
|
||||||
LegacyRef,
|
LegacyRef,
|
||||||
forwardRef,
|
|
||||||
RefObject,
|
RefObject,
|
||||||
} from "react";
|
} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -59,124 +58,121 @@ interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButto
|
||||||
notificationState?: NotificationState;
|
notificationState?: NotificationState;
|
||||||
isNarrow?: boolean;
|
isNarrow?: boolean;
|
||||||
avatarSize?: number;
|
avatarSize?: number;
|
||||||
|
innerRef?: RefObject<HTMLElement>;
|
||||||
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
|
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
|
||||||
onClick?(ev?: ButtonEvent): void;
|
onClick?(ev?: ButtonEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceButton = forwardRef<HTMLElement, IButtonProps>(
|
export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
(
|
space,
|
||||||
{
|
spaceKey,
|
||||||
space,
|
className,
|
||||||
spaceKey,
|
selected,
|
||||||
className,
|
label,
|
||||||
selected,
|
contextMenuTooltip,
|
||||||
label,
|
notificationState,
|
||||||
contextMenuTooltip,
|
avatarSize,
|
||||||
notificationState,
|
isNarrow,
|
||||||
avatarSize,
|
children,
|
||||||
isNarrow,
|
innerRef,
|
||||||
children,
|
ContextMenuComponent,
|
||||||
ContextMenuComponent,
|
...props
|
||||||
...props
|
}) => {
|
||||||
},
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(innerRef);
|
||||||
ref: RefObject<HTMLElement>,
|
const [onFocus, isActive] = useRovingTabIndex(handle);
|
||||||
) => {
|
const tabIndex = isActive ? 0 : -1;
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(ref);
|
|
||||||
const [onFocus, isActive] = useRovingTabIndex(handle);
|
|
||||||
const tabIndex = isActive ? 0 : -1;
|
|
||||||
|
|
||||||
let avatar = (
|
let avatar = (
|
||||||
<div className="mx_SpaceButton_avatarPlaceholder">
|
<div className="mx_SpaceButton_avatarPlaceholder">
|
||||||
<div className="mx_SpaceButton_icon" />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
if (space) {
|
}
|
||||||
avatar = <RoomAvatar width={avatarSize} height={avatarSize} room={space} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let notifBadge;
|
let contextMenu: JSX.Element | undefined;
|
||||||
if (space && notificationState) {
|
if (space && menuDisplayed && handle.current && ContextMenuComponent) {
|
||||||
let ariaLabel = _t("Jump to first unread room.");
|
contextMenu = (
|
||||||
if (space.getMyMembership() === "invite") {
|
<ContextMenuComponent
|
||||||
ariaLabel = _t("Jump to first invite.");
|
{...toRightOf(handle.current.getBoundingClientRect(), 0)}
|
||||||
}
|
space={space}
|
||||||
|
onFinished={closeMenu}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
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> {
|
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
|
|
@ -105,7 +105,7 @@ const EVENT_TILE_TYPES = new Map<string, Factory>([
|
||||||
[M_POLL_END.altName, MessageEventFactory],
|
[M_POLL_END.altName, MessageEventFactory],
|
||||||
[EventType.KeyVerificationCancel, KeyVerificationConclFactory],
|
[EventType.KeyVerificationCancel, KeyVerificationConclFactory],
|
||||||
[EventType.KeyVerificationDone, 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>([
|
const STATE_EVENT_TILE_TYPES = new Map<string, Factory>([
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
||||||
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||||
import { Optional } from "matrix-events-sdk";
|
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 React from "react";
|
||||||
import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo";
|
import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo";
|
||||||
import { PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
import { PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||||
|
@ -81,23 +81,22 @@ export class ProxiedModuleApi implements ModuleApi {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public openDialog<
|
public openDialog<M extends object, P extends DialogProps, C extends DialogContent<P>>(
|
||||||
M extends object,
|
|
||||||
P extends DialogProps = DialogProps,
|
|
||||||
C extends React.Component = React.Component,
|
|
||||||
>(
|
|
||||||
title: string,
|
title: string,
|
||||||
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
|
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
|
||||||
|
props?: Omit<P, keyof DialogProps>,
|
||||||
): Promise<{ didOkOrSubmit: boolean; model: M }> {
|
): Promise<{ didOkOrSubmit: boolean; model: M }> {
|
||||||
return new Promise<{ didOkOrSubmit: boolean; model: M }>((resolve) => {
|
return new Promise<{ didOkOrSubmit: boolean; model: M }>((resolve) => {
|
||||||
Modal.createDialog(
|
Modal.createDialog(
|
||||||
ModuleUiDialog,
|
ModuleUiDialog<P, C>,
|
||||||
{
|
{
|
||||||
title: title,
|
title: title,
|
||||||
contentFactory: body,
|
contentFactory: body,
|
||||||
contentProps: <DialogProps>{
|
// Typescript isn't very happy understanding that `props` satisfies `Omit<P, keyof DialogProps>`
|
||||||
|
contentProps: {
|
||||||
|
...props,
|
||||||
moduleApi: this,
|
moduleApi: this,
|
||||||
},
|
} as unknown as P,
|
||||||
},
|
},
|
||||||
"mx_CompoundDialog",
|
"mx_CompoundDialog",
|
||||||
).finished.then(([didOkOrSubmit, model]) => {
|
).finished.then(([didOkOrSubmit, model]) => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import FixedDistributor from "./fixed";
|
import { BaseDistributor } from "./fixed";
|
||||||
import ResizeItem from "../item";
|
import ResizeItem from "../item";
|
||||||
import Resizer, { IConfig } from "../resizer";
|
import Resizer, { IConfig } from "../resizer";
|
||||||
import Sizer from "../sizer";
|
import Sizer from "../sizer";
|
||||||
|
@ -25,7 +25,7 @@ export interface ICollapseConfig extends IConfig {
|
||||||
isItemCollapsed(element: HTMLElement): boolean;
|
isItemCollapsed(element: HTMLElement): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CollapseItem extends ResizeItem<ICollapseConfig> {
|
export class CollapseItem extends ResizeItem<ICollapseConfig> {
|
||||||
public notifyCollapsed(collapsed: boolean): void {
|
public notifyCollapsed(collapsed: boolean): void {
|
||||||
this.resizer.config?.onCollapsed?.(collapsed, this.id, this.domNode);
|
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(
|
public static createItem(
|
||||||
resizeHandle: HTMLDivElement,
|
resizeHandle: HTMLDivElement,
|
||||||
resizer: Resizer<ICollapseConfig>,
|
resizer: Resizer<ICollapseConfig, CollapseItem>,
|
||||||
sizer: Sizer,
|
sizer: Sizer,
|
||||||
container?: HTMLElement,
|
container?: HTMLElement,
|
||||||
): CollapseItem {
|
): CollapseItem {
|
||||||
|
|
|
@ -18,21 +18,7 @@ import ResizeItem from "../item";
|
||||||
import Sizer from "../sizer";
|
import Sizer from "../sizer";
|
||||||
import Resizer, { IConfig } from "../resizer";
|
import Resizer, { IConfig } from "../resizer";
|
||||||
|
|
||||||
/**
|
export abstract class BaseDistributor<C extends IConfig, I extends ResizeItem<C> = ResizeItem<C>> {
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
|
public static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
|
||||||
return new Sizer(containerElement, vertical, reverse);
|
return new Sizer(containerElement, vertical, reverse);
|
||||||
}
|
}
|
||||||
|
@ -67,3 +53,22 @@ export default class FixedDistributor<C extends IConfig, I extends ResizeItem<an
|
||||||
this.item.finish();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
||||||
import Resizer, { IConfig } from "./resizer";
|
import Resizer, { IConfig } from "./resizer";
|
||||||
import Sizer from "./sizer";
|
import Sizer from "./sizer";
|
||||||
|
|
||||||
export default class ResizeItem<C extends IConfig = IConfig> {
|
export default class ResizeItem<C extends IConfig> {
|
||||||
public readonly domNode: HTMLElement;
|
public readonly domNode: HTMLElement;
|
||||||
protected readonly id: string | null;
|
protected readonly id: string | null;
|
||||||
protected reverse: boolean;
|
protected reverse: boolean;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
handle: HTMLElement,
|
handle: HTMLElement,
|
||||||
public readonly resizer: Resizer<C>,
|
public readonly resizer: Resizer<C, any>,
|
||||||
public readonly sizer: Sizer,
|
public readonly sizer: Sizer,
|
||||||
public readonly container?: HTMLElement,
|
public readonly container?: HTMLElement,
|
||||||
) {
|
) {
|
||||||
|
@ -37,12 +37,17 @@ export default class ResizeItem<C extends IConfig = IConfig> {
|
||||||
this.id = handle.getAttribute("data-id");
|
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;
|
const Ctor = this.constructor as typeof ResizeItem;
|
||||||
return new Ctor(handle, resizer, sizer, container);
|
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
|
// opposite direction from fromResizeHandle to get back to handle
|
||||||
let handle: Element | null | undefined = this.reverse
|
let handle: Element | null | undefined = this.reverse
|
||||||
? this.domNode.previousElementSibling
|
? 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);
|
return this.advance(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public previous(): ResizeItem | undefined {
|
public previous(): ResizeItem<C> | undefined {
|
||||||
return this.advance(false);
|
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);
|
this.resizer.config?.onResized?.(null, this.id, this.domNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public first(): ResizeItem | undefined {
|
public first(): ResizeItem<C> | undefined {
|
||||||
if (!this.domNode.parentElement?.children) {
|
if (!this.domNode.parentElement?.children) {
|
||||||
return;
|
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) {
|
if (!this.domNode.parentElement?.children) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export interface IConfig {
|
||||||
handler?: HTMLDivElement;
|
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;
|
private classNames: IClassNames;
|
||||||
|
|
||||||
// TODO move vertical/horizontal to config option/container class
|
// TODO move vertical/horizontal to config option/container class
|
||||||
|
@ -46,13 +46,8 @@ export default class Resizer<C extends IConfig = IConfig> {
|
||||||
public constructor(
|
public constructor(
|
||||||
public container: HTMLElement | null,
|
public container: HTMLElement | null,
|
||||||
private readonly distributorCtor: {
|
private readonly distributorCtor: {
|
||||||
new (item: ResizeItem): FixedDistributor<C, any>;
|
new (item: I): FixedDistributor<C, I>;
|
||||||
createItem(
|
createItem(resizeHandle: HTMLDivElement, resizer: Resizer<C, I>, sizer: Sizer, container?: HTMLElement): I;
|
||||||
resizeHandle: HTMLDivElement,
|
|
||||||
resizer: Resizer,
|
|
||||||
sizer: Sizer,
|
|
||||||
container?: HTMLElement,
|
|
||||||
): ResizeItem;
|
|
||||||
createSizer(containerElement: HTMLElement | null, vertical: boolean, reverse: boolean): Sizer;
|
createSizer(containerElement: HTMLElement | null, vertical: boolean, reverse: boolean): Sizer;
|
||||||
},
|
},
|
||||||
public readonly config?: C,
|
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
|
@param {number} handleIndex the index of the resize handle in the container
|
||||||
@return {FixedDistributor} a new distributor for the given handle
|
@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 handles = this.getResizeHandles();
|
||||||
const handle = handles[handleIndex];
|
const handle = handles[handleIndex];
|
||||||
if (handle) {
|
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 handles = this.getResizeHandles();
|
||||||
const handle = handles.find((h) => h.getAttribute("data-id") === id);
|
const handle = handles.find((h) => h.getAttribute("data-id") === id);
|
||||||
if (handle) {
|
if (handle) {
|
||||||
|
@ -178,7 +173,7 @@ export default class Resizer<C extends IConfig = IConfig> {
|
||||||
{ trailing: true, leading: true },
|
{ trailing: true, leading: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
public getDistributors = (): FixedDistributor<any, ResizeItem<any>>[] => {
|
public getDistributors = (): FixedDistributor<C, I>[] => {
|
||||||
return this.getResizeHandles().map((handle) => {
|
return this.getResizeHandles().map((handle) => {
|
||||||
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
|
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
|
||||||
return distributor;
|
return distributor;
|
||||||
|
@ -187,7 +182,7 @@ export default class Resizer<C extends IConfig = IConfig> {
|
||||||
|
|
||||||
private createSizerAndDistributor(resizeHandle: HTMLDivElement): {
|
private createSizerAndDistributor(resizeHandle: HTMLDivElement): {
|
||||||
sizer: Sizer;
|
sizer: Sizer;
|
||||||
distributor: FixedDistributor<any>;
|
distributor: FixedDistributor<C, I>;
|
||||||
} {
|
} {
|
||||||
const vertical = resizeHandle.classList.contains(this.classNames.vertical!);
|
const vertical = resizeHandle.classList.contains(this.classNames.vertical!);
|
||||||
const reverse = this.isReverseResizeHandle(resizeHandle);
|
const reverse = this.isReverseResizeHandle(resizeHandle);
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"lib": ["es2020", "dom", "dom.iterable"],
|
"lib": ["es2020", "dom", "dom.iterable"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictFunctionTypes": false,
|
|
||||||
"useUnknownInCatchVariables": false
|
"useUnknownInCatchVariables": false
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
@ -1619,10 +1619,10 @@
|
||||||
version "3.2.14"
|
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"
|
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":
|
"@matrix-org/react-sdk-module-api@^0.0.6":
|
||||||
version "0.0.5"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.5.tgz#78bd80f42b918394978965ef3e08496e97948c7a"
|
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.6.tgz#941872ed081acdca9d247ccd6e146265aa24010b"
|
||||||
integrity sha512-QhH1T1E6Q6csCUitQzm32SRnX49Ox73TF5BZ4p5TOGFpPD3QuYc5/dDC1Yh3xUljgqOS2C6H24qaskw6olCtfQ==
|
integrity sha512-FydbJYSMecpDIGk4fVQ9djjckQdbJPV9bH3px78TQ+MX/WHmzPmjEpMPTeP3uDSeg0EWmfoIFdNypJglMqAHpw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.17.9"
|
"@babel/runtime" "^7.17.9"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue