Skip to content

Commit 390933b

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 390933b

File tree

5 files changed

+90
-5
lines changed

5 files changed

+90
-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: 53 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,37 @@ 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 DownloadedFileRecord = {
22+
id: string;
23+
filePath: string;
24+
};
25+
26+
const downloadedFiles = new Map<string, DownloadedFileRecord>();
27+
const downloadOrder: string[] = [];
28+
29+
function trackDownloadedFile(filePath: string): DownloadedFileRecord {
30+
const record: DownloadedFileRecord = {
31+
id: randomBytes(16).toString("hex"),
32+
filePath,
33+
};
34+
35+
downloadedFiles.set(record.id, record);
36+
downloadOrder.push(record.id);
37+
38+
if (downloadOrder.length > maxTrackedDownloads) {
39+
const oldestId = downloadOrder.shift()!;
40+
downloadedFiles.delete(oldestId);
41+
}
42+
43+
return record;
44+
}
45+
46+
export function getDownloadedFilePath(downloadId: string): string | undefined {
47+
return downloadedFiles.get(downloadId)?.filePath;
48+
}
49+
1850
function isUploadsUrl(server: string, url: URL): boolean {
1951
return url.origin === server && url.pathname.startsWith("/user_uploads/");
2052
}
@@ -125,16 +157,34 @@ export default function handleExternalLink(
125157
url: url.href,
126158
downloadPath,
127159
async completed(filePath: string, fileName: string) {
160+
const notificationTitle = t.__("Download complete");
161+
const notificationBody = t.__(
162+
"Click to view the folder where the file ({{{fileName}}}) was downloaded.",
163+
{
164+
fileName,
165+
},
166+
);
167+
// Show native notification
128168
const downloadNotification = new Notification({
129-
title: t.__("Download Complete"),
130-
body: t.__("Click to show {{{fileName}}} in folder", {fileName}),
169+
title: notificationTitle,
170+
body: notificationBody,
131171
silent: true, // We'll play our own sound - ding.ogg
132172
});
133173
downloadNotification.on("click", () => {
134174
// Reveal file in download folder
135175
shell.showItemInFolder(filePath);
136176
});
137177
downloadNotification.show();
178+
const {id: downloadId} = trackDownloadedFile(filePath);
179+
// Event to show in-app notification in addition to the native
180+
// notification.
181+
send(
182+
contents,
183+
"show-download-success",
184+
notificationTitle,
185+
notificationBody,
186+
downloadId,
187+
);
138188

139189
// Play sound to indicate download complete
140190
if (!ConfigUtil.getConfigItem("silent", false)) {
@@ -150,7 +200,7 @@ export default function handleExternalLink(
150200
if (state !== "cancelled") {
151201
if (ConfigUtil.getConfigItem("promptDownload", false)) {
152202
new Notification({
153-
title: t.__("Download Complete"),
203+
title: t.__("Download complete"),
154204
body: t.__("Download failed"),
155205
}).show();
156206
} 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)