From cd4f3f9ff211529fa10d89bba4b3f0eb11bc5fd7 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 18 May 2024 12:17:04 -0400 Subject: [PATCH] Use inline content disposition for common images and PDFs (#6924) * Use inline content disposition for common images and PDFs * Add double-click on widgets to download --- plugins/storage/server/api/files.ts | 4 +++- server/storage/files/BaseStorage.ts | 25 +++++++++++++++++++++++++ server/storage/files/S3Storage.ts | 5 ++--- shared/editor/components/Widget.tsx | 20 +++++++++++++++++--- shared/editor/nodes/Attachment.tsx | 4 ++++ 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/plugins/storage/server/api/files.ts b/plugins/storage/server/api/files.ts index 6b5c90736..4aa5601eb 100644 --- a/plugins/storage/server/api/files.ts +++ b/plugins/storage/server/api/files.ts @@ -94,7 +94,9 @@ router.get( ctx.set("Cache-Control", cacheHeader); ctx.set("Content-Type", attachment.contentType); - ctx.attachment(attachment.name); + ctx.attachment(attachment.name, { + type: FileStorage.getContentDisposition(attachment.contentType), + }); ctx.body = attachment.stream; } } diff --git a/server/storage/files/BaseStorage.ts b/server/storage/files/BaseStorage.ts index 9ad7a0e7b..d3b3d8e0b 100644 --- a/server/storage/files/BaseStorage.ts +++ b/server/storage/files/BaseStorage.ts @@ -214,4 +214,29 @@ export default abstract class BaseStorage { * @returns A promise that resolves when the file is deleted */ public abstract deleteFile(key: string): Promise; + + /** + * Returns the content disposition for a given content type. + * + * @param contentType The content type + * @returns The content disposition + */ + public getContentDisposition(contentType?: string) { + if (contentType && this.safeInlineContentTypes.includes(contentType)) { + return "inline"; + } + + return "attachment"; + } + + /** + * A list of content types considered safe to display inline in the browser. + */ + protected safeInlineContentTypes = [ + "application/pdf", + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + ]; } diff --git a/server/storage/files/S3Storage.ts b/server/storage/files/S3Storage.ts index 9edd04e11..ba59b8310 100644 --- a/server/storage/files/S3Storage.ts +++ b/server/storage/files/S3Storage.ts @@ -38,7 +38,7 @@ export default class S3Storage extends BaseStorage { ["starts-with", "$Cache-Control", ""], ]), Fields: { - "Content-Disposition": "attachment", + "Content-Disposition": this.getContentDisposition(contentType), key, acl, }, @@ -114,7 +114,7 @@ export default class S3Storage extends BaseStorage { Key: key, ContentType: contentType, ContentLength: contentLength, - ContentDisposition: "attachment", + ContentDisposition: this.getContentDisposition(contentType), Body: body, }) .promise(); @@ -145,7 +145,6 @@ export default class S3Storage extends BaseStorage { Bucket: env.AWS_S3_UPLOAD_BUCKET_NAME, Key: key, Expires: expiresIn, - ResponseContentDisposition: "attachment", }; const url = isDocker diff --git a/shared/editor/components/Widget.tsx b/shared/editor/components/Widget.tsx index 215171aae..74fddb8f8 100644 --- a/shared/editor/components/Widget.tsx +++ b/shared/editor/components/Widget.tsx @@ -4,24 +4,38 @@ import { s } from "../../styles"; import { sanitizeUrl } from "../../utils/urls"; type Props = { + /** Icon to display on the left side of the widget */ icon: React.ReactNode; + /** Title of the widget */ title: React.ReactNode; + /** Context, displayed to right of title */ context?: React.ReactNode; + /** URL to open when the widget is clicked */ href: string; + /** Whether the widget is currently selected */ isSelected: boolean; + /** Children to display to the right of the context */ children?: React.ReactNode; + /** Callback fired when the widget is double clicked */ + onDoubleClick?: React.MouseEventHandler; + /** Callback fired when the widget is clicked */ onMouseDown?: React.MouseEventHandler; + /** Callback fired when the widget is clicked */ onClick?: React.MouseEventHandler; }; export default function Widget(props: Props & ThemeProps) { + const className = props.isSelected + ? "ProseMirror-selectednode widget" + : "widget"; + return ( diff --git a/shared/editor/nodes/Attachment.tsx b/shared/editor/nodes/Attachment.tsx index 1bb747d3e..64a5725bf 100644 --- a/shared/editor/nodes/Attachment.tsx +++ b/shared/editor/nodes/Attachment.tsx @@ -86,6 +86,9 @@ export default class Attachment extends Node { href={node.attrs.href} title={node.attrs.title} onMouseDown={this.handleSelect(props)} + onDoubleClick={() => { + this.editor.commands.downloadAttachment(); + }} onClick={(event) => { if (isEditable) { event.preventDefault(); @@ -159,6 +162,7 @@ export default class Attachment extends Node { // create a temporary link node and click it const link = document.createElement("a"); link.href = node.attrs.href; + link.target = "_blank"; document.body.appendChild(link); link.click();