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:
@@ -5,8 +5,15 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BaseViewModel, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
||||
import {
|
||||
_t,
|
||||
BaseViewModel,
|
||||
type UploadButtonViewActions,
|
||||
type UploadButtonViewSnapshot,
|
||||
useCreateAutoDisposedViewModel,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||
import { AttachmentIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import React, {
|
||||
type ChangeEventHandler,
|
||||
createContext,
|
||||
@@ -24,30 +31,32 @@ import {
|
||||
RoomEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { ComposerApiFileUploadOption } from "@element-hq/element-web-module-api";
|
||||
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||
import { useMatrixClientContext } from "../../contexts/MatrixClientContext";
|
||||
import ContentMessages from "../../ContentMessages";
|
||||
import type { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { chromeFileInputFix } from "../../utils/BrowserWorkarounds";
|
||||
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ModuleApi } from "../../modules/Api";
|
||||
import { ModuleComposerApiEvents } from "../../modules/ComposerApi";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import type { ComposerInsertFilesPayload } from "../../dispatcher/payloads/ComposerInsertFilePayload";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import type { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
||||
const logger = rootLogger.getChild("RoomUploadViewModel");
|
||||
|
||||
export interface RoomUploadViewSnapshot {
|
||||
mayUpload: boolean;
|
||||
}
|
||||
|
||||
export interface RoomUploadViewActions {
|
||||
initiateViaInputFiles(files: FileList | null): Promise<void>;
|
||||
initiateViaDataTransfer(dataTransfer: DataTransfer): Promise<void>;
|
||||
openUploadDialog(): void;
|
||||
interface RoomUploadViewSnapshot extends UploadButtonViewSnapshot {
|
||||
mayDragAndDropFile: boolean;
|
||||
}
|
||||
|
||||
export class RoomUploadViewModel
|
||||
extends BaseViewModel<RoomUploadViewSnapshot, Record<string, never>>
|
||||
implements RoomUploadViewActions
|
||||
implements UploadButtonViewActions
|
||||
{
|
||||
private readonly uploadSelectFns = new Map<string, ComposerApiFileUploadOption["onSelected"]>();
|
||||
public constructor(
|
||||
private readonly room: Room,
|
||||
private readonly client: MatrixClient,
|
||||
@@ -56,22 +65,66 @@ export class RoomUploadViewModel
|
||||
private replyToEvent: MatrixEvent | undefined,
|
||||
private threadRelation: IEventRelation | undefined,
|
||||
public readonly openUploadDialog: () => void,
|
||||
private readonly moduleComposerApi = ModuleApi.instance.composer,
|
||||
) {
|
||||
super(
|
||||
{},
|
||||
{
|
||||
mayUpload: room.maySendMessage(),
|
||||
options: [],
|
||||
mayDragAndDropFile: false,
|
||||
},
|
||||
);
|
||||
// Initial check.
|
||||
this.onRoomCurrentStateUpdated();
|
||||
// Configure upload functions
|
||||
for (const option of moduleComposerApi.fileUploadOptions) {
|
||||
this.uploadSelectFns.set(option.type, option.onSelected);
|
||||
}
|
||||
this.uploadSelectFns.set("local", this.openUploadDialog);
|
||||
room.on(RoomEvent.CurrentStateUpdated, this.onRoomCurrentStateUpdated);
|
||||
this.disposables.track(() => {
|
||||
room.off(RoomEvent.CurrentStateUpdated, this.onRoomCurrentStateUpdated);
|
||||
});
|
||||
this.disposables.trackListener(room, RoomEvent.CurrentStateUpdated, this.onRoomCurrentStateUpdated);
|
||||
|
||||
moduleComposerApi.on(ModuleComposerApiEvents.UploaderOptionsChanged, this.onUploaderOptionsChanged);
|
||||
this.disposables.trackListener(
|
||||
moduleComposerApi,
|
||||
ModuleComposerApiEvents.UploaderOptionsChanged,
|
||||
// Types issue.
|
||||
this.onUploaderOptionsChanged as any,
|
||||
);
|
||||
}
|
||||
|
||||
private onRoomCurrentStateUpdated = (): void => {
|
||||
const maySendMessage = this.room.maySendMessage();
|
||||
this.snapshot.merge({
|
||||
mayUpload: this.room.maySendMessage(),
|
||||
mayDragAndDropFile: maySendMessage,
|
||||
options: maySendMessage
|
||||
? [
|
||||
{
|
||||
type: "local",
|
||||
label: _t("common|attachment"),
|
||||
icon: AttachmentIcon,
|
||||
},
|
||||
...this.moduleComposerApi.fileUploadOptions.map((option) => ({
|
||||
type: option.type,
|
||||
label: option.label,
|
||||
icon: option.icon,
|
||||
})),
|
||||
]
|
||||
: [],
|
||||
});
|
||||
};
|
||||
|
||||
private readonly onUploaderOptionsChanged = (option: ComposerApiFileUploadOption): void => {
|
||||
this.uploadSelectFns.set(option.type, option.onSelected);
|
||||
this.snapshot.merge({
|
||||
options: [
|
||||
...this.snapshot.current.options,
|
||||
{
|
||||
type: option.type,
|
||||
label: option.label,
|
||||
icon: option.icon,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -127,6 +180,28 @@ export class RoomUploadViewModel
|
||||
}
|
||||
};
|
||||
|
||||
public onUploadOptionSelected = (type: ComposerApiFileUploadOption["type"]): void => {
|
||||
const fn = this.uploadSelectFns.get(type);
|
||||
if (!fn) {
|
||||
throw new Error("Unexpectedly called onUploadOptionSelected with an unknown type");
|
||||
}
|
||||
// At the point of this function being called, we should be in a state that is either rendering a room
|
||||
// or timeline.
|
||||
if (![TimelineRenderingType.Room, TimelineRenderingType.Thread].includes(this.timelineRenderingType)) {
|
||||
throw new Error("TimelineRenderingType must be Room or Thread");
|
||||
}
|
||||
fn(
|
||||
this.room.roomId,
|
||||
{
|
||||
view: this.timelineRenderingType === TimelineRenderingType.Room ? "room" : "thread",
|
||||
},
|
||||
{
|
||||
inReplyToEventId: this.replyToEvent?.getId(),
|
||||
relType: this.threadRelation?.rel_type,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
private checkCanUpload(): boolean {
|
||||
if (this.client.isGuest()) {
|
||||
this.dispatcher.dispatch({ action: "require_registration" });
|
||||
@@ -175,6 +250,7 @@ export function RoomUploadContextProvider({
|
||||
return new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
// Checked earlier
|
||||
timelineRenderingType,
|
||||
defaultDispatcher,
|
||||
replyToEvent,
|
||||
@@ -208,6 +284,21 @@ export function RoomUploadContextProvider({
|
||||
[vm],
|
||||
);
|
||||
|
||||
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
|
||||
if (payload.action !== Action.ComposerFileInsert) {
|
||||
return;
|
||||
}
|
||||
const fileInsert = payload as ComposerInsertFilesPayload;
|
||||
if (fileInsert.timelineRenderingType === timelineRenderingType) {
|
||||
logger.info(
|
||||
`Got ComposerFileInsert with ${fileInsert.files.length} files`,
|
||||
timelineRenderingType,
|
||||
threadRelation,
|
||||
);
|
||||
vm.initiateViaInputFiles(fileInsert.files);
|
||||
}
|
||||
});
|
||||
|
||||
// Note, while this logic could be largely replaced with https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker
|
||||
// it does not enjoy support across all our target platforms.
|
||||
// Therefore, we use the invisible input element trick.
|
||||
|
||||
Reference in New Issue
Block a user