desktop: add support for opening file links (#9784)

* desktop: add support for opening file links
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: unescape paths with spaces

* web: wrap message in confirm dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: show message box if path doesn't exist when opening file link
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: disable opening file links on flatpak
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: wrap long file paths in electron dialog
* on linux, long messages in dialog aren't wrapped, so we need to wrap manually

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: fix persistent "path not found" error

* intl: update strings

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
01zulfi
2026-05-19 13:27:46 +05:00
committed by GitHub
parent b594d58d31
commit 8f8ac25d20
6 changed files with 80 additions and 5 deletions

View File

@@ -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");
}

View File

@@ -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");
}
};

View File

@@ -123,6 +123,7 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog(
<Text
as="div"
variant="body"
sx={{ overflowWrap: "break-word" }}
dangerouslySetInnerHTML={{ __html: mdToHtml(message) }}
/>
) : null}

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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.`
};