mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-11-02 21:13:32 +00:00
Move handleExternalLink to main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
@@ -3,11 +3,7 @@ import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
import {html} from "../../../common/html";
|
||||
|
||||
export function isUploadsUrl(server: string, url: URL): boolean {
|
||||
return url.origin === server && url.pathname.startsWith("/user_uploads/");
|
||||
}
|
||||
import {html} from "./html";
|
||||
|
||||
export async function openBrowser(url: URL): Promise<void> {
|
||||
if (["http:", "https:", "mailto:"].includes(url.protocol)) {
|
||||
@@ -4,7 +4,6 @@ import type {MenuProps, ServerConf} from "./types";
|
||||
export interface MainMessage {
|
||||
"clear-app-settings": () => void;
|
||||
"configure-spell-checker": () => void;
|
||||
downloadFile: (url: string, downloadPath: string) => void;
|
||||
"fetch-user-agent": () => string;
|
||||
"focus-app": () => void;
|
||||
"focus-this-webview": () => void;
|
||||
@@ -35,8 +34,6 @@ export interface RendererMessage {
|
||||
back: () => void;
|
||||
"copy-zulip-url": () => void;
|
||||
destroytray: () => void;
|
||||
downloadFileCompleted: (filePath: string, fileName: string) => void;
|
||||
downloadFileFailed: (state: string) => void;
|
||||
"enter-fullscreen": () => void;
|
||||
focus: () => void;
|
||||
"focus-webview-with-id": (webviewId: number) => void;
|
||||
@@ -55,6 +52,7 @@ export interface RendererMessage {
|
||||
options: {webContentsId: number | null; origin: string; permission: string},
|
||||
rendererCallbackId: number,
|
||||
) => void;
|
||||
"play-ding-sound": () => void;
|
||||
"reload-current-viewer": () => void;
|
||||
"reload-proxy": (showAlert: boolean) => void;
|
||||
"reload-viewer": () => void;
|
||||
|
||||
157
app/main/handle-external-link.ts
Normal file
157
app/main/handle-external-link.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {shell} from "electron/common";
|
||||
import type {
|
||||
HandlerDetails,
|
||||
SaveDialogOptions,
|
||||
WebContents,
|
||||
} from "electron/main";
|
||||
import {Notification, app} from "electron/main";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util";
|
||||
import * as LinkUtil from "../common/link-util";
|
||||
|
||||
import {send} from "./typed-ipc-main";
|
||||
|
||||
function isUploadsUrl(server: string, url: URL): boolean {
|
||||
return url.origin === server && url.pathname.startsWith("/user_uploads/");
|
||||
}
|
||||
|
||||
function downloadFile({
|
||||
contents,
|
||||
url,
|
||||
downloadPath,
|
||||
completed,
|
||||
failed,
|
||||
}: {
|
||||
contents: WebContents;
|
||||
url: string;
|
||||
downloadPath: string;
|
||||
completed(filePath: string, fileName: string): Promise<void>;
|
||||
failed(state: string): void;
|
||||
}) {
|
||||
contents.downloadURL(url);
|
||||
contents.session.once("will-download", async (_event: Event, item) => {
|
||||
if (ConfigUtil.getConfigItem("promptDownload", false)) {
|
||||
const showDialogOptions: SaveDialogOptions = {
|
||||
defaultPath: path.join(downloadPath, item.getFilename()),
|
||||
};
|
||||
item.setSaveDialogOptions(showDialogOptions);
|
||||
} else {
|
||||
const getTimeStamp = (): number => {
|
||||
const date = new Date();
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const formatFile = (filePath: string): string => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, fileExtension);
|
||||
return `${baseName}-${getTimeStamp()}${fileExtension}`;
|
||||
};
|
||||
|
||||
const filePath = path.join(downloadPath, item.getFilename());
|
||||
|
||||
// Update the name and path of the file if it already exists
|
||||
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
|
||||
const setFilePath: string = fs.existsSync(filePath)
|
||||
? updatedFilePath
|
||||
: filePath;
|
||||
item.setSavePath(setFilePath);
|
||||
}
|
||||
|
||||
const updatedListener = (_event: Event, state: string): void => {
|
||||
switch (state) {
|
||||
case "interrupted": {
|
||||
// Can interrupted to due to network error, cancel download then
|
||||
console.log(
|
||||
"Download interrupted, cancelling and fallback to dialog download.",
|
||||
);
|
||||
item.cancel();
|
||||
break;
|
||||
}
|
||||
|
||||
case "progressing": {
|
||||
if (item.isPaused()) {
|
||||
item.cancel();
|
||||
}
|
||||
|
||||
// This event can also be used to show progress in percentage in future.
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.info("Unknown updated state of download item");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
item.on("updated", updatedListener);
|
||||
item.once("done", async (_event: Event, state) => {
|
||||
if (state === "completed") {
|
||||
await completed(item.getSavePath(), path.basename(item.getSavePath()));
|
||||
} else {
|
||||
console.log("Download failed state:", state);
|
||||
failed(state);
|
||||
}
|
||||
|
||||
// To stop item for listening to updated events of this file
|
||||
item.removeListener("updated", updatedListener);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default function handleExternalLink(
|
||||
contents: WebContents,
|
||||
details: HandlerDetails,
|
||||
mainContents: WebContents,
|
||||
): void {
|
||||
const url = new URL(details.url);
|
||||
const downloadPath = ConfigUtil.getConfigItem(
|
||||
"downloadsPath",
|
||||
`${app.getPath("downloads")}`,
|
||||
);
|
||||
|
||||
if (isUploadsUrl(new URL(contents.getURL()).origin, url)) {
|
||||
downloadFile({
|
||||
contents,
|
||||
url: url.href,
|
||||
downloadPath,
|
||||
async completed(filePath: string, fileName: string) {
|
||||
const downloadNotification = new Notification({
|
||||
title: "Download Complete",
|
||||
body: `Click to show ${fileName} in folder`,
|
||||
silent: true, // We'll play our own sound - ding.ogg
|
||||
});
|
||||
downloadNotification.on("click", () => {
|
||||
// Reveal file in download folder
|
||||
shell.showItemInFolder(filePath);
|
||||
});
|
||||
downloadNotification.show();
|
||||
|
||||
// Play sound to indicate download complete
|
||||
if (!ConfigUtil.getConfigItem("silent", false)) {
|
||||
send(mainContents, "play-ding-sound");
|
||||
}
|
||||
},
|
||||
failed(state: string) {
|
||||
// Automatic download failed, so show save dialog prompt and download
|
||||
// through webview
|
||||
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
|
||||
// prompts right after each other)
|
||||
// Check that the download is not cancelled by user
|
||||
if (state !== "cancelled") {
|
||||
if (ConfigUtil.getConfigItem("promptDownload", false)) {
|
||||
new Notification({
|
||||
title: "Download Complete",
|
||||
body: "Download failed",
|
||||
}).show();
|
||||
} else {
|
||||
contents.downloadURL(url.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
(async () => LinkUtil.openBrowser(url))();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {IpcMainEvent, SaveDialogOptions, WebContents} from "electron/main";
|
||||
import type {IpcMainEvent, WebContents} from "electron/main";
|
||||
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import * as remoteMain from "@electron/remote/main";
|
||||
@@ -12,6 +11,7 @@ import type {MenuProps} from "../common/types";
|
||||
|
||||
import {appUpdater} from "./autoupdater";
|
||||
import * as BadgeSettings from "./badge-settings";
|
||||
import handleExternalLink from "./handle-external-link";
|
||||
import * as AppMenu from "./menu";
|
||||
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request";
|
||||
import {sentryInit} from "./sentry";
|
||||
@@ -172,6 +172,13 @@ function createMainWindow(): BrowserWindow {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
app.on("web-contents-created", (_event: Event, contents: WebContents) => {
|
||||
contents.setWindowOpenHandler((details) => {
|
||||
handleExternalLink(contents, details, page);
|
||||
return {action: "deny"};
|
||||
});
|
||||
});
|
||||
|
||||
const ses = session.fromPartition("persist:webviewsession");
|
||||
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
|
||||
|
||||
@@ -354,85 +361,6 @@ ${error}`,
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
"downloadFile",
|
||||
(_event: IpcMainEvent, url: string, downloadPath: string) => {
|
||||
page.downloadURL(url);
|
||||
page.session.once("will-download", async (_event: Event, item) => {
|
||||
if (ConfigUtil.getConfigItem("promptDownload", false)) {
|
||||
const showDialogOptions: SaveDialogOptions = {
|
||||
defaultPath: path.join(downloadPath, item.getFilename()),
|
||||
};
|
||||
item.setSaveDialogOptions(showDialogOptions);
|
||||
} else {
|
||||
const getTimeStamp = (): number => {
|
||||
const date = new Date();
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const formatFile = (filePath: string): string => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, fileExtension);
|
||||
return `${baseName}-${getTimeStamp()}${fileExtension}`;
|
||||
};
|
||||
|
||||
const filePath = path.join(downloadPath, item.getFilename());
|
||||
|
||||
// Update the name and path of the file if it already exists
|
||||
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
|
||||
const setFilePath: string = fs.existsSync(filePath)
|
||||
? updatedFilePath
|
||||
: filePath;
|
||||
item.setSavePath(setFilePath);
|
||||
}
|
||||
|
||||
const updatedListener = (_event: Event, state: string): void => {
|
||||
switch (state) {
|
||||
case "interrupted": {
|
||||
// Can interrupted to due to network error, cancel download then
|
||||
console.log(
|
||||
"Download interrupted, cancelling and fallback to dialog download.",
|
||||
);
|
||||
item.cancel();
|
||||
break;
|
||||
}
|
||||
|
||||
case "progressing": {
|
||||
if (item.isPaused()) {
|
||||
item.cancel();
|
||||
}
|
||||
|
||||
// This event can also be used to show progress in percentage in future.
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.info("Unknown updated state of download item");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
item.on("updated", updatedListener);
|
||||
item.once("done", (_event: Event, state) => {
|
||||
if (state === "completed") {
|
||||
send(
|
||||
page,
|
||||
"downloadFileCompleted",
|
||||
item.getSavePath(),
|
||||
path.basename(item.getSavePath()),
|
||||
);
|
||||
} else {
|
||||
console.log("Download failed state:", state);
|
||||
send(page, "downloadFileFailed", state);
|
||||
}
|
||||
|
||||
// To stop item for listening to updated events of this file
|
||||
item.removeListener("updated", updatedListener);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
"realm-name-changed",
|
||||
(_event: IpcMainEvent, serverURL: string, realmName: string) => {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import {shell} from "electron/common";
|
||||
import type {HandlerDetails} from "electron/renderer";
|
||||
|
||||
import {app} from "@electron/remote";
|
||||
|
||||
import * as ConfigUtil from "../../../common/config-util";
|
||||
import {ipcRenderer} from "../typed-ipc-renderer";
|
||||
import * as LinkUtil from "../utils/link-util";
|
||||
|
||||
import type WebView from "./webview";
|
||||
|
||||
const dingSound = new Audio("../resources/sounds/ding.ogg");
|
||||
|
||||
export default function handleExternalLink(
|
||||
this: WebView,
|
||||
details: HandlerDetails,
|
||||
): void {
|
||||
const url = new URL(details.url);
|
||||
const downloadPath = ConfigUtil.getConfigItem(
|
||||
"downloadsPath",
|
||||
`${app.getPath("downloads")}`,
|
||||
);
|
||||
|
||||
if (LinkUtil.isUploadsUrl(this.props.url, url)) {
|
||||
ipcRenderer.send("downloadFile", url.href, downloadPath);
|
||||
ipcRenderer.once(
|
||||
"downloadFileCompleted",
|
||||
async (_event: Event, filePath: string, fileName: string) => {
|
||||
const downloadNotification = new Notification("Download Complete", {
|
||||
body: `Click to show ${fileName} in folder`,
|
||||
silent: true, // We'll play our own sound - ding.ogg
|
||||
});
|
||||
|
||||
downloadNotification.addEventListener("click", () => {
|
||||
// Reveal file in download folder
|
||||
shell.showItemInFolder(filePath);
|
||||
});
|
||||
ipcRenderer.removeAllListeners("downloadFileFailed");
|
||||
|
||||
// Play sound to indicate download complete
|
||||
if (!ConfigUtil.getConfigItem("silent", false)) {
|
||||
await dingSound.play();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ipcRenderer.once("downloadFileFailed", (_event: Event, state: string) => {
|
||||
// Automatic download failed, so show save dialog prompt and download
|
||||
// through webview
|
||||
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
|
||||
// prompts right after each other)
|
||||
// Check that the download is not cancelled by user
|
||||
if (state !== "cancelled") {
|
||||
if (ConfigUtil.getConfigItem("promptDownload", false)) {
|
||||
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
|
||||
// doesn't work
|
||||
// eslint-disable-next-line no-new
|
||||
new Notification("Download Complete", {
|
||||
body: "Download failed",
|
||||
});
|
||||
} else {
|
||||
this.getWebContents().downloadURL(url.href);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.removeAllListeners("downloadFileCompleted");
|
||||
});
|
||||
} else {
|
||||
(async () => LinkUtil.openBrowser(url))();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import * as SystemUtil from "../utils/system-util";
|
||||
|
||||
import {generateNodeFromHtml} from "./base";
|
||||
import {contextMenu} from "./context-menu";
|
||||
import handleExternalLink from "./handle-external-link";
|
||||
|
||||
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
|
||||
|
||||
@@ -127,11 +126,6 @@ export default class WebView {
|
||||
registerListeners(): void {
|
||||
const webContents = this.getWebContents();
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
handleExternalLink.call(this, details);
|
||||
return {action: "deny"};
|
||||
});
|
||||
|
||||
if (shouldSilentWebview) {
|
||||
webContents.setAudioMuted(true);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as ConfigUtil from "../../common/config-util";
|
||||
import * as DNDUtil from "../../common/dnd-util";
|
||||
import type {DndSettings} from "../../common/dnd-util";
|
||||
import * as EnterpriseUtil from "../../common/enterprise-util";
|
||||
import * as LinkUtil from "../../common/link-util";
|
||||
import Logger from "../../common/logger-util";
|
||||
import * as Messages from "../../common/messages";
|
||||
import type {NavItem, ServerConf, TabData} from "../../common/types";
|
||||
@@ -22,7 +23,6 @@ import {PreferenceView} from "./pages/preference/preference";
|
||||
import {initializeTray} from "./tray";
|
||||
import {ipcRenderer} from "./typed-ipc-renderer";
|
||||
import * as DomainUtil from "./utils/domain-util";
|
||||
import * as LinkUtil from "./utils/link-util";
|
||||
import ReconnectUtil from "./utils/reconnect-util";
|
||||
|
||||
Sentry.init({});
|
||||
@@ -48,6 +48,8 @@ type ServerOrFunctionalTab = ServerTab | FunctionalTab;
|
||||
|
||||
const rootWebContents = remote.getCurrentWebContents();
|
||||
|
||||
const dingSound = new Audio("../resources/sounds/ding.ogg");
|
||||
|
||||
export class ServerManagerView {
|
||||
$addServerButton: HTMLButtonElement;
|
||||
$tabsContainer: Element;
|
||||
@@ -1177,6 +1179,10 @@ export class ServerManagerView {
|
||||
ipcRenderer.on("open-network-settings", async () => {
|
||||
await this.openSettings("Network");
|
||||
});
|
||||
|
||||
ipcRenderer.on("play-ding-sound", async () => {
|
||||
await dingSound.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {html} from "../../../../common/html";
|
||||
import * as LinkUtil from "../../../../common/link-util";
|
||||
import * as t from "../../../../common/translation-util";
|
||||
import {generateNodeFromHtml} from "../../components/base";
|
||||
import * as LinkUtil from "../../utils/link-util";
|
||||
|
||||
interface FindAccountsProps {
|
||||
$root: Element;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {dialog} from "@electron/remote";
|
||||
|
||||
import {html} from "../../../../common/html";
|
||||
import * as LinkUtil from "../../../../common/link-util";
|
||||
import * as t from "../../../../common/translation-util";
|
||||
import {generateNodeFromHtml} from "../../components/base";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer";
|
||||
import * as DomainUtil from "../../utils/domain-util";
|
||||
import * as LinkUtil from "../../utils/link-util";
|
||||
|
||||
interface NewServerFormProps {
|
||||
$root: Element;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {html} from "../../../../common/html";
|
||||
import * as LinkUtil from "../../../../common/link-util";
|
||||
import * as t from "../../../../common/translation-util";
|
||||
import * as LinkUtil from "../../utils/link-util";
|
||||
|
||||
interface ShortcutsSectionProps {
|
||||
$root: Element;
|
||||
|
||||
Reference in New Issue
Block a user