Skip to content

Commit 4020cf6

Browse files
committed
Fix markdown links and event listener memory leak
1 parent b98ab7b commit 4020cf6

3 files changed

Lines changed: 89 additions & 63 deletions

File tree

src/main/IpcHandlers.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,16 @@ function handleRequestArchive(event: IpcMainEvent, lineNumber?: number): void {
303303

304304
// ─── System handlers ─────────────────────────────────────────────────────
305305

306-
function handleOpenInBrowser(_: IpcMainEvent, url: string): void {
306+
async function handleOpenInBrowser(
307+
event: IpcMainEvent,
308+
url: string,
309+
): Promise<void> {
307310
try {
308-
shell.openExternal(url);
311+
await shell.openExternal(url);
309312
} catch (error) {
310-
if (error instanceof Error) HandleError(error);
313+
if (error instanceof Error) {
314+
event.reply("responseFromMainProcess", error);
315+
}
311316
}
312317
}
313318

src/renderer/Archive.tsx

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useEffect, useState, useCallback } from "react";
22
import { useTranslation } from "react-i18next";
33
import { PromptItem } from "@sleek-types";
44

@@ -18,63 +18,69 @@ const ArchiveComponent: React.FC<ArchiveComponentProps> = ({
1818
number | undefined
1919
>(undefined);
2020

21-
const handleArchiveAllConfirm = (): void => {
21+
const handleArchiveAllConfirm = useCallback((): void => {
2222
ipcRenderer.send("archiveTodos");
23-
};
23+
}, []);
2424

25-
const handleArchiveSingleConfirm = (): void => {
25+
const handleArchiveSingleConfirm = useCallback((): void => {
2626
ipcRenderer.send("archiveSingleTodo", archiveLineNumber);
27-
};
27+
}, [archiveLineNumber]);
2828

29-
const handleOpenDoneFile = (): void => {
29+
const handleOpenDoneFile = useCallback((): void => {
3030
ipcRenderer.send("openFile", true, archiveLineNumber);
31-
};
31+
}, [archiveLineNumber]);
3232

33-
const handleCreateDoneFile = (): void => {
33+
const handleCreateDoneFile = useCallback((): void => {
3434
ipcRenderer.send("createFile", true, archiveLineNumber);
35-
};
36-
37-
const handleTriggerArchiving = (
38-
doneFileAvailable: boolean,
39-
lineNumber?: number,
40-
): void => {
41-
setArchiveLineNumber(lineNumber);
42-
43-
// Define prompt items here to get fresh translations every time
44-
const promptItemArchivingAll = {
45-
id: "archive",
46-
headline: t("prompt.archive.headline"),
47-
text: t("prompt.archive.text"),
48-
button1: t("archive"),
49-
onButton1: handleArchiveAllConfirm,
50-
};
51-
52-
const promptItemArchivingSingle = {
53-
id: "archive",
54-
headline: t("prompt.archive.headline.single"),
55-
text: t("prompt.archive.text.single"),
56-
button1: t("archive"),
57-
onButton1: handleArchiveSingleConfirm,
58-
};
59-
60-
const promptItemChooseChangeFile = {
61-
id: "changeFile",
62-
headline: t("prompt.archive.changeFile.headline"),
63-
text: t("prompt.archive.changeFile.text"),
64-
button1: t("openFile"),
65-
onButton1: handleOpenDoneFile,
66-
button2: t("createFile"),
67-
onButton2: handleCreateDoneFile,
68-
};
69-
70-
setPromptItem(
71-
doneFileAvailable
72-
? lineNumber
73-
? promptItemArchivingSingle
74-
: promptItemArchivingAll
75-
: promptItemChooseChangeFile,
76-
);
77-
};
35+
}, [archiveLineNumber]);
36+
37+
const handleTriggerArchiving = useCallback(
38+
(doneFileAvailable: boolean, lineNumber?: number): void => {
39+
setArchiveLineNumber(lineNumber);
40+
41+
// Define prompt items here to get fresh translations every time
42+
const promptItemArchivingAll = {
43+
id: "archive",
44+
headline: t("prompt.archive.headline"),
45+
text: t("prompt.archive.text"),
46+
button1: t("archive"),
47+
onButton1: handleArchiveAllConfirm,
48+
};
49+
50+
const promptItemArchivingSingle = {
51+
id: "archive",
52+
headline: t("prompt.archive.headline.single"),
53+
text: t("prompt.archive.text.single"),
54+
button1: t("archive"),
55+
onButton1: handleArchiveSingleConfirm,
56+
};
57+
58+
const promptItemChooseChangeFile = {
59+
id: "changeFile",
60+
headline: t("prompt.archive.changeFile.headline"),
61+
text: t("prompt.archive.changeFile.text"),
62+
button1: t("openFile"),
63+
onButton1: handleOpenDoneFile,
64+
button2: t("createFile"),
65+
onButton2: handleCreateDoneFile,
66+
};
67+
68+
setPromptItem(
69+
doneFileAvailable
70+
? lineNumber
71+
? promptItemArchivingSingle
72+
: promptItemArchivingAll
73+
: promptItemChooseChangeFile,
74+
);
75+
},
76+
[
77+
t,
78+
handleArchiveAllConfirm,
79+
handleArchiveSingleConfirm,
80+
handleOpenDoneFile,
81+
handleCreateDoneFile,
82+
],
83+
);
7884

7985
useEffect((): void => {
8086
if (triggerArchiving) {

src/renderer/Grid/Renderer.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { JSX, memo } from "react";
2-
import ReactMarkdown, { Components } from "react-markdown";
2+
import ReactMarkdown, { Components, defaultUrlTransform } from "react-markdown";
33
import remarkGfm from "remark-gfm";
44
import Chip from "@mui/material/Chip";
55
import { SnackbarAction } from "../../@types";
@@ -235,6 +235,15 @@ const RendererComponent: React.FC<RendererComponentProps> = memo(
235235
"custom-tag": (value) => <span>{value}</span>,
236236
};
237237

238+
const allowCustomProtocols = (url: string): string => {
239+
// Allow file:// and other custom protocol URLs (joplin://, cbthunderlink://, etc.)
240+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//i.test(url)) {
241+
return url; // pass through all protocol URLs untouched
242+
}
243+
// Fall back to default security behavior for web URLs
244+
return defaultUrlTransform(url);
245+
};
246+
238247
const options: Components = {
239248
p: ({ children }): JSX.Element | null => {
240249
const mappedChildren = React.Children.map(children, (child) => {
@@ -328,11 +337,13 @@ const RendererComponent: React.FC<RendererComponentProps> = memo(
328337
const childrenStr =
329338
typeof children === "string" ? children : String(children);
330339

331-
// Use href from destructure first, then fall back
332-
// Treat empty strings as missing - trim whitespace and use if available
333-
const href = hrefFromDestructure?.trim() || childrenStr;
340+
// Use href from ReactMarkdown if available and non-empty.
341+
// Otherwise extract a URL from the children text (bare URL case).
342+
const urlInChildren =
343+
/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+)/g.exec(childrenStr)?.[1] ?? null;
344+
const href =
345+
hrefFromDestructure?.trim() || urlInChildren || childrenStr;
334346

335-
const match = /([a-zA-Z]+:\/\/\S+)/g.exec(childrenStr);
336347
const maxChars = 40;
337348
const truncatedChildren =
338349
childrenStr.length > maxChars
@@ -343,9 +354,9 @@ const RendererComponent: React.FC<RendererComponentProps> = memo(
343354
<a
344355
{...restProps}
345356
href={href}
346-
onClick={(event) =>
347-
handleLinkClick(event, match ? childrenStr : href || childrenStr)
348-
}
357+
onClick={(event) => {
358+
handleLinkClick(event, href);
359+
}}
349360
>
350361
{truncatedChildren}
351362
<OpenInNewIcon />
@@ -372,7 +383,11 @@ const RendererComponent: React.FC<RendererComponentProps> = memo(
372383
};
373384

374385
return (
375-
<ReactMarkdown remarkPlugins={[remarkGfm]} components={options}>
386+
<ReactMarkdown
387+
remarkPlugins={[remarkGfm]}
388+
components={options}
389+
urlTransform={allowCustomProtocols}
390+
>
376391
{preprocessBody(todoObject.body)}
377392
</ReactMarkdown>
378393
);

0 commit comments

Comments
 (0)