Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/common/typed-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type MainMessage = {
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
"realm-name-changed": (serverURL: string, realmName: string) => void;
"reload-full-app": () => void;
"show-downloaded-file-in-folder": (downloadId: string) => void;
"save-last-tab": (index: number) => void;
"switch-server-tab": (index: number) => void;
"toggle-app": () => void;
Expand Down Expand Up @@ -59,6 +60,11 @@ export type RendererMessage = {
"reload-proxy": (showAlert: boolean) => void;
"reload-viewer": () => void;
"render-taskbar-icon": (messageCount: number) => void;
"show-download-success": (
title: string,
description: string,
downloadId: string,
) => void;
"set-active": () => void;
"set-idle": () => void;
"show-keyboard-shortcuts": () => void;
Expand Down
51 changes: 48 additions & 3 deletions app/main/handle-external-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type WebContents,
app,
} from "electron/main";
import {randomBytes} from "node:crypto";
import fs from "node:fs";
import path from "node:path";

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

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

const maxTrackedDownloads = 50;

type DownloadedFile = {
id: string;
filePath: string;
};

const downloadedFiles = new Map<string, DownloadedFile>();

function trackDownloadedFile(filePath: string): DownloadedFile {
const downloadedFile: DownloadedFile = {
id: randomBytes(16).toString("hex"),
filePath,
};

downloadedFiles.set(downloadedFile.id, downloadedFile);

if (downloadedFiles.size > maxTrackedDownloads) {
const oldestDownloadedFileId = [...downloadedFiles.keys()][0];
downloadedFiles.delete(oldestDownloadedFileId);
}

return downloadedFile;
}

export function getDownloadedFilePath(downloadId: string): string | undefined {
return downloadedFiles.get(downloadId)?.filePath;
}

function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith("/user_uploads/");
}
Expand Down Expand Up @@ -125,16 +155,31 @@ export default function handleExternalLink(
url: url.href,
downloadPath,
async completed(filePath: string, fileName: string) {
const notificationTitle = t.__("Downloaded {{{fileName}}}.", {
fileName,
});
const notificationBody = t.__("Click to open downloads folder.");
// Show native notification
const downloadNotification = new Notification({
title: t.__("Download Complete"),
body: t.__("Click to show {{{fileName}}} in folder", {fileName}),
title: notificationTitle,
body: notificationBody,
silent: true, // We'll play our own sound - ding.ogg
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the original issue is a complaint about the native notifications not working on Windows. Do we understand how that's happening? I feel like papering over a bug like that without understanding it is risky -- have you tried arranging an interactive debugging session with someone experiencing the bug?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user didn't confirm whether they had desktop notifications enabled or not.
Screenshot 2025-11-06 at 2 31 18 PM

The original intention was to solve #desktop > several clicks to download a file, I can unlink the issue from the PR if we think it's a windows bug (which I highly doubt). Reply from the issue poster felt incoherent.

downloadNotification.on("click", () => {
// Reveal file in download folder
shell.showItemInFolder(filePath);
});
downloadNotification.show();
const {id: downloadId} = trackDownloadedFile(filePath);
// Event to show in-app notification in addition to the native
// notification.
send(
contents,
"show-download-success",
notificationTitle,
notificationBody,
downloadId,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to do both the old notification mechanism and the new one -- is that intendedd?

It is important for backwards-compatibility to make sure we do the old thing on existing web app versions. But I don't know if doing both would be problematic, and in any case, I would expect some sort of detection logic and comments explaining the reasoning behind what we're doing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is intended. My understanding of the requirements is that we want to show both in-app and native notifications. @alya let me know if that is not the case.

Added comments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I was imagining we'd have both, but I'm not sure if it's silly to do it that way. If you'd be up for it, it might be good to check how a few other apps handle it. Trying Discord, Slack, and WhatsApp, it seems like they pick one or the other, unless my desktop notifications aren't working properly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So possibly we'd suppress the native notification when showing the in-app one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't want to send both. There are some potential advantages to the native notification; It appears even if your desktop app is not focused -- say if you started downloading a long video and then tabbed away.

Is there a way to check if the native notification actually sent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to check if the native notification actually sent?

I checked this and there's not for all OS. Only windows emits a failed event: https://www.electronjs.org/docs/latest/api/notification#event-failed-windows.

Might be ok having both notifications in that case?


// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem("silent", false)) {
Expand All @@ -150,7 +195,7 @@ export default function handleExternalLink(
if (state !== "cancelled") {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
new Notification({
title: t.__("Download Complete"),
title: t.__("Download complete"),
body: t.__("Download failed"),
}).show();
} else {
Expand Down
13 changes: 11 additions & 2 deletions app/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {clipboard} from "electron/common";
import {clipboard, shell} from "electron/common";
import {
BrowserWindow,
type IpcMainEvent,
Expand All @@ -25,7 +25,9 @@ import type {MenuProperties} from "../common/types.ts";

import {appUpdater, shouldQuitForUpdate} from "./autoupdater.ts";
import * as BadgeSettings from "./badge-settings.ts";
import handleExternalLink from "./handle-external-link.ts";
import handleExternalLink, {
getDownloadedFilePath,
} from "./handle-external-link.ts";
import * as AppMenu from "./menu.ts";
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.ts";
import {sentryInit} from "./sentry.ts";
Expand Down Expand Up @@ -452,6 +454,13 @@ function createMainWindow(): BrowserWindow {
},
);

ipcMain.on("show-downloaded-file-in-folder", (_event, downloadId: string) => {
const filePath = getDownloadedFilePath(downloadId);
if (filePath !== undefined) {
shell.showItemInFolder(filePath);
}
});

ipcMain.on("save-last-tab", (_event, index: number) => {
ConfigUtil.setConfigItem("lastActiveTab", index);
});
Expand Down
7 changes: 7 additions & 0 deletions app/renderer/js/electron-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ bridgeEvents.addEventListener("realm_icon_url", (event) => {
);
});

bridgeEvents.addEventListener("show-downloaded-file-in-folder", (event) => {
const [downloadId] = z
.tuple([z.string()])
.parse(z.instanceof(BridgeEvent).parse(event).arguments_);
ipcRenderer.send("show-downloaded-file-in-folder", downloadId);
});

// Set user as active and update the time of last activity
ipcRenderer.on("set-active", () => {
idle = false;
Expand Down
13 changes: 13 additions & 0 deletions app/renderer/js/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ ipcRenderer.on("show-notification-settings", () => {
bridgeEvents.dispatchEvent(new BridgeEvent("show-notification-settings"));
});

ipcRenderer.on(
"show-download-success",
(_event, title: string, description: string, downloadId: string) => {
bridgeEvents.dispatchEvent(
new BridgeEvent("show-download-success", [
title,
description,
downloadId,
]),
);
},
);

window.addEventListener("load", () => {
if (!location.href.includes("app/renderer/network.html")) {
return;
Expand Down