Module API for adding new file upload mechanisms (#33355)

* 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

* Begin fleshing out

* Park changes

* More stuff

* Use condensed version

* Cleanup tests

* more cleaning

* last bity

* Add a test for the composer

* Park up changes

* Rewrite Measured to be a functional component

* Add tests to cover narrow viewports

* lint

* breakpoint is optional

* Cleanup

* Support narrow mode

* fixup

* begone

* Provide default value

* add label

* fixup test

* update copyright

* cleanup

* Be a bit more lazy with FileDropTarget

* remove a debug statement

* Fixup

* fix two snaps

* Update screenshot

* and the other one

* Update snaps

* unfake CIDER

* update screens again

* remove extra test

* Undo accidental snapshots

* Bit of tidyup

* fixup

* even more tidyup

* may drag and drop file

* tidy up again

* snap snap snap

* Use load to make sonarQube happy

* Bunch of refactors

* More cleanup

* cleanup debug code

* tweaks

* remove a test we no longer need

* make it happy

* fix import

* fixup

* Update snaps

* typo

* one off

* Add tests

* lint

* remove only

* Reduce screenshot scope

* fix snapshot usage

* cleanup
This commit is contained in:
Will Hunt
2026-05-18 21:41:38 +00:00
committed by GitHub
parent 66b739fea9
commit 02b6520f09
30 changed files with 1627 additions and 831 deletions
@@ -29,10 +29,10 @@ const FileDropTarget: React.FC<IProps> = ({ parent }) => {
counter: 0,
});
const vm = useRoomUploadViewModel();
const { mayUpload } = useViewModel(vm);
const { mayDragAndDropFile } = useViewModel(vm);
useEffect(() => {
if (!mayUpload || !parent || parent.ondrop) return;
if (!mayDragAndDropFile || !parent || parent.ondrop) return;
const onDragEnter = (ev: DragEvent): void => {
ev.stopPropagation();
@@ -106,9 +106,9 @@ const FileDropTarget: React.FC<IProps> = ({ parent }) => {
parent?.removeEventListener("dragenter", onDragEnter);
parent?.removeEventListener("dragleave", onDragLeave);
};
}, [parent, mayUpload, vm]);
}, [parent, mayDragAndDropFile, vm]);
if (mayUpload && state.dragging) {
if (mayDragAndDropFile && state.dragging) {
return (
<div className="mx_FileDropTarget">
<img src={UploadBigSvg} className="mx_FileDropTarget_image" alt="" />
@@ -1299,7 +1299,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const composerInsertPayload = payload as ComposerInsertPayload;
if (composerInsertPayload.composerType) break;
let timelineRenderingType: TimelineRenderingType | undefined;
let timelineRenderingType = composerInsertPayload.timelineRenderingType;
// ThreadView handles Action.ComposerInsert itself due to it having its own editState
if (composerInsertPayload.timelineRenderingType === TimelineRenderingType.Thread) break;
if (
@@ -1311,12 +1311,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
timelineRenderingType = TimelineRenderingType.Room;
}
// If the dispatchee didn't request a timeline rendering type, use the current one.
timelineRenderingType =
timelineRenderingType ??
composerInsertPayload.timelineRenderingType ??
this.state.timelineRenderingType;
// re-dispatch to the correct composer
defaultDispatcher.dispatch<ComposerInsertPayload>({
...composerInsertPayload,
@@ -17,13 +17,13 @@ import {
} from "matrix-js-sdk/src/matrix";
import React, { type JSX, createContext, type ReactElement, type ReactNode, useContext } from "react";
import {
AttachmentIcon,
MicOnIcon,
OverflowHorizontalIcon,
PollsIcon,
StickerIcon,
TextFormattingIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { UploadButton, useViewModel } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { CollapsibleButton } from "./CollapsibleButton";
@@ -34,7 +34,10 @@ import Modal from "../../../Modal";
import PollCreateDialog from "../elements/PollCreateDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import { EmojiButton } from "./EmojiButton";
import { filterBoolean } from "../../../utils/arrays";
import { useSettingValue } from "../../../hooks/useSettings";
@@ -64,6 +67,8 @@ export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient = useContext(MatrixClientContext);
const roomUploadVM = useRoomUploadViewModel();
const roomUploadSnapshot = useViewModel(roomUploadVM);
const { room, narrow } = useScopedRoomContext("room", "narrow");
const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer");
@@ -87,7 +92,15 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
),
];
moreButtons = [
uploadButton(), // props passed via UploadButtonContext
// This a textual list of buttons, so we can't use the UploadButton here.
roomUploadSnapshot.options.map(({ type, icon: Icon, label }) => (
<IconizedContextMenuOption
onClick={() => roomUploadVM.onUploadOptionSelected(type)}
icon={Icon && <Icon />}
label={label}
key={type}
/>
)),
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton ? pollButton(room, props.relation) : null,
@@ -104,7 +117,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
) : (
emojiButton(props)
),
uploadButton(), // props passed via UploadButtonContext
<UploadButton key="upload" vm={roomUploadVM} />,
];
moreButtons = [
showStickersButton(props),
@@ -162,27 +175,6 @@ function emojiButton(props: IProps): ReactElement {
);
}
function uploadButton(): ReactElement {
return <UploadButton key="controls_upload" />;
}
// Must be rendered within an UploadButtonContextProvider
const UploadButton: React.FC = () => {
const overflowMenuCloser = useContext(OverflowMenuContext);
const vm = useRoomUploadViewModel();
const onClick = (): void => {
vm.openUploadDialog();
overflowMenuCloser?.(); // close overflow menu
};
return (
<CollapsibleButton className="mx_MessageComposer_button" onClick={onClick} title={_t("common|attachment")}>
<AttachmentIcon />
</CollapsibleButton>
);
};
function showStickersButton(props: IProps): ReactElement | null {
return props.showStickersButton ? (
<CollapsibleButton
@@ -296,5 +288,4 @@ function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonP
</CollapsibleButton>
);
}
export default MessageComposerButtons;