feat: Native video display (#5866)

This commit is contained in:
Tom Moor
2023-09-28 20:28:09 -04:00
committed by GitHub
parent bd06e03b1e
commit f4fd9dae5f
24 changed files with 840 additions and 344 deletions

View File

@@ -1,23 +1,29 @@
import * as Sentry from "@sentry/react";
import invariant from "invariant";
import { NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { v4 as uuidv4 } from "uuid";
import FileHelper from "../lib/FileHelper";
import uploadPlaceholderPlugin, {
findPlaceholder,
} from "../lib/uploadPlaceholder";
import findAttachmentById from "../queries/findAttachmentById";
export type Options = {
/** Dictionary object containing translation strings */
dictionary: any;
/** Set to true to force images to become attachments */
/** Set to true to force images and videos to become file attachments */
isAttachment?: boolean;
/** Set to true to replace any existing image at the users selection */
replaceExisting?: boolean;
/** Callback fired to upload a file */
uploadFile?: (file: File) => Promise<string>;
/** Callback fired when the user starts a file upload */
onFileUploadStart?: () => void;
/** Callback fired when the user completes a file upload */
onFileUploadStop?: () => void;
/** Callback fired when a toast needs to be displayed */
onShowToast: (message: string) => void;
/** Attributes to overwrite */
attrs?: {
/** Width to use when inserting image */
width?: number;
@@ -44,19 +50,12 @@ const insertFiles = function (
onShowToast,
} = options;
invariant(
uploadFile,
"uploadFile callback must be defined to handle uploads."
);
// okay, we have some dropped files and a handler lets stop this
// event going any further up the stack
event.preventDefault();
// let the user know we're starting to process the files
if (onFileUploadStart) {
onFileUploadStart();
}
onFileUploadStart?.();
const { schema } = view.state;
@@ -66,7 +65,10 @@ const insertFiles = function (
const filesToUpload = files.map((file) => ({
id: `upload-${uuidv4()}`,
isImage: file.type.startsWith("image/") && !options.isAttachment,
isImage:
FileHelper.isImage(file) && !options.isAttachment && !!schema.nodes.image,
isVideo:
FileHelper.isVideo(file) && !options.isAttachment && !!schema.nodes.video,
file,
}));
@@ -75,11 +77,6 @@ const insertFiles = function (
const { tr } = view.state;
if (upload.isImage) {
// Skip if the editor does not support images.
if (!view.state.schema.nodes.image) {
continue;
}
// insert a placeholder at this position, or mark an existing file as being
// replaced
tr.setMeta(uploadPlaceholderPlugin, {
@@ -92,6 +89,18 @@ const insertFiles = function (
},
});
view.dispatch(tr);
} else if (upload.isVideo) {
// insert a placeholder at this position, or mark an existing file as being
// replaced
tr.setMeta(uploadPlaceholderPlugin, {
add: {
id: upload.id,
file: upload.file,
pos,
isVideo: true,
},
});
view.dispatch(tr);
} else if (!attachmentPlaceholdersSet) {
// Skip if the editor does not support attachments.
if (!view.state.schema.nodes.attachment) {
@@ -108,7 +117,7 @@ const insertFiles = function (
attachmentsToUpload.map((attachment) =>
schema.nodes.attachment.create({
id: attachment.id,
title: attachment.file.name ?? "Untitled",
title: attachment.file.name ?? dictionary.untitled,
size: attachment.file.size,
})
)
@@ -120,8 +129,8 @@ const insertFiles = function (
// start uploading the file to the server. Using "then" syntax
// to allow all placeholders to be entered at once with the uploads
// happening in the background in parallel.
uploadFile(upload.file)
.then((src) => {
uploadFile?.(upload.file)
.then(async (src) => {
if (upload.isImage) {
const newImg = new Image();
newImg.onload = () => {
@@ -161,6 +170,44 @@ const insertFiles = function (
};
newImg.src = src;
} else if (upload.isVideo) {
const result = findPlaceholder(view.state, upload.id);
// if the content around the placeholder has been deleted
// then forget about inserting this file
if (result === null) {
return;
}
const [from, to] = result;
const dimensions = await FileHelper.getVideoDimensions(upload.file);
view.dispatch(
view.state.tr
.replaceWith(
from,
to || from,
schema.nodes.video.create({
src,
title: upload.file.name ?? dictionary.untitled,
width: dimensions.width,
height: dimensions.height,
...options.attrs,
})
)
.setMeta(uploadPlaceholderPlugin, { remove: { id: upload.id } })
);
// If the users selection is still at the file then make sure to select
// the entire node once done. Otherwise, if the selection has moved
// elsewhere then we don't want to modify it
if (view.state.selection.from === from) {
view.dispatch(
view.state.tr.setSelection(
new NodeSelection(view.state.doc.resolve(from))
)
);
}
} else {
const result = findAttachmentById(view.state, upload.id);
@@ -176,7 +223,7 @@ const insertFiles = function (
to || from,
schema.nodes.attachment.create({
href: src,
title: upload.file.name ?? "Untitled",
title: upload.file.name ?? dictionary.untitled,
size: upload.file.size,
})
)
@@ -198,7 +245,7 @@ const insertFiles = function (
Sentry.captureException(error);
// cleanup the placeholder if there is a failure
if (upload.isImage) {
if (upload.isImage || upload.isVideo) {
view.dispatch(
view.state.tr.setMeta(uploadPlaceholderPlugin, {
remove: { id: upload.id },