diff --git a/apps/mobile/app/common/filesystem/download-attachment.js b/apps/mobile/app/common/filesystem/download-attachment.js index d35848a26..bb3dc5952 100644 --- a/apps/mobile/app/common/filesystem/download-attachment.js +++ b/apps/mobile/app/common/filesystem/download-attachment.js @@ -29,19 +29,29 @@ import { db } from "../database"; import Storage from "../database/storage"; 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); + console.log(attachment); if (!attachment) { console.log("attachment not found"); return; } let folder = {}; - if (Platform.OS === "android") { - folder = await ScopedStorage.openDocumentTree(); - if (!folder) return; - } else { - folder.uri = await Storage.checkAndCreateDir("/downloads/"); + if (!options.cache) { + if (Platform.OS === "android") { + folder = await ScopedStorage.openDocumentTree(); + if (!folder) return; + } else { + folder.uri = await Storage.checkAndCreateDir("/downloads/"); + } } try { @@ -63,19 +73,29 @@ export default async function downloadAttachment(hash, global = true) { hash: attachment.metadata.hash, hashType: attachment.metadata.hashType, mime: attachment.metadata.type, - fileName: attachment.metadata.filename, - uri: folder.uri, + fileName: options.cache ? undefined : attachment.metadata.filename, + uri: options.cache ? undefined : folder.uri, chunkSize: attachment.chunkSize }; - let fileUri = await Sodium.decryptFile(key, info, "file"); - ToastEvent.show({ - heading: "Download successful", - message: attachment.metadata.filename + " downloaded", - type: "success" - }); + let fileUri = await Sodium.decryptFile( + key, + info, + options.cache ? "cache" : "file" + ); - if (attachment.dateUploaded) { + if (!options.silent) { + ToastEvent.show({ + heading: "Download successful", + message: attachment.metadata.filename + " downloaded", + type: "success" + }); + } + + if ( + attachment.dateUploaded && + !attachment.metadata?.type?.startsWith("image") + ) { RNFetchBlob.fs .unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`) .catch(console.log); @@ -85,24 +105,26 @@ export default async function downloadAttachment(hash, global = true) { fileUri = folder.uri + `/${attachment.metadata.filename}`; } console.log("saved file uri: ", fileUri); + if (!options.silent) { + presentSheet({ + title: "File downloaded", + paragraph: `${attachment.metadata.filename} saved to ${ + Platform.OS === "android" + ? "selected path" + : "File Manager/Notesnook/downloads" + }`, + icon: "download", + context: global ? null : attachment.metadata.hash, + component: ( + + ) + }); + } - presentSheet({ - title: "File downloaded", - paragraph: `${attachment.metadata.filename} saved to ${ - Platform.OS === "android" - ? "selected path" - : "File Manager/Notesnook/downloads" - }`, - icon: "download", - context: global ? null : attachment.metadata.hash, - component: ( - - ) - }); return fileUri; } catch (e) { console.log("download attachment error: ", e); diff --git a/apps/mobile/app/components/image-preview/index.js b/apps/mobile/app/components/image-preview/index.js index 72d632ac4..c350b36e5 100644 --- a/apps/mobile/app/components/image-preview/index.js +++ b/apps/mobile/app/components/image-preview/index.js @@ -20,16 +20,22 @@ along with this program. If not, see . import React, { useEffect, useState } from "react"; import { View } from "react-native"; import ImageViewer from "react-native-image-zoom-viewer"; +import downloadAttachment from "../../common/filesystem/download-attachment"; +import { cacheDir } from "../../common/filesystem/utils"; import { eSubscribeEvent, eUnSubscribeEvent } from "../../services/event-manager"; +import { useThemeStore } from "../../stores/use-theme-store"; import BaseDialog from "../dialog/base-dialog"; import { IconButton } from "../ui/icon-button"; +import { ProgressBarComponent } from "../ui/svg/lazy"; const ImagePreview = () => { + const colors = useThemeStore((state) => state.colors); const [visible, setVisible] = useState(false); const [image, setImage] = useState(""); + const [loading, setLoading] = useState(false); useEffect(() => { eSubscribeEvent("ImagePreview", open); @@ -39,9 +45,20 @@ const ImagePreview = () => { }; }, []); - const open = (image) => { - setImage(image); + const open = async (image) => { 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 = () => { @@ -59,44 +76,60 @@ const ImagePreview = () => { backgroundColor: "black" }} > - <>} - enableSwipeDown - useNativeDriver - onSwipeDown={close} - saveToLocalByLongPress={false} - renderHeader={() => ( - - { - close(); + {loading ? ( + + + + ) : ( + <>} + enableSwipeDown + useNativeDriver + onSwipeDown={close} + saveToLocalByLongPress={false} + renderHeader={() => ( + - - )} - imageUrls={[ - { - url: image - } - ]} - /> + > + { + close(); + }} + /> + + )} + imageUrls={[ + { + url: image + } + ]} + /> + )} ) diff --git a/apps/mobile/app/screens/editor/source.ts b/apps/mobile/app/screens/editor/source.ts index 3f343edf2..c47ac529c 100644 --- a/apps/mobile/app/screens/editor/source.ts +++ b/apps/mobile/app/screens/editor/source.ts @@ -28,5 +28,5 @@ const EditorMobileSourceUrl = * The url should be something like this: http://192.168.100.126:3000/index.html */ export const EDITOR_URI = __DEV__ - ? EditorMobileSourceUrl + ? "http://192.168.8.103:3000/index.html" : EditorMobileSourceUrl; diff --git a/apps/mobile/app/screens/editor/tiptap/editor-events.ts b/apps/mobile/app/screens/editor/tiptap/editor-events.ts index eb7ce414a..8e2297e17 100644 --- a/apps/mobile/app/screens/editor/tiptap/editor-events.ts +++ b/apps/mobile/app/screens/editor/tiptap/editor-events.ts @@ -34,5 +34,6 @@ export const EventTypes = { fullscreen: "editor-event:fullscreen", link: "editor-event:link", contentchange: "editor-event:content-change", - reminders: "editor-event:reminders" + reminders: "editor-event:reminders", + previewAttachment: "editor-event:preview-attachment" }; diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts b/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts index 66773168b..7be714a60 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.ts @@ -29,6 +29,7 @@ import { } from "react-native"; import { WebViewMessageEvent } from "react-native-webview"; import { db } from "../../../common/database"; +import ImagePreview from "../../../components/image-preview"; import { RelationsList } from "../../../components/sheets/relations-list"; import ReminderSheet from "../../../components/sheets/reminder"; import useKeyboard from "../../../hooks/use-keyboard"; @@ -380,6 +381,10 @@ export const useEditorEvents = ( case EventTypes.link: openLinkInBrowser(editorMessage.value as string); break; + + case EventTypes.previewAttachment: + eSendEvent("ImagePreview", editorMessage.value); + break; default: break; } diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx index b5a663c9b..fd990fdd5 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -92,6 +92,10 @@ const Tiptap = ({ global.editorController.downloadAttachment(attachment); return true; }, + onPreviewAttachment(editor, attachment) { + global.editorController.previewAttachment(attachment); + return true; + }, theme: editorTheme, element: !layout ? undefined : contentRef.current || undefined, editable: !settings.readonly, diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index 6528d0147..86c2fd29a 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -58,6 +58,7 @@ export type EditorController = { setTitle: React.Dispatch>; openFilePicker: (type: "image" | "file" | "camera") => void; downloadAttachment: (attachment: Attachment) => void; + previewAttachment: (attachment: Attachment) => void; content: MutableRefObject; onUpdate: () => void; titlePlaceholder: string; @@ -170,7 +171,9 @@ export function useEditorController(update: () => void): EditorController { const downloadAttachment = useCallback((attachment: Attachment) => { post(EventTypes.download, attachment); }, []); - + const previewAttachment = useCallback((attachment: Attachment) => { + post(EventTypes.previewAttachment, attachment); + }, []); const openLink = useCallback((url: string) => { post(EventTypes.link, url); return true; @@ -187,6 +190,7 @@ export function useEditorController(update: () => void): EditorController { setTitlePlaceholder, openFilePicker, downloadAttachment, + previewAttachment, content: htmlContentRef, openLink, onUpdate: onUpdate diff --git a/packages/editor-mobile/src/utils/index.ts b/packages/editor-mobile/src/utils/index.ts index 564b12462..286bc262d 100644 --- a/packages/editor-mobile/src/utils/index.ts +++ b/packages/editor-mobile/src/utils/index.ts @@ -145,7 +145,8 @@ export const EventTypes = { fullscreen: "editor-event:fullscreen", link: "editor-event:link", contentchange: "editor-event:content-change", - reminders: "editor-event:reminders" + reminders: "editor-event:reminders", + previewAttachment: "editor-event:preview-attachment" } as const; export function isReactNative(): boolean { diff --git a/packages/editor/src/extensions/attachment/attachment.ts b/packages/editor/src/extensions/attachment/attachment.ts index 5427a5dd6..fc50d3541 100644 --- a/packages/editor/src/extensions/attachment/attachment.ts +++ b/packages/editor/src/extensions/attachment/attachment.ts @@ -27,6 +27,7 @@ export interface AttachmentOptions { HTMLAttributes: Record; onDownloadAttachment: (editor: Editor, attachment: Attachment) => boolean; onOpenAttachmentPicker: (editor: Editor, type: AttachmentType) => boolean; + onPreviewAttachment: (editor: Editor, attachment: Attachment) => boolean; } export type AttachmentWithProgress = AttachmentProgress & Attachment; @@ -52,6 +53,7 @@ declare module "@tiptap/core" { removeAttachment: () => ReturnType; downloadAttachment: (attachment: Attachment) => ReturnType; setAttachmentProgress: (progress: AttachmentProgress) => ReturnType; + previewAttachment: (options: Attachment) => ReturnType; }; } } @@ -67,7 +69,8 @@ export const AttachmentNode = Node.create({ return { HTMLAttributes: {}, onDownloadAttachment: () => false, - onOpenAttachmentPicker: () => false + onOpenAttachmentPicker: () => false, + onPreviewAttachment: () => false }; }, @@ -158,6 +161,12 @@ export const AttachmentNode = Node.create({ tr.setMeta("addToHistory", false); if (dispatch) dispatch(tr); return true; + }, + + previewAttachment: + (attachment) => + ({ editor }) => { + return this.options.onPreviewAttachment(editor, attachment); } }; } diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index f20e2ac1a..28c7681e0 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -17,64 +17,64 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -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 { 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 { EditorOptions, extensions as TiptapCoreExtensions, getHTMLFromFragment } from "@tiptap/core"; -import { usePermissionHandler } from "./hooks/use-permission-handler"; -import { Highlight } from "./extensions/highlight"; -import { Paragraph } from "./extensions/paragraph"; -import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer"; +import CharacterCount from "@tiptap/extension-character-count"; import { Code } from "@tiptap/extension-code"; -import { DateTime } from "./extensions/date-time"; -import { OpenLink, OpenLinkOptions } from "./extensions/open-link"; +import Color from "@tiptap/extension-color"; 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 { 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 { 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"; const CoreExtensions = Object.entries(TiptapCoreExtensions) @@ -85,6 +85,7 @@ const CoreExtensions = Object.entries(TiptapCoreExtensions) type TiptapOptions = EditorOptions & Omit & Omit & + Omit & OpenLinkOptions & { downloadOptions?: DownloadOptions; theme: Theme; @@ -104,6 +105,7 @@ const useTiptap = ( isKeyboardOpen, onDownloadAttachment, onOpenAttachmentPicker, + onPreviewAttachment, onOpenLink, onBeforeCreate, downloadOptions, @@ -219,7 +221,8 @@ const useTiptap = ( EmbedNode, AttachmentNode.configure({ onDownloadAttachment, - onOpenAttachmentPicker + onOpenAttachmentPicker, + onPreviewAttachment }), OutlineListItem, OutlineList, @@ -241,6 +244,7 @@ const useTiptap = ( injectCSS: false }), [ + onPreviewAttachment, onDownloadAttachment, onOpenAttachmentPicker, PortalProviderAPI, @@ -260,6 +264,12 @@ const useTiptap = ( 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 { useTiptap, Toolbar, @@ -267,9 +277,3 @@ export { getHTMLFromFragment, 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"; diff --git a/packages/editor/src/toolbar/icons.ts b/packages/editor/src/toolbar/icons.ts index 17146c0a5..7614ca104 100644 --- a/packages/editor/src/toolbar/icons.ts +++ b/packages/editor/src/toolbar/icons.ts @@ -113,7 +113,8 @@ import { mdiWeb, mdiPageNextOutline, mdiSortBoolAscendingVariant, - mdiApplicationCogOutline + mdiApplicationCogOutline, + mdiArrowExpand } from "@mdi/js"; export const Icons = { @@ -156,6 +157,7 @@ export const Icons = { fullscreen: mdiFullscreen, url: mdiLink, image: mdiImageOutline, + previewAttachment: mdiArrowExpand, imageDownload: mdiProgressDownload, imageFailed: mdiProgressAlert, imageSettings: mdiImageEditOutline, diff --git a/packages/editor/src/toolbar/tool-definitions.ts b/packages/editor/src/toolbar/tool-definitions.ts index 0b56b02a0..e30ad2382 100644 --- a/packages/editor/src/toolbar/tool-definitions.ts +++ b/packages/editor/src/toolbar/tool-definitions.ts @@ -237,7 +237,6 @@ const tools: Record = { title: "Cell border width", conditional: true }, - imageSettings: { icon: "imageSettings", title: "Image settings", @@ -263,6 +262,11 @@ const tools: Record = { title: "Image properties", conditional: true }, + previewAttachment: { + icon: "previewAttachment", + title: "Preview attachment", + conditional: true + }, attachmentSettings: { icon: "attachmentSettings", title: "Attachment settings", @@ -354,6 +358,7 @@ export const STATIC_TOOLBAR_GROUPS: ToolbarDefinition = [ "cellProperties", "imageSettings", "embedSettings", + "previewAttachment", "attachmentSettings", "linkSettings", "codeRemove", diff --git a/packages/editor/src/toolbar/tools/attachment.tsx b/packages/editor/src/toolbar/tools/attachment.tsx index 1d056e8ca..0d4abb437 100644 --- a/packages/editor/src/toolbar/tools/attachment.tsx +++ b/packages/editor/src/toolbar/tools/attachment.tsx @@ -27,7 +27,11 @@ import { Attachment } from "../../extensions/attachment"; export function AttachmentSettings(props: ToolProps) { const { editor } = props; const isBottom = useToolbarLocation() === "bottom"; - if (!editor.isActive("attachment") || !isBottom) return null; + if ( + (!editor.isActive("attachment") && !editor.isActive("image")) || + !isBottom + ) + return null; return ( { + 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) { const { editor } = props; return ( diff --git a/packages/editor/src/toolbar/tools/index.ts b/packages/editor/src/toolbar/tools/index.ts index 98611ba1a..6be37cdea 100644 --- a/packages/editor/src/toolbar/tools/index.ts +++ b/packages/editor/src/toolbar/tools/index.ts @@ -71,6 +71,7 @@ import { import { AttachmentSettings, DownloadAttachment, + PreviewAttachment, RemoveAttachment } from "./attachment"; import { @@ -133,6 +134,7 @@ const tools = { webclipOpenSource: WebClipOpenSource, webclipSettings: WebClipSettings, + previewAttachment: PreviewAttachment, attachmentSettings: AttachmentSettings, downloadAttachment: DownloadAttachment, removeAttachment: RemoveAttachment,