Skip to content

Commit a593cc1

Browse files
committed
download: Show in-app notification on downlaoding a file.
Fixes #1371. We do not accept a filePath from the web app when recieving the event to show the file in a folder, since that could introduce securithy vulnerabilies. We instead keep the list of latest 50 downloaded files in memory and give each filePath a unique downloadId that can be passed around between the webapp and the desktop app.
1 parent 3956252 commit a593cc1

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed

app/common/typed-ipc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type MainMessage = {
1313
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
1414
"realm-name-changed": (serverURL: string, realmName: string) => void;
1515
"reload-full-app": () => void;
16+
"show-downloaded-file-in-folder": (downloadId: string) => void;
1617
"save-last-tab": (index: number) => void;
1718
"switch-server-tab": (index: number) => void;
1819
"toggle-app": () => void;
@@ -59,6 +60,11 @@ export type RendererMessage = {
5960
"reload-proxy": (showAlert: boolean) => void;
6061
"reload-viewer": () => void;
6162
"render-taskbar-icon": (messageCount: number) => void;
63+
"show-download-success": (
64+
title: string,
65+
description: string,
66+
downloadId: string,
67+
) => void;
6268
"set-active": () => void;
6369
"set-idle": () => void;
6470
"show-keyboard-shortcuts": () => void;

app/main/handle-external-link.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type WebContents,
77
app,
88
} from "electron/main";
9+
import {randomBytes} from "node:crypto";
910
import fs from "node:fs";
1011
import path from "node:path";
1112

@@ -15,6 +16,35 @@ import * as t from "../common/translation-util.ts";
1516

1617
import {send} from "./typed-ipc-main.ts";
1718

19+
const maxTrackedDownloads = 50;
20+
21+
type DownloadedFile = {
22+
id: string;
23+
filePath: string;
24+
};
25+
26+
const downloadedFiles = new Map<string, DownloadedFile>();
27+
28+
function trackDownloadedFile(filePath: string): DownloadedFile {
29+
const downloadedFile: DownloadedFile = {
30+
id: randomBytes(16).toString("hex"),
31+
filePath,
32+
};
33+
34+
downloadedFiles.set(downloadedFile.id, downloadedFile);
35+
36+
if (downloadedFiles.size > maxTrackedDownloads) {
37+
const oldestDownloadedFileId = [...downloadedFiles.keys()][0];
38+
downloadedFiles.delete(oldestDownloadedFileId);
39+
}
40+
41+
return downloadedFile;
42+
}
43+
44+
export function getDownloadedFilePath(downloadId: string): string | undefined {
45+
return downloadedFiles.get(downloadId)?.filePath;
46+
}
47+
1848
function isUploadsUrl(server: string, url: URL): boolean {
1949
return url.origin === server && url.pathname.startsWith("/user_uploads/");
2050
}
@@ -125,16 +155,31 @@ export default function handleExternalLink(
125155
url: url.href,
126156
downloadPath,
127157
async completed(filePath: string, fileName: string) {
158+
const notificationTitle = t.__("Downloaded {{{fileName}}}.", {
159+
fileName,
160+
});
161+
const notificationBody = t.__("Click to open downloads folder.");
162+
// Show native notification
128163
const downloadNotification = new Notification({
129-
title: t.__("Download Complete"),
130-
body: t.__("Click to show {{{fileName}}} in folder", {fileName}),
164+
title: notificationTitle,
165+
body: notificationBody,
131166
silent: true, // We'll play our own sound - ding.ogg
132167
});
133168
downloadNotification.on("click", () => {
134169
// Reveal file in download folder
135170
shell.showItemInFolder(filePath);
136171
});
137172
downloadNotification.show();
173+
const {id: downloadId} = trackDownloadedFile(filePath);
174+
// Event to show in-app notification in addition to the native
175+
// notification.
176+
send(
177+
contents,
178+
"show-download-success",
179+
notificationTitle,
180+
notificationBody,
181+
downloadId,
182+
);
138183

139184
// Play sound to indicate download complete
140185
if (!ConfigUtil.getConfigItem("silent", false)) {
@@ -150,7 +195,7 @@ export default function handleExternalLink(
150195
if (state !== "cancelled") {
151196
if (ConfigUtil.getConfigItem("promptDownload", false)) {
152197
new Notification({
153-
title: t.__("Download Complete"),
198+
title: t.__("Download complete"),
154199
body: t.__("Download failed"),
155200
}).show();
156201
} else {

app/main/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {clipboard} from "electron/common";
1+
import {clipboard, shell} from "electron/common";
22
import {
33
BrowserWindow,
44
type IpcMainEvent,
@@ -25,7 +25,9 @@ import type {MenuProperties} from "../common/types.ts";
2525

2626
import {appUpdater, shouldQuitForUpdate} from "./autoupdater.ts";
2727
import * as BadgeSettings from "./badge-settings.ts";
28-
import handleExternalLink from "./handle-external-link.ts";
28+
import handleExternalLink, {
29+
getDownloadedFilePath,
30+
} from "./handle-external-link.ts";
2931
import * as AppMenu from "./menu.ts";
3032
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.ts";
3133
import {sentryInit} from "./sentry.ts";
@@ -452,6 +454,13 @@ function createMainWindow(): BrowserWindow {
452454
},
453455
);
454456

457+
ipcMain.on("show-downloaded-file-in-folder", (_event, downloadId: string) => {
458+
const filePath = getDownloadedFilePath(downloadId);
459+
if (filePath !== undefined) {
460+
shell.showItemInFolder(filePath);
461+
}
462+
});
463+
455464
ipcMain.on("save-last-tab", (_event, index: number) => {
456465
ConfigUtil.setConfigItem("lastActiveTab", index);
457466
});

app/renderer/js/electron-bridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ bridgeEvents.addEventListener("realm_icon_url", (event) => {
103103
);
104104
});
105105

106+
bridgeEvents.addEventListener("show-downloaded-file-in-folder", (event) => {
107+
const [downloadId] = z
108+
.tuple([z.string()])
109+
.parse(z.instanceof(BridgeEvent).parse(event).arguments_);
110+
ipcRenderer.send("show-downloaded-file-in-folder", downloadId);
111+
});
112+
106113
// Set user as active and update the time of last activity
107114
ipcRenderer.on("set-active", () => {
108115
idle = false;

app/renderer/js/preload.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ ipcRenderer.on("show-notification-settings", () => {
1818
bridgeEvents.dispatchEvent(new BridgeEvent("show-notification-settings"));
1919
});
2020

21+
ipcRenderer.on(
22+
"show-download-success",
23+
(_event, title: string, description: string, downloadId: string) => {
24+
bridgeEvents.dispatchEvent(
25+
new BridgeEvent("show-download-success", [
26+
title,
27+
description,
28+
downloadId,
29+
]),
30+
);
31+
},
32+
);
33+
2134
window.addEventListener("load", () => {
2235
if (!location.href.includes("app/renderer/network.html")) {
2336
return;

0 commit comments

Comments
 (0)