diff --git a/apps/desktop/src/api/os-integration.ts b/apps/desktop/src/api/os-integration.ts index e3192d045..9ee21b79b 100644 --- a/apps/desktop/src/api/os-integration.ts +++ b/apps/desktop/src/api/os-integration.ts @@ -33,7 +33,7 @@ import { AutoLaunch } from "../utils/autolaunch"; import { config, DesktopIntegration } from "../utils/config"; import { bringToFront } from "../utils/bring-to-front"; import { getTheme, setTheme, Theme } from "../utils/theme"; -import { mkdirSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, writeFileSync } from "fs"; import { dirname } from "path"; import { resolvePath } from "../utils/resolve-path"; import { observable } from "@trpc/server/observable"; @@ -43,6 +43,7 @@ import { setupDesktopIntegration } from "../utils/desktop-integration"; import { rm } from "fs/promises"; import { disableCustomDns, enableCustomDns } from "../utils/custom-dns"; import type { MenuItem as NNMenuItem } from "@notesnook/ui"; +import { platform } from "os"; const t = initTRPC.create(); @@ -192,9 +193,28 @@ export const osIntegrationRouter = t.router({ }), openPath: t.procedure .input(z.object({ type: z.literal("path"), link: z.string() })) - .query(({ input }) => { + .query(async ({ input }) => { + if (isFlatpak()) return; + const { type, link } = input; - if (type === "path") return shell.openPath(resolvePath(link)); + if (type !== "path") return; + + const resolvedPath = resolvePath( + // remove leading slash from path on windows + platform() === "win32" ? link.slice(1) : link + ); + if (!existsSync(resolvedPath)) { + if (globalThis.window) { + await dialog.showMessageBox(globalThis.window, { + type: "error", + title: "Path not found", + message: `The path does not exist:\n${wrapPath(resolvedPath)}` + }); + } + return; + } + + await shell.openPath(resolvedPath); }), bringToFront: t.procedure.query(() => bringToFront()), changeTheme: t.procedure @@ -299,3 +319,7 @@ function toMenuItem( } } } + +function wrapPath(path: string, maxLineLength = 100): string { + return path.replace(new RegExp(`(.{${maxLineLength}})`, "g"), "$1\n"); +} diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index d32117318..a6fae9907 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -58,6 +58,7 @@ import { useStore as useThemeStore } from "../../stores/theme-store"; import { writeToClipboard } from "../../utils/clipboard"; import { useEditorStore } from "../../stores/editor-store"; import { DayFormat, parseInternalLink } from "@notesnook/core"; +import { desktop } from "../../common/desktop-bridge"; import Skeleton from "react-loading-skeleton"; import useMobile from "../../hooks/use-mobile"; import useTablet from "../../hooks/use-tablet"; @@ -68,6 +69,7 @@ import { showFeatureNotAllowedToast } from "../../common/toasts"; import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog"; import { ConfirmDialog } from "../../dialogs/confirm"; import { strings } from "@notesnook/intl"; +import { showToast } from "../../utils/toast"; export type OnChangeHandler = ( content: () => string, @@ -421,13 +423,32 @@ function TipTap(props: TipTapProps) { previewAttachment: onPreviewAttachment, createInternalLink: onInsertInternalLink, getAttachmentData: onGetAttachmentData, - openLink: (url, openInNewTab) => { + openLink: async (url, openInNewTab) => { const link = parseInternalLink(url); if (link && link.type === "note") { useEditorStore.getState().openSession(link.id, { activeBlockId: link.params?.blockId || undefined, openInNewTab: openInNewTab }); + } else if (url.startsWith("file:")) { + if (!IS_DESKTOP_APP) { + showToast("error", strings.cantOpenFileLinksInBrowsers()); + return; + } + + const path = new URL(url).pathname; + const ok = await ConfirmDialog.show({ + title: strings.openingLocalFile(), + message: strings.openingLocalFileDesc(path), + positiveButtonText: strings.open(), + negativeButtonText: strings.cancel() + }); + if (!ok) return; + + await desktop?.integration.openPath.query({ + type: "path", + link: decodeURIComponent(path) + }); } else window.open(url, "_blank"); } }; diff --git a/apps/web/src/dialogs/confirm.tsx b/apps/web/src/dialogs/confirm.tsx index 27abf84ce..79c8873cd 100644 --- a/apps/web/src/dialogs/confirm.tsx +++ b/apps/web/src/dialogs/confirm.tsx @@ -123,6 +123,7 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog( ) : null} diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 8a3570685..39ba48777 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -948,6 +948,10 @@ msgstr "Are you sure you want to logout and clear all data stored on THIS DEVICE msgid "Are you sure you want to logout from this device? Any unsynced changes will be lost." msgstr "Are you sure you want to logout from this device? Any unsynced changes will be lost." +#: src/strings.ts:2687 +msgid "Are you sure you want to open this file: {filePath}?" +msgstr "Are you sure you want to open this file: {filePath}?" + #: src/strings.ts:1046 msgid "Are you sure you want to remove your name?" msgstr "Are you sure you want to remove your name?" @@ -2933,6 +2937,10 @@ msgstr "File length is 0. Please upload this file again from the attachment mana msgid "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." msgstr "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." +#: src/strings.ts:2689 +msgid "File links cannot be opened in browsers. Please use the Notesnook desktop app." +msgstr "File links cannot be opened in browsers. Please use the Notesnook desktop app." + #: src/strings.ts:949 msgid "File mismatch" msgstr "File mismatch" @@ -4517,6 +4525,10 @@ msgstr "Open source." msgid "Open the two-factor authentication (TOTP) app to view your authentication code." msgstr "Open the two-factor authentication (TOTP) app to view your authentication code." +#: src/strings.ts:2685 +msgid "Opening local file" +msgstr "Opening local file" + #: src/strings.ts:1857 msgid "Optional" msgstr "Optional" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 5af418f71..68a0acd5d 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -948,6 +948,10 @@ msgstr "" msgid "Are you sure you want to logout from this device? Any unsynced changes will be lost." msgstr "" +#: src/strings.ts:2687 +msgid "Are you sure you want to open this file: {filePath}?" +msgstr "" + #: src/strings.ts:1046 msgid "Are you sure you want to remove your name?" msgstr "" @@ -2922,6 +2926,10 @@ msgstr "" msgid "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." msgstr "" +#: src/strings.ts:2689 +msgid "File links cannot be opened in browsers. Please use the Notesnook desktop app." +msgstr "" + #: src/strings.ts:949 msgid "File mismatch" msgstr "" @@ -4491,6 +4499,10 @@ msgstr "" msgid "Open the two-factor authentication (TOTP) app to view your authentication code." msgstr "" +#: src/strings.ts:2685 +msgid "Opening local file" +msgstr "" + #: src/strings.ts:1857 msgid "Optional" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 42ecbee89..9f5ebcd60 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2681,5 +2681,10 @@ Continue without attachments?`, encrypting: () => t`Encrypting`, fileSizeLimitExceededPleaseUpgrade: () => t`File size limit exceeded. Please upgrade your plan.`, - compressionFailed: () => t`Compression failed` + compressionFailed: () => t`Compression failed`, + openingLocalFile: () => t`Opening local file`, + openingLocalFileDesc: (filePath: string) => + t`Are you sure you want to open this file: ${filePath}?`, + cantOpenFileLinksInBrowsers: () => + t`File links cannot be opened in browsers. Please use the Notesnook desktop app.` };