Rework Upload internals to use MVVM (#33307)
* Initial reword of upload to MVVM. * Update tests * More incremental improvements * Refactor tests to use helper method for composer uploads. * Add drag and drop tests * lint * Add commentary * fixup test * More precise selector * Retarget uploads * lint * fixup * one more type * update snap * Fixup composerUploadFiles * fix import * lint * Copy and paste fixes too * Add tests for pasting * Add tests for pasting files. * Remove redundant fn * rm comment * tidy up * Test cleanup * More clean up * another fix * Use condensed version * Cleanup tests * more cleaning * last bity * s/throw Error/throw new Error/
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -7,16 +8,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { useViewModel } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
import UploadBigSvg from "../../../res/img/upload-big.svg";
|
||||
import { useRoomState } from "../../hooks/useRoomState.ts";
|
||||
import { useRoomUploadViewModel } from "../../viewmodels/room/RoomUploadViewModel";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
parent: HTMLElement | null;
|
||||
onFileDrop(this: void, dataTransfer: DataTransfer): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -24,15 +23,16 @@ interface IState {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop, room }) => {
|
||||
const FileDropTarget: React.FC<IProps> = ({ parent }) => {
|
||||
const [state, setState] = useState<IState>({
|
||||
dragging: false,
|
||||
counter: 0,
|
||||
});
|
||||
const hasPermission = useRoomState(room, (state) => state.maySendMessage(room.client.getUserId()!));
|
||||
const vm = useRoomUploadViewModel();
|
||||
const { mayUpload } = useViewModel(vm);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPermission || !parent || parent.ondrop) return;
|
||||
if (!mayUpload || !parent || parent.ondrop) return;
|
||||
|
||||
const onDragEnter = (ev: DragEvent): void => {
|
||||
ev.stopPropagation();
|
||||
@@ -83,7 +83,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop, room }) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!ev.dataTransfer) return;
|
||||
onFileDrop(ev.dataTransfer);
|
||||
void vm.initiateViaDataTransfer(ev.dataTransfer);
|
||||
|
||||
setState((state) => ({
|
||||
dragging: false,
|
||||
@@ -106,9 +106,9 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop, room }) => {
|
||||
parent?.removeEventListener("dragenter", onDragEnter);
|
||||
parent?.removeEventListener("dragleave", onDragLeave);
|
||||
};
|
||||
}, [parent, onFileDrop, hasPermission]);
|
||||
}, [parent, mayUpload, vm]);
|
||||
|
||||
if (hasPermission && state.dragging) {
|
||||
if (mayUpload && state.dragging) {
|
||||
return (
|
||||
<div className="mx_FileDropTarget">
|
||||
<img src={UploadBigSvg} className="mx_FileDropTarget_image" alt="" />
|
||||
|
||||
@@ -142,6 +142,7 @@ import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
||||
import { EncryptionEventViewModel } from "../../viewmodels/room/timeline/event-tile/EncryptionEventViewModel.ts";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
import { RoomUploadContextProvider } from "../../viewmodels/room/RoomUploadViewModel.tsx";
|
||||
import { EventPresentationContextProvider } from "../../utils/EventPresentationContextProvider";
|
||||
|
||||
const DEBUG = false;
|
||||
@@ -302,7 +303,6 @@ interface LocalRoomViewProps {
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
roomView: RefObject<HTMLElement | null>;
|
||||
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
||||
mainSplitContentType: MainSplitContentType;
|
||||
e2eStatus?: E2EStatus;
|
||||
}
|
||||
@@ -343,17 +343,19 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
<div className="mx_RoomView mx_RoomView--local">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader room={room} />
|
||||
<main className="mx_RoomView_body" ref={props.roomView} aria-label={_t("room|room_content")}>
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} room={room} />
|
||||
<div className="mx_RoomView_timeline">
|
||||
<ScrollPanel className="mx_RoomView_messagePanel">
|
||||
{encryptionTile}
|
||||
<NewRoomIntro />
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
{statusBar}
|
||||
{composer}
|
||||
</main>
|
||||
<RoomUploadContextProvider>
|
||||
<main className="mx_RoomView_body" ref={props.roomView} aria-label={_t("room|room_content")}>
|
||||
<FileDropTarget parent={props.roomView.current} />
|
||||
<div className="mx_RoomView_timeline">
|
||||
<ScrollPanel className="mx_RoomView_messagePanel">
|
||||
{encryptionTile}
|
||||
<NewRoomIntro />
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
{statusBar}
|
||||
{composer}
|
||||
</main>
|
||||
</RoomUploadContextProvider>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
@@ -2121,19 +2123,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
});
|
||||
}
|
||||
|
||||
private onFileDrop = async (dataTransfer: DataTransfer): Promise<void> => {
|
||||
const roomId = this.getRoomId();
|
||||
if (!roomId || !this.context.client) return;
|
||||
await ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
roomId,
|
||||
undefined,
|
||||
this.state.replyToEvent,
|
||||
this.context.client,
|
||||
TimelineRenderingType.Room,
|
||||
);
|
||||
};
|
||||
|
||||
private onMeasurement = (narrow: boolean): void => {
|
||||
this.setState({ narrow });
|
||||
};
|
||||
@@ -2169,7 +2158,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
roomView={this.roomView}
|
||||
onFileDrop={this.onFileDrop}
|
||||
mainSplitContentType={this.state.mainSplitContentType}
|
||||
/>
|
||||
</ScopedRoomContextProvider>
|
||||
@@ -2673,16 +2661,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
case MainSplitContentType.Timeline:
|
||||
mainSplitContentClassName = "mx_MainSplit_timeline";
|
||||
mainSplitBody = (
|
||||
<>
|
||||
<RoomUploadContextProvider>
|
||||
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
|
||||
{auxPanel}
|
||||
{pinnedMessageBanner}
|
||||
<main className={timelineClasses} data-testid="timeline">
|
||||
<FileDropTarget
|
||||
parent={this.roomView.current}
|
||||
onFileDrop={this.onFileDrop}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<FileDropTarget parent={this.roomView.current} />
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
@@ -2691,7 +2675,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
{statusBarArea}
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</>
|
||||
</RoomUploadContextProvider>
|
||||
);
|
||||
break;
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
|
||||
@@ -29,7 +29,6 @@ import TimelinePanel from "./TimelinePanel";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { type ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { type E2EStatus } from "../../utils/ShieldUtils";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
@@ -51,6 +50,7 @@ import { type ComposerInsertPayload, ComposerType } from "../../dispatcher/paylo
|
||||
import Heading from "../views/typography/Heading";
|
||||
import { type ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
|
||||
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
|
||||
import { RoomUploadContextProvider } from "../../viewmodels/room/RoomUploadViewModel.tsx";
|
||||
import { EventPresentationContextProvider } from "../../utils/EventPresentationContextProvider";
|
||||
|
||||
interface IProps {
|
||||
@@ -329,22 +329,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
private onFileDrop = (dataTransfer: DataTransfer): void => {
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
if (roomId) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
roomId,
|
||||
this.threadRelation,
|
||||
this.context.replyToEvent,
|
||||
MatrixClientPeg.safeGet(),
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
} else {
|
||||
console.warn("Unknwon roomId for event", this.props.mxEvent);
|
||||
}
|
||||
};
|
||||
|
||||
private get threadRelation(): IEventRelation {
|
||||
const relation: IEventRelation = {
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
@@ -393,7 +377,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
|
||||
timeline = (
|
||||
<>
|
||||
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} room={this.props.room} />
|
||||
<FileDropTarget parent={this.card.current} />
|
||||
<EventPresentationContextProvider layout={layout}>
|
||||
<TimelinePanel
|
||||
key={this.state.thread.id}
|
||||
@@ -437,38 +421,40 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
|
||||
narrow={this.state.narrow}
|
||||
>
|
||||
<BaseCard
|
||||
className={classNames("mx_ThreadView mx_ThreadPanel", {
|
||||
mx_ThreadView_narrow: this.state.narrow,
|
||||
})}
|
||||
onClose={this.props.onClose}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderThreadViewHeader()}
|
||||
ref={this.card}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onBack={(ev: ButtonEvent) => {
|
||||
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
|
||||
}}
|
||||
>
|
||||
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
|
||||
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
|
||||
<RoomUploadContextProvider threadRelation={this.threadRelation}>
|
||||
<BaseCard
|
||||
className={classNames("mx_ThreadView mx_ThreadPanel", {
|
||||
mx_ThreadView_narrow: this.state.narrow,
|
||||
})}
|
||||
onClose={this.props.onClose}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderThreadViewHeader()}
|
||||
ref={this.card}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onBack={(ev: ButtonEvent) => {
|
||||
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
|
||||
}}
|
||||
>
|
||||
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
|
||||
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
|
||||
|
||||
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (
|
||||
<UploadBar room={this.props.room} relation={threadRelation} />
|
||||
)}
|
||||
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (
|
||||
<UploadBar room={this.props.room} relation={threadRelation} />
|
||||
)}
|
||||
|
||||
{this.state.thread?.timelineSet && (
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
relation={threadRelation}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
</BaseCard>
|
||||
{this.state.thread?.timelineSet && (
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
relation={threadRelation}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
</BaseCard>
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user