editor: add support for attachment previews (#2123)

This adds support for basic attachment preview support. Currently only image previews are supported on mobile.

    Fixed image downloading not working on mobile
    Added image preview support on mobile with support for loading full quality images in full screen.
    Added Attachment preview logic in editor, so in future we can support preview for other files such as audio & video
This commit is contained in:
Ammar Ahmed
2023-03-17 14:43:53 +05:00
committed by GitHub
parent 93baed35f4
commit 1ee24cde07
14 changed files with 252 additions and 134 deletions

View File

@@ -29,20 +29,30 @@ import { db } from "../database";
import Storage from "../database/storage"; import Storage from "../database/storage";
import { cacheDir } from "./utils"; import { cacheDir } from "./utils";
export default async function downloadAttachment(hash, global = true) { export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false
}
) {
let attachment = db.attachments.attachment(hash); let attachment = db.attachments.attachment(hash);
console.log(attachment);
if (!attachment) { if (!attachment) {
console.log("attachment not found"); console.log("attachment not found");
return; return;
} }
let folder = {}; let folder = {};
if (!options.cache) {
if (Platform.OS === "android") { if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree(); folder = await ScopedStorage.openDocumentTree();
if (!folder) return; if (!folder) return;
} else { } else {
folder.uri = await Storage.checkAndCreateDir("/downloads/"); folder.uri = await Storage.checkAndCreateDir("/downloads/");
} }
}
try { try {
await db.fs.downloadFile( await db.fs.downloadFile(
@@ -63,19 +73,29 @@ export default async function downloadAttachment(hash, global = true) {
hash: attachment.metadata.hash, hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType, hashType: attachment.metadata.hashType,
mime: attachment.metadata.type, mime: attachment.metadata.type,
fileName: attachment.metadata.filename, fileName: options.cache ? undefined : attachment.metadata.filename,
uri: folder.uri, uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize chunkSize: attachment.chunkSize
}; };
let fileUri = await Sodium.decryptFile(key, info, "file"); let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
if (!options.silent) {
ToastEvent.show({ ToastEvent.show({
heading: "Download successful", heading: "Download successful",
message: attachment.metadata.filename + " downloaded", message: attachment.metadata.filename + " downloaded",
type: "success" type: "success"
}); });
}
if (attachment.dateUploaded) { if (
attachment.dateUploaded &&
!attachment.metadata?.type?.startsWith("image")
) {
RNFetchBlob.fs RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`) .unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log); .catch(console.log);
@@ -85,7 +105,7 @@ export default async function downloadAttachment(hash, global = true) {
fileUri = folder.uri + `/${attachment.metadata.filename}`; fileUri = folder.uri + `/${attachment.metadata.filename}`;
} }
console.log("saved file uri: ", fileUri); console.log("saved file uri: ", fileUri);
if (!options.silent) {
presentSheet({ presentSheet({
title: "File downloaded", title: "File downloaded",
paragraph: `${attachment.metadata.filename} saved to ${ paragraph: `${attachment.metadata.filename} saved to ${
@@ -103,6 +123,8 @@ export default async function downloadAttachment(hash, global = true) {
/> />
) )
}); });
}
return fileUri; return fileUri;
} catch (e) { } catch (e) {
console.log("download attachment error: ", e); console.log("download attachment error: ", e);

View File

@@ -20,16 +20,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer"; import ImageViewer from "react-native-image-zoom-viewer";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import { import {
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import BaseDialog from "../dialog/base-dialog"; import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button"; import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
const ImagePreview = () => { const ImagePreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [image, setImage] = useState(""); const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
eSubscribeEvent("ImagePreview", open); eSubscribeEvent("ImagePreview", open);
@@ -39,9 +45,20 @@ const ImagePreview = () => {
}; };
}, []); }, []);
const open = (image) => { const open = async (image) => {
setImage(image);
setVisible(true); setVisible(true);
setLoading(true);
setTimeout(async () => {
const hash = image.hash;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
console.log(path);
setImage("file://" + path);
setLoading(false);
}, 100);
}; };
const close = () => { const close = () => {
@@ -59,6 +76,21 @@ const ImagePreview = () => {
backgroundColor: "black" backgroundColor: "black"
}} }}
> >
{loading ? (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate
color={colors.accent}
borderColor="transparent"
/>
</View>
) : (
<ImageViewer <ImageViewer
enableImageZoom={true} enableImageZoom={true}
renderIndicator={() => <></>} renderIndicator={() => <></>}
@@ -97,6 +129,7 @@ const ImagePreview = () => {
} }
]} ]}
/> />
)}
</View> </View>
</BaseDialog> </BaseDialog>
) )

View File

@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
* The url should be something like this: http://192.168.100.126:3000/index.html * The url should be something like this: http://192.168.100.126:3000/index.html
*/ */
export const EDITOR_URI = __DEV__ export const EDITOR_URI = __DEV__
? EditorMobileSourceUrl ? "http://192.168.8.103:3000/index.html"
: EditorMobileSourceUrl; : EditorMobileSourceUrl;

View File

@@ -34,5 +34,6 @@ export const EventTypes = {
fullscreen: "editor-event:fullscreen", fullscreen: "editor-event:fullscreen",
link: "editor-event:link", link: "editor-event:link",
contentchange: "editor-event:content-change", contentchange: "editor-event:content-change",
reminders: "editor-event:reminders" reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment"
}; };

View File

@@ -29,6 +29,7 @@ import {
} from "react-native"; } from "react-native";
import { WebViewMessageEvent } from "react-native-webview"; import { WebViewMessageEvent } from "react-native-webview";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import ImagePreview from "../../../components/image-preview";
import { RelationsList } from "../../../components/sheets/relations-list"; import { RelationsList } from "../../../components/sheets/relations-list";
import ReminderSheet from "../../../components/sheets/reminder"; import ReminderSheet from "../../../components/sheets/reminder";
import useKeyboard from "../../../hooks/use-keyboard"; import useKeyboard from "../../../hooks/use-keyboard";
@@ -380,6 +381,10 @@ export const useEditorEvents = (
case EventTypes.link: case EventTypes.link:
openLinkInBrowser(editorMessage.value as string); openLinkInBrowser(editorMessage.value as string);
break; break;
case EventTypes.previewAttachment:
eSendEvent("ImagePreview", editorMessage.value);
break;
default: default:
break; break;
} }

View File

@@ -92,6 +92,10 @@ const Tiptap = ({
global.editorController.downloadAttachment(attachment); global.editorController.downloadAttachment(attachment);
return true; return true;
}, },
onPreviewAttachment(editor, attachment) {
global.editorController.previewAttachment(attachment);
return true;
},
theme: editorTheme, theme: editorTheme,
element: !layout ? undefined : contentRef.current || undefined, element: !layout ? undefined : contentRef.current || undefined,
editable: !settings.readonly, editable: !settings.readonly,

View File

@@ -58,6 +58,7 @@ export type EditorController = {
setTitle: React.Dispatch<React.SetStateAction<string>>; setTitle: React.Dispatch<React.SetStateAction<string>>;
openFilePicker: (type: "image" | "file" | "camera") => void; openFilePicker: (type: "image" | "file" | "camera") => void;
downloadAttachment: (attachment: Attachment) => void; downloadAttachment: (attachment: Attachment) => void;
previewAttachment: (attachment: Attachment) => void;
content: MutableRefObject<string | null>; content: MutableRefObject<string | null>;
onUpdate: () => void; onUpdate: () => void;
titlePlaceholder: string; titlePlaceholder: string;
@@ -170,7 +171,9 @@ export function useEditorController(update: () => void): EditorController {
const downloadAttachment = useCallback((attachment: Attachment) => { const downloadAttachment = useCallback((attachment: Attachment) => {
post(EventTypes.download, attachment); post(EventTypes.download, attachment);
}, []); }, []);
const previewAttachment = useCallback((attachment: Attachment) => {
post(EventTypes.previewAttachment, attachment);
}, []);
const openLink = useCallback((url: string) => { const openLink = useCallback((url: string) => {
post(EventTypes.link, url); post(EventTypes.link, url);
return true; return true;
@@ -187,6 +190,7 @@ export function useEditorController(update: () => void): EditorController {
setTitlePlaceholder, setTitlePlaceholder,
openFilePicker, openFilePicker,
downloadAttachment, downloadAttachment,
previewAttachment,
content: htmlContentRef, content: htmlContentRef,
openLink, openLink,
onUpdate: onUpdate onUpdate: onUpdate

View File

@@ -145,7 +145,8 @@ export const EventTypes = {
fullscreen: "editor-event:fullscreen", fullscreen: "editor-event:fullscreen",
link: "editor-event:link", link: "editor-event:link",
contentchange: "editor-event:content-change", contentchange: "editor-event:content-change",
reminders: "editor-event:reminders" reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment"
} as const; } as const;
export function isReactNative(): boolean { export function isReactNative(): boolean {

View File

@@ -27,6 +27,7 @@ export interface AttachmentOptions {
HTMLAttributes: Record<string, unknown>; HTMLAttributes: Record<string, unknown>;
onDownloadAttachment: (editor: Editor, attachment: Attachment) => boolean; onDownloadAttachment: (editor: Editor, attachment: Attachment) => boolean;
onOpenAttachmentPicker: (editor: Editor, type: AttachmentType) => boolean; onOpenAttachmentPicker: (editor: Editor, type: AttachmentType) => boolean;
onPreviewAttachment: (editor: Editor, attachment: Attachment) => boolean;
} }
export type AttachmentWithProgress = AttachmentProgress & Attachment; export type AttachmentWithProgress = AttachmentProgress & Attachment;
@@ -52,6 +53,7 @@ declare module "@tiptap/core" {
removeAttachment: () => ReturnType; removeAttachment: () => ReturnType;
downloadAttachment: (attachment: Attachment) => ReturnType; downloadAttachment: (attachment: Attachment) => ReturnType;
setAttachmentProgress: (progress: AttachmentProgress) => ReturnType; setAttachmentProgress: (progress: AttachmentProgress) => ReturnType;
previewAttachment: (options: Attachment) => ReturnType;
}; };
} }
} }
@@ -67,7 +69,8 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
return { return {
HTMLAttributes: {}, HTMLAttributes: {},
onDownloadAttachment: () => false, onDownloadAttachment: () => false,
onOpenAttachmentPicker: () => false onOpenAttachmentPicker: () => false,
onPreviewAttachment: () => false
}; };
}, },
@@ -158,6 +161,12 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
tr.setMeta("addToHistory", false); tr.setMeta("addToHistory", false);
if (dispatch) dispatch(tr); if (dispatch) dispatch(tr);
return true; return true;
},
previewAttachment:
(attachment) =>
({ editor }) => {
return this.options.onPreviewAttachment(editor, attachment);
} }
}; };
} }

View File

@@ -17,64 +17,64 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import "./extensions";
import CharacterCount from "@tiptap/extension-character-count";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useMemo } from "react";
import Toolbar from "./toolbar";
import TextAlign from "@tiptap/extension-text-align";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import FontSize from "./extensions/font-size";
import TextDirection from "./extensions/text-direction";
import TextStyle from "@tiptap/extension-text-style";
import FontFamily from "./extensions/font-family";
import BulletList from "./extensions/bullet-list";
import OrderedList from "./extensions/ordered-list";
import Color from "@tiptap/extension-color";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "./extensions/table-cell";
import TableHeader from "@tiptap/extension-table-header";
import { ImageNode } from "./extensions/image";
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
import { TaskListNode } from "./extensions/task-list";
import { TaskItemNode } from "./extensions/task-item";
import { SearchReplace } from "./extensions/search-replace";
import { EmbedNode } from "./extensions/embed";
import { CodeBlock } from "./extensions/code-block";
import { ListItem } from "./extensions/list-item";
import { Link } from "@tiptap/extension-link";
import { Codemark } from "./extensions/code-mark";
import { MathInline, MathBlock } from "./extensions/math";
import {
NodeViewSelectionNotifier,
usePortalProvider
} from "./extensions/react";
import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item";
import { KeepInView } from "./extensions/keep-in-view";
import { SelectionPersist } from "./extensions/selection-persist";
import { Table } from "./extensions/table";
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
import { useEditor } from "./hooks/use-editor";
import { import {
EditorOptions, EditorOptions,
extensions as TiptapCoreExtensions, extensions as TiptapCoreExtensions,
getHTMLFromFragment getHTMLFromFragment
} from "@tiptap/core"; } from "@tiptap/core";
import { usePermissionHandler } from "./hooks/use-permission-handler"; import CharacterCount from "@tiptap/extension-character-count";
import { Highlight } from "./extensions/highlight";
import { Paragraph } from "./extensions/paragraph";
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
import { Code } from "@tiptap/extension-code"; import { Code } from "@tiptap/extension-code";
import { DateTime } from "./extensions/date-time"; import Color from "@tiptap/extension-color";
import { OpenLink, OpenLinkOptions } from "./extensions/open-link";
import HorizontalRule from "@tiptap/extension-horizontal-rule"; import HorizontalRule from "@tiptap/extension-horizontal-rule";
import { Link } from "@tiptap/extension-link";
import Placeholder from "@tiptap/extension-placeholder";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row";
import TextAlign from "@tiptap/extension-text-align";
import TextStyle from "@tiptap/extension-text-style";
import Underline from "@tiptap/extension-underline";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useMemo } from "react";
import "./extensions";
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
import BulletList from "./extensions/bullet-list";
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
import { CodeBlock } from "./extensions/code-block";
import { Codemark } from "./extensions/code-mark";
import { DateTime } from "./extensions/date-time";
import { EmbedNode } from "./extensions/embed";
import FontFamily from "./extensions/font-family";
import FontSize from "./extensions/font-size";
import { Highlight } from "./extensions/highlight";
import { ImageNode, ImageOptions } from "./extensions/image";
import { KeepInView } from "./extensions/keep-in-view";
import { KeyMap } from "./extensions/key-map"; import { KeyMap } from "./extensions/key-map";
import { ListItem } from "./extensions/list-item";
import { MathBlock, MathInline } from "./extensions/math";
import { OpenLink, OpenLinkOptions } from "./extensions/open-link";
import OrderedList from "./extensions/ordered-list";
import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item";
import { Paragraph } from "./extensions/paragraph";
import {
NodeViewSelectionNotifier,
usePortalProvider
} from "./extensions/react";
import { SearchReplace } from "./extensions/search-replace";
import { SelectionPersist } from "./extensions/selection-persist";
import { Table } from "./extensions/table";
import TableCell from "./extensions/table-cell";
import { TaskItemNode } from "./extensions/task-item";
import { TaskListNode } from "./extensions/task-list";
import TextDirection from "./extensions/text-direction";
import { WebClipNode, WebClipOptions } from "./extensions/web-clip"; import { WebClipNode, WebClipOptions } from "./extensions/web-clip";
import { useEditor } from "./hooks/use-editor";
import { usePermissionHandler } from "./hooks/use-permission-handler";
import Toolbar from "./toolbar";
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
import { DownloadOptions } from "./utils/downloader"; import { DownloadOptions } from "./utils/downloader";
const CoreExtensions = Object.entries(TiptapCoreExtensions) const CoreExtensions = Object.entries(TiptapCoreExtensions)
@@ -85,6 +85,7 @@ const CoreExtensions = Object.entries(TiptapCoreExtensions)
type TiptapOptions = EditorOptions & type TiptapOptions = EditorOptions &
Omit<AttachmentOptions, "HTMLAttributes"> & Omit<AttachmentOptions, "HTMLAttributes"> &
Omit<WebClipOptions, "HTMLAttributes"> & Omit<WebClipOptions, "HTMLAttributes"> &
Omit<ImageOptions, "HTMLAttributes"> &
OpenLinkOptions & { OpenLinkOptions & {
downloadOptions?: DownloadOptions; downloadOptions?: DownloadOptions;
theme: Theme; theme: Theme;
@@ -104,6 +105,7 @@ const useTiptap = (
isKeyboardOpen, isKeyboardOpen,
onDownloadAttachment, onDownloadAttachment,
onOpenAttachmentPicker, onOpenAttachmentPicker,
onPreviewAttachment,
onOpenLink, onOpenLink,
onBeforeCreate, onBeforeCreate,
downloadOptions, downloadOptions,
@@ -219,7 +221,8 @@ const useTiptap = (
EmbedNode, EmbedNode,
AttachmentNode.configure({ AttachmentNode.configure({
onDownloadAttachment, onDownloadAttachment,
onOpenAttachmentPicker onOpenAttachmentPicker,
onPreviewAttachment
}), }),
OutlineListItem, OutlineListItem,
OutlineList, OutlineList,
@@ -241,6 +244,7 @@ const useTiptap = (
injectCSS: false injectCSS: false
}), }),
[ [
onPreviewAttachment,
onDownloadAttachment, onDownloadAttachment,
onOpenAttachmentPicker, onOpenAttachmentPicker,
PortalProviderAPI, PortalProviderAPI,
@@ -260,6 +264,12 @@ const useTiptap = (
return editor; return editor;
}; };
export { type Fragment } from "prosemirror-model";
export { type Attachment, type AttachmentType } from "./extensions/attachment";
export * from "./extensions/react";
export * from "./toolbar";
export * from "./types";
export * from "./utils/word-counter";
export { export {
useTiptap, useTiptap,
Toolbar, Toolbar,
@@ -267,9 +277,3 @@ export {
getHTMLFromFragment, getHTMLFromFragment,
type DownloadOptions type DownloadOptions
}; };
export * from "./types";
export * from "./extensions/react";
export * from "./toolbar";
export { type AttachmentType, type Attachment } from "./extensions/attachment";
export { type Fragment } from "prosemirror-model";
export * from "./utils/word-counter";

View File

@@ -113,7 +113,8 @@ import {
mdiWeb, mdiWeb,
mdiPageNextOutline, mdiPageNextOutline,
mdiSortBoolAscendingVariant, mdiSortBoolAscendingVariant,
mdiApplicationCogOutline mdiApplicationCogOutline,
mdiArrowExpand
} from "@mdi/js"; } from "@mdi/js";
export const Icons = { export const Icons = {
@@ -156,6 +157,7 @@ export const Icons = {
fullscreen: mdiFullscreen, fullscreen: mdiFullscreen,
url: mdiLink, url: mdiLink,
image: mdiImageOutline, image: mdiImageOutline,
previewAttachment: mdiArrowExpand,
imageDownload: mdiProgressDownload, imageDownload: mdiProgressDownload,
imageFailed: mdiProgressAlert, imageFailed: mdiProgressAlert,
imageSettings: mdiImageEditOutline, imageSettings: mdiImageEditOutline,

View File

@@ -237,7 +237,6 @@ const tools: Record<ToolId, ToolDefinition> = {
title: "Cell border width", title: "Cell border width",
conditional: true conditional: true
}, },
imageSettings: { imageSettings: {
icon: "imageSettings", icon: "imageSettings",
title: "Image settings", title: "Image settings",
@@ -263,6 +262,11 @@ const tools: Record<ToolId, ToolDefinition> = {
title: "Image properties", title: "Image properties",
conditional: true conditional: true
}, },
previewAttachment: {
icon: "previewAttachment",
title: "Preview attachment",
conditional: true
},
attachmentSettings: { attachmentSettings: {
icon: "attachmentSettings", icon: "attachmentSettings",
title: "Attachment settings", title: "Attachment settings",
@@ -354,6 +358,7 @@ export const STATIC_TOOLBAR_GROUPS: ToolbarDefinition = [
"cellProperties", "cellProperties",
"imageSettings", "imageSettings",
"embedSettings", "embedSettings",
"previewAttachment",
"attachmentSettings", "attachmentSettings",
"linkSettings", "linkSettings",
"codeRemove", "codeRemove",

View File

@@ -27,7 +27,11 @@ import { Attachment } from "../../extensions/attachment";
export function AttachmentSettings(props: ToolProps) { export function AttachmentSettings(props: ToolProps) {
const { editor } = props; const { editor } = props;
const isBottom = useToolbarLocation() === "bottom"; const isBottom = useToolbarLocation() === "bottom";
if (!editor.isActive("attachment") || !isBottom) return null; if (
(!editor.isActive("attachment") && !editor.isActive("image")) ||
!isBottom
)
return null;
return ( return (
<MoreTools <MoreTools
@@ -58,6 +62,28 @@ export function DownloadAttachment(props: ToolProps) {
); );
} }
export function PreviewAttachment(props: ToolProps) {
const { editor } = props;
const isBottom = useToolbarLocation() === "bottom";
if (!editor.isActive("image") || !isBottom) return null;
return (
<ToolButton
{...props}
toggled={false}
onClick={() => {
const attachmentNode =
findSelectedNode(editor, "attachment") ||
findSelectedNode(editor, "image");
const attachment = (attachmentNode?.attrs || {}) as Attachment;
editor.current?.chain().focus().previewAttachment(attachment).run();
}}
/>
);
}
export function RemoveAttachment(props: ToolProps) { export function RemoveAttachment(props: ToolProps) {
const { editor } = props; const { editor } = props;
return ( return (

View File

@@ -71,6 +71,7 @@ import {
import { import {
AttachmentSettings, AttachmentSettings,
DownloadAttachment, DownloadAttachment,
PreviewAttachment,
RemoveAttachment RemoveAttachment
} from "./attachment"; } from "./attachment";
import { import {
@@ -133,6 +134,7 @@ const tools = {
webclipOpenSource: WebClipOpenSource, webclipOpenSource: WebClipOpenSource,
webclipSettings: WebClipSettings, webclipSettings: WebClipSettings,
previewAttachment: PreviewAttachment,
attachmentSettings: AttachmentSettings, attachmentSettings: AttachmentSettings,
downloadAttachment: DownloadAttachment, downloadAttachment: DownloadAttachment,
removeAttachment: RemoveAttachment, removeAttachment: RemoveAttachment,