Skip to content

Commit 5edffbd

Browse files
committed
Move handleExternalLink to main process.
Signed-off-by: Anders Kaseorg <[email protected]>
1 parent 27576c9 commit 5edffbd

File tree

10 files changed

+178
-170
lines changed

10 files changed

+178
-170
lines changed

app/renderer/js/utils/link-util.ts renamed to app/common/link-util.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ import fs from "fs";
33
import os from "os";
44
import path from "path";
55

6-
import {html} from "../../../common/html";
7-
8-
export function isUploadsUrl(server: string, url: URL): boolean {
9-
return url.origin === server && url.pathname.startsWith("/user_uploads/");
10-
}
6+
import {html} from "./html";
117

128
export async function openBrowser(url: URL): Promise<void> {
139
if (["http:", "https:", "mailto:"].includes(url.protocol)) {

app/common/typed-ipc.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {MenuProps, ServerConf} from "./types";
44
export interface MainMessage {
55
"clear-app-settings": () => void;
66
"configure-spell-checker": () => void;
7-
downloadFile: (url: string, downloadPath: string) => void;
87
"fetch-user-agent": () => string;
98
"focus-app": () => void;
109
"focus-this-webview": () => void;
@@ -35,8 +34,6 @@ export interface RendererMessage {
3534
back: () => void;
3635
"copy-zulip-url": () => void;
3736
destroytray: () => void;
38-
downloadFileCompleted: (filePath: string, fileName: string) => void;
39-
downloadFileFailed: (state: string) => void;
4037
"enter-fullscreen": () => void;
4138
focus: () => void;
4239
"focus-webview-with-id": (webviewId: number) => void;
@@ -55,6 +52,7 @@ export interface RendererMessage {
5552
options: {webContentsId: number | null; origin: string; permission: string},
5653
rendererCallbackId: number,
5754
) => void;
55+
"play-ding-sound": () => void;
5856
"reload-current-viewer": () => void;
5957
"reload-proxy": (showAlert: boolean) => void;
6058
"reload-viewer": () => void;

app/main/handle-external-link.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {shell} from "electron/common";
2+
import type {
3+
HandlerDetails,
4+
SaveDialogOptions,
5+
WebContents,
6+
} from "electron/main";
7+
import {Notification, app} from "electron/main";
8+
import fs from "fs";
9+
import path from "path";
10+
11+
import * as ConfigUtil from "../common/config-util";
12+
import * as LinkUtil from "../common/link-util";
13+
14+
import {send} from "./typed-ipc-main";
15+
16+
function isUploadsUrl(server: string, url: URL): boolean {
17+
return url.origin === server && url.pathname.startsWith("/user_uploads/");
18+
}
19+
20+
function downloadFile({
21+
contents,
22+
url,
23+
downloadPath,
24+
completed,
25+
failed,
26+
}: {
27+
contents: WebContents;
28+
url: string;
29+
downloadPath: string;
30+
completed(filePath: string, fileName: string): Promise<void>;
31+
failed(state: string): void;
32+
}) {
33+
contents.downloadURL(url);
34+
contents.session.once("will-download", async (_event: Event, item) => {
35+
if (ConfigUtil.getConfigItem("promptDownload", false)) {
36+
const showDialogOptions: SaveDialogOptions = {
37+
defaultPath: path.join(downloadPath, item.getFilename()),
38+
};
39+
item.setSaveDialogOptions(showDialogOptions);
40+
} else {
41+
const getTimeStamp = (): number => {
42+
const date = new Date();
43+
return date.getTime();
44+
};
45+
46+
const formatFile = (filePath: string): string => {
47+
const fileExtension = path.extname(filePath);
48+
const baseName = path.basename(filePath, fileExtension);
49+
return `${baseName}-${getTimeStamp()}${fileExtension}`;
50+
};
51+
52+
const filePath = path.join(downloadPath, item.getFilename());
53+
54+
// Update the name and path of the file if it already exists
55+
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
56+
const setFilePath: string = fs.existsSync(filePath)
57+
? updatedFilePath
58+
: filePath;
59+
item.setSavePath(setFilePath);
60+
}
61+
62+
const updatedListener = (_event: Event, state: string): void => {
63+
switch (state) {
64+
case "interrupted": {
65+
// Can interrupted to due to network error, cancel download then
66+
console.log(
67+
"Download interrupted, cancelling and fallback to dialog download.",
68+
);
69+
item.cancel();
70+
break;
71+
}
72+
73+
case "progressing": {
74+
if (item.isPaused()) {
75+
item.cancel();
76+
}
77+
78+
// This event can also be used to show progress in percentage in future.
79+
break;
80+
}
81+
82+
default: {
83+
console.info("Unknown updated state of download item");
84+
}
85+
}
86+
};
87+
88+
item.on("updated", updatedListener);
89+
item.once("done", async (_event: Event, state) => {
90+
if (state === "completed") {
91+
await completed(item.getSavePath(), path.basename(item.getSavePath()));
92+
} else {
93+
console.log("Download failed state:", state);
94+
failed(state);
95+
}
96+
97+
// To stop item for listening to updated events of this file
98+
item.removeListener("updated", updatedListener);
99+
});
100+
});
101+
}
102+
103+
export default function handleExternalLink(
104+
contents: WebContents,
105+
details: HandlerDetails,
106+
mainContents: WebContents,
107+
): void {
108+
const url = new URL(details.url);
109+
const downloadPath = ConfigUtil.getConfigItem(
110+
"downloadsPath",
111+
`${app.getPath("downloads")}`,
112+
);
113+
114+
if (isUploadsUrl(new URL(contents.getURL()).origin, url)) {
115+
downloadFile({
116+
contents,
117+
url: url.href,
118+
downloadPath,
119+
async completed(filePath: string, fileName: string) {
120+
const downloadNotification = new Notification({
121+
title: "Download Complete",
122+
body: `Click to show ${fileName} in folder`,
123+
silent: true, // We'll play our own sound - ding.ogg
124+
});
125+
downloadNotification.on("click", () => {
126+
// Reveal file in download folder
127+
shell.showItemInFolder(filePath);
128+
});
129+
downloadNotification.show();
130+
131+
// Play sound to indicate download complete
132+
if (!ConfigUtil.getConfigItem("silent", false)) {
133+
send(mainContents, "play-ding-sound");
134+
}
135+
},
136+
failed(state: string) {
137+
// Automatic download failed, so show save dialog prompt and download
138+
// through webview
139+
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
140+
// prompts right after each other)
141+
// Check that the download is not cancelled by user
142+
if (state !== "cancelled") {
143+
if (ConfigUtil.getConfigItem("promptDownload", false)) {
144+
new Notification({
145+
title: "Download Complete",
146+
body: "Download failed",
147+
}).show();
148+
} else {
149+
contents.downloadURL(url.href);
150+
}
151+
}
152+
},
153+
});
154+
} else {
155+
(async () => LinkUtil.openBrowser(url))();
156+
}
157+
}

app/main/index.ts

Lines changed: 9 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type {IpcMainEvent, SaveDialogOptions, WebContents} from "electron/main";
1+
import type {IpcMainEvent, WebContents} from "electron/main";
22
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
3-
import fs from "fs";
43
import path from "path";
54

65
import * as remoteMain from "@electron/remote/main";
@@ -12,6 +11,7 @@ import type {MenuProps} from "../common/types";
1211

1312
import {appUpdater} from "./autoupdater";
1413
import * as BadgeSettings from "./badge-settings";
14+
import handleExternalLink from "./handle-external-link";
1515
import * as AppMenu from "./menu";
1616
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request";
1717
import {sentryInit} from "./sentry";
@@ -172,6 +172,13 @@ function createMainWindow(): BrowserWindow {
172172
mainWindow.show();
173173
});
174174

175+
app.on("web-contents-created", (_event: Event, contents: WebContents) => {
176+
contents.setWindowOpenHandler((details) => {
177+
handleExternalLink(contents, details, page);
178+
return {action: "deny"};
179+
});
180+
});
181+
175182
const ses = session.fromPartition("persist:webviewsession");
176183
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
177184

@@ -354,85 +361,6 @@ ${error}`,
354361
},
355362
);
356363

357-
ipcMain.on(
358-
"downloadFile",
359-
(_event: IpcMainEvent, url: string, downloadPath: string) => {
360-
page.downloadURL(url);
361-
page.session.once("will-download", async (_event: Event, item) => {
362-
if (ConfigUtil.getConfigItem("promptDownload", false)) {
363-
const showDialogOptions: SaveDialogOptions = {
364-
defaultPath: path.join(downloadPath, item.getFilename()),
365-
};
366-
item.setSaveDialogOptions(showDialogOptions);
367-
} else {
368-
const getTimeStamp = (): number => {
369-
const date = new Date();
370-
return date.getTime();
371-
};
372-
373-
const formatFile = (filePath: string): string => {
374-
const fileExtension = path.extname(filePath);
375-
const baseName = path.basename(filePath, fileExtension);
376-
return `${baseName}-${getTimeStamp()}${fileExtension}`;
377-
};
378-
379-
const filePath = path.join(downloadPath, item.getFilename());
380-
381-
// Update the name and path of the file if it already exists
382-
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
383-
const setFilePath: string = fs.existsSync(filePath)
384-
? updatedFilePath
385-
: filePath;
386-
item.setSavePath(setFilePath);
387-
}
388-
389-
const updatedListener = (_event: Event, state: string): void => {
390-
switch (state) {
391-
case "interrupted": {
392-
// Can interrupted to due to network error, cancel download then
393-
console.log(
394-
"Download interrupted, cancelling and fallback to dialog download.",
395-
);
396-
item.cancel();
397-
break;
398-
}
399-
400-
case "progressing": {
401-
if (item.isPaused()) {
402-
item.cancel();
403-
}
404-
405-
// This event can also be used to show progress in percentage in future.
406-
break;
407-
}
408-
409-
default: {
410-
console.info("Unknown updated state of download item");
411-
}
412-
}
413-
};
414-
415-
item.on("updated", updatedListener);
416-
item.once("done", (_event: Event, state) => {
417-
if (state === "completed") {
418-
send(
419-
page,
420-
"downloadFileCompleted",
421-
item.getSavePath(),
422-
path.basename(item.getSavePath()),
423-
);
424-
} else {
425-
console.log("Download failed state:", state);
426-
send(page, "downloadFileFailed", state);
427-
}
428-
429-
// To stop item for listening to updated events of this file
430-
item.removeListener("updated", updatedListener);
431-
});
432-
});
433-
},
434-
);
435-
436364
ipcMain.on(
437365
"realm-name-changed",
438366
(_event: IpcMainEvent, serverURL: string, realmName: string) => {

app/renderer/js/components/handle-external-link.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)