From c5faf6239834a347b2b6abea2b0f5f515595dfcf Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 3 Dec 2022 08:57:04 +0500 Subject: [PATCH] web: lazily load web clips just like images --- apps/web/src/common/attachments.js | 24 ------- apps/web/src/components/editor/index.tsx | 15 ++-- apps/web/src/components/editor/tiptap.tsx | 7 +- apps/web/src/components/editor/types.ts | 1 + apps/web/src/utils/html.ts | 5 +- packages/core/collections/attachments.js | 60 +++++++++++++--- packages/core/collections/content.js | 6 +- .../src/extensions/web-clip/component.tsx | 67 +++++++++--------- .../src/extensions/web-clip/web-clip.ts | 69 ++++++++++++++----- packages/editor/src/index.ts | 8 +-- .../editor/src/toolbar/tools/web-clip.tsx | 2 +- .../editor/src/toolbar/utils/prosemirror.ts | 2 +- 12 files changed, 157 insertions(+), 109 deletions(-) diff --git a/apps/web/src/common/attachments.js b/apps/web/src/common/attachments.js index 33e4c821f..dd5833b50 100644 --- a/apps/web/src/common/attachments.js +++ b/apps/web/src/common/attachments.js @@ -64,27 +64,3 @@ export function getTotalSize(attachments) { } return size; } - -export async function readAttachment(hash) { - const attachment = db.attachments.attachment(hash); - if (!attachment) return; - const downloadResult = await db.fs.downloadFile( - attachment.metadata.hash, - attachment.metadata.hash, - attachment.chunkSize, - attachment.metadata - ); - if (!downloadResult) throw new Error("Failed to download file."); - - const key = await db.attachments.decryptKey(attachment.key); - if (!key) throw new Error("Invalid key for attachment."); - - return await FS.readEncrypted(attachment.metadata.hash, key, { - chunkSize: attachment.chunkSize, - iv: attachment.iv, - salt: attachment.salt, - length: attachment.length, - alg: attachment.alg, - outputType: "text" - }); -} diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index 8cc3ce782..a41c3eea8 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -37,7 +37,7 @@ import Header from "./header"; import { Attachment } from "../icons"; import { useEditorInstance } from "./context"; import { attachFile, AttachmentProgress, insertAttachment } from "./picker"; -import { downloadAttachment, readAttachment } from "../../common/attachments"; +import { downloadAttachment } from "../../common/attachments"; import { EV, EVENTS } from "@notesnook/core/common"; import { db } from "../../common/db"; import useMobile from "../../hooks/use-mobile"; @@ -139,7 +139,7 @@ export default function EditorManager({ true ); } else if (noteId && editorstore.get().session.content) { - await db.attachments?.downloadImages(noteId); + await db.attachments?.downloadMedia(noteId); } }, [noteId]); @@ -246,14 +246,20 @@ export function Editor(props: EditorProps) { ({ groupId, hash, + attachmentType, src }: { groupId?: string; + attachmentType: "image" | "webclip" | "generic"; hash: string; src: string; }) => { if (groupId?.startsWith("monograph")) return; - editor.current?.loadImage(hash, src); + if (attachmentType === "image") { + editor.current?.loadImage(hash, src); + } else if (attachmentType === "webclip") { + editor.current?.loadWebClip(hash, src); + } } ); @@ -278,9 +284,6 @@ export function Editor(props: EditorProps) { onDownloadAttachment={(attachment) => downloadAttachment(attachment.hash) } - onReadAttachment={(hash) => - readAttachment(hash) as Promise - } onInsertAttachment={(type) => { const mime = type === "file" ? "*/*" : "image/*"; insertAttachment(mime).then((file) => { diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index f6573c498..cf1578d3d 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -58,7 +58,6 @@ type TipTapProps = { onChange?: (id: string, sessionId: string, content: string) => void; onInsertAttachment?: (type: AttachmentType) => void; onDownloadAttachment?: (attachment: Attachment) => void; - onReadAttachment?: (hash: string) => Promise; onAttachFile?: (file: File) => void; onFocus?: () => void; content?: string; @@ -101,7 +100,6 @@ function TipTap(props: TipTapProps) { onChange, onInsertAttachment, onDownloadAttachment, - onReadAttachment, onAttachFile, onFocus = () => {}, content, @@ -222,9 +220,6 @@ function TipTap(props: TipTapProps) { onDownloadAttachment?.(attachment); return true; }, - async onLoadWebClip(_editor, attachmentHash) { - return await onReadAttachment?.(attachmentHash); - }, onOpenLink: (url) => { window.open(url, "_blank"); return true; @@ -361,6 +356,8 @@ function toIEditor(editor: Editor): IEditor { editor.current?.commands.insertImage({ ...file, src: file.dataurl }); } else editor.current?.commands.insertAttachment(file); }, + loadWebClip: (hash, src) => + editor.current?.commands.updateWebClip({ hash }, { src }), loadImage: (hash, src) => editor.current?.commands.updateImage( { hash }, diff --git a/apps/web/src/components/editor/types.ts b/apps/web/src/components/editor/types.ts index 1bfd3d27f..da16e6477 100644 --- a/apps/web/src/components/editor/types.ts +++ b/apps/web/src/components/editor/types.ts @@ -32,6 +32,7 @@ export interface IEditor { redo: () => void; updateContent: (content: string) => void; attachFile: (file: Attachment) => void; + loadWebClip: (hash: string, html: string) => void; loadImage: (hash: string, src: string) => void; sendAttachmentProgress: ( hash: string, diff --git a/apps/web/src/utils/html.ts b/apps/web/src/utils/html.ts index f14cf8c29..fc6c0910d 100644 --- a/apps/web/src/utils/html.ts +++ b/apps/web/src/utils/html.ts @@ -20,7 +20,7 @@ along with this program. If not, see . export function h( tag: keyof HTMLElementTagNameMap | "text", children: (HTMLElement | string)[] = [], - attr: Record = {} + attr: Record = {} ) { const element = document.createElement(tag); element.append( @@ -29,7 +29,8 @@ export function h( ) ); for (const key in attr) { - element.setAttribute(key, attr[key]); + const value = attr[key]; + if (value) element.setAttribute(key, value); } return element; } diff --git a/packages/core/collections/attachments.js b/packages/core/collections/attachments.js index 7b01ee078..9d9156ed1 100644 --- a/packages/core/collections/attachments.js +++ b/packages/core/collections/attachments.js @@ -206,7 +206,7 @@ export default class Attachments extends Collection { /** * Get specified type of attachments of a note * @param {string} noteId - * @param {"files"|"images"|"all"} type + * @param {"files"|"images"|"webclips"|"all"} type * @returns {Array} */ ofNote(noteId, type) { @@ -214,6 +214,7 @@ export default class Attachments extends Collection { if (type === "files") attachments = this.files; else if (type === "images") attachments = this.images; + else if (type === "webclips") attachments = this.webclips; else if (type === "all") attachments = this.all; return attachments.filter((attachment) => @@ -228,9 +229,10 @@ export default class Attachments extends Collection { /** * @param {string} hash + * @param {"base64" | "text"} outputType * @returns {Promise} dataurl formatted string */ - async read(hash) { + async read(hash, outputType) { const attachment = this.all.find((a) => a.metadata.hash === hash); if (!attachment) return; @@ -244,10 +246,12 @@ export default class Attachments extends Collection { salt: attachment.salt, length: attachment.length, alg: attachment.alg, - outputType: "base64" + outputType } ); - return dataurl.fromObject({ type: attachment.metadata.type, data }); + return outputType === "base64" + ? dataurl.fromObject({ type: attachment.metadata.type, data }) + : data; } attachment(hashOrId) { @@ -284,14 +288,14 @@ export default class Attachments extends Collection { return { key, metadata }; } - async downloadImages(noteId) { - const attachments = this.images.filter((attachment) => + async downloadMedia(noteId) { + const attachments = this.media.filter((attachment) => hasItem(attachment.noteIds, noteId) ); try { for (let i = 0; i < attachments.length; i++) { const attachment = attachments[i]; - await this._downloadMedia(attachment, { + await this._download(attachment, { total: attachments.length, current: i, groupId: noteId @@ -302,7 +306,7 @@ export default class Attachments extends Collection { } } - async _downloadMedia(attachment, { total, current, groupId }, notify = true) { + async _download(attachment, { total, current, groupId }, notify = true) { const { metadata, chunkSize } = attachment; const filename = metadata.hash; @@ -315,13 +319,14 @@ export default class Attachments extends Collection { ); if (!isDownloaded) return; - const src = await this.read(metadata.hash); + const src = await this.read(metadata.hash, getOutputType(attachment)); if (!src) return; if (notify) EV.publish(EVENTS.mediaAttachmentDownloaded, { groupId, hash: metadata.hash, + attachmentType: getAttachmentType(attachment), src }); @@ -370,12 +375,32 @@ export default class Attachments extends Collection { ); } - get files() { + get webclips() { return this.all.filter( - (attachment) => !attachment.metadata.type.startsWith("image/") + (attachment) => + attachment.metadata.type === "application/vnd.notesnook.web-clip" ); } + get media() { + return this.all.filter( + (attachment) => + attachment.metadata.type.startsWith("image/") || + attachment.metadata.type === "application/vnd.notesnook.web-clip" + ); + } + + get files() { + return this.all.filter( + (attachment) => + !attachment.metadata.type.startsWith("image/") && + attachment.metadata.type !== "application/vnd.notesnook.web-clip" + ); + } + + /** + * @returns {any[]} + */ get all() { return this._collection.getItems(); } @@ -404,3 +429,16 @@ export default class Attachments extends Collection { return this.key; } } + +function getOutputType(attachment) { + if (attachment.metadata.type === "application/vnd.notesnook.web-clip") + return "text"; + else if (attachment.metadata.type.startsWith("image/")) return "base64"; +} + +function getAttachmentType(attachment) { + if (attachment.metadata.type === "application/vnd.notesnook.web-clip") + return "webclip"; + else if (attachment.metadata.type.startsWith("image/")) return "image"; + else return "generic"; +} diff --git a/packages/core/collections/content.js b/packages/core/collections/content.js index fc4b28f70..7511fd088 100644 --- a/packages/core/collections/content.js +++ b/packages/core/collections/content.js @@ -125,11 +125,7 @@ export default class Content extends Collection { groupId }; - return this._db.attachments._downloadMedia( - attachment, - progressData, - notify - ); + return this._db.attachments._download(attachment, progressData, notify); }); return contentItem; } diff --git a/packages/editor/src/extensions/web-clip/component.tsx b/packages/editor/src/extensions/web-clip/component.tsx index 657169e27..690a37ce4 100644 --- a/packages/editor/src/extensions/web-clip/component.tsx +++ b/packages/editor/src/extensions/web-clip/component.tsx @@ -21,7 +21,7 @@ import { Box, Flex, Text } from "@theme-ui/components"; import { useEffect, useRef, useState } from "react"; import { SelectionBasedReactNodeViewProps } from "../react"; import { Icon, Icons } from "../../toolbar"; -import { WebClipAttributes, WebClipOptions } from "./web-clip"; +import { WebClipAttributes } from "./web-clip"; import { DesktopOnly } from "../../components/responsive"; import { ToolbarGroup } from "../../toolbar/components/toolbar-group"; @@ -40,23 +40,18 @@ export function WebClipComponent( const [isLoading, setIsLoading] = useState(true); const embedRef = useRef(null); const resizeObserverRef = useRef(); - const { src, title, hash, fullscreen } = node.attrs; - const { onLoadWebClip } = editor.storage.webclip as WebClipOptions; - + const { src, title, fullscreen, html } = node.attrs; + console.log(node.attrs); useEffect(() => { - (async function () { - const iframe = embedRef.current; - if (!iframe || !iframe.contentDocument) return; - iframe.contentDocument.open(); - iframe.contentDocument.write( - (await onLoadWebClip(editor, hash)) || FAILED_CONTENT - ); - iframe.contentDocument.close(); - iframe.contentDocument.head.innerHTML += ``; + const iframe = embedRef.current; + if (!iframe || !iframe.contentDocument || !isLoading || !html) return; + iframe.contentDocument.open(); + iframe.contentDocument.write(html || FAILED_CONTENT); + iframe.contentDocument.close(); + iframe.contentDocument.head.innerHTML += ``; - setIsLoading(false); - })(); - }, [hash, onLoadWebClip]); + setIsLoading(false); + }, [html]); useEffect(() => { function fullscreenchanged() { @@ -65,7 +60,7 @@ export function WebClipComponent( resetIframeSize(embedRef.current); } else { updateAttributes({ fullscreen: false }); - resizeIframe(embedRef.current); + resizeIframe(node.attrs, embedRef.current); if (embedRef.current?.contentDocument) { resizeObserverRef.current?.observe( @@ -81,6 +76,17 @@ export function WebClipComponent( }; }, [updateAttributes]); + useEffect(() => { + if (embedRef.current?.contentDocument) { + resizeObserverRef.current = new ResizeObserver(() => { + resizeIframe(node.attrs, embedRef.current); + }); + resizeObserverRef.current.observe( + embedRef.current?.contentDocument?.body + ); + } + }, []); + return ( <>