mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 16:13:37 +00:00
310 lines
8.6 KiB
TypeScript
310 lines
8.6 KiB
TypeScript
import type {WebContents} from "electron/main";
|
|
import fs from "node:fs";
|
|
import process from "node:process";
|
|
|
|
import * as remote from "@electron/remote";
|
|
import {app, dialog} from "@electron/remote";
|
|
|
|
import * as ConfigUtil from "../../../common/config-util.js";
|
|
import type {Html} from "../../../common/html.js";
|
|
import {html} from "../../../common/html.js";
|
|
import type {RendererMessage} from "../../../common/typed-ipc.js";
|
|
import type {TabRole} from "../../../common/types.js";
|
|
import preloadCss from "../../css/preload.css?raw"; // eslint-disable-line n/file-extension-in-import
|
|
import {ipcRenderer} from "../typed-ipc-renderer.js";
|
|
import * as SystemUtil from "../utils/system-util.js";
|
|
|
|
import {generateNodeFromHtml} from "./base.js";
|
|
import {contextMenu} from "./context-menu.js";
|
|
|
|
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
|
|
|
|
type WebViewProps = {
|
|
$root: Element;
|
|
rootWebContents: WebContents;
|
|
index: number;
|
|
tabIndex: number;
|
|
url: string;
|
|
role: TabRole;
|
|
isActive: () => boolean;
|
|
switchLoading: (loading: boolean, url: string) => void;
|
|
onNetworkError: (index: number) => void;
|
|
preload?: string;
|
|
onTitleChange: () => void;
|
|
hasPermission?: (origin: string, permission: string) => boolean;
|
|
};
|
|
|
|
export default class WebView {
|
|
static templateHtml(props: WebViewProps): Html {
|
|
return html`
|
|
<webview
|
|
data-tab-id="${props.tabIndex}"
|
|
src="${props.url}"
|
|
${props.preload === undefined
|
|
? html``
|
|
: html`preload="${props.preload}" webpreferences="sandbox=no"`}
|
|
partition="persist:webviewsession"
|
|
allowpopups
|
|
>
|
|
</webview>
|
|
`;
|
|
}
|
|
|
|
static async create(props: WebViewProps): Promise<WebView> {
|
|
const $element = generateNodeFromHtml(
|
|
WebView.templateHtml(props),
|
|
) as HTMLElement;
|
|
props.$root.append($element);
|
|
|
|
// Wait for did-navigate rather than did-attach to work around
|
|
// https://github.com/electron/electron/issues/31918
|
|
await new Promise<void>((resolve) => {
|
|
$element.addEventListener(
|
|
"did-navigate",
|
|
() => {
|
|
resolve();
|
|
},
|
|
true,
|
|
);
|
|
});
|
|
|
|
// Work around https://github.com/electron/electron/issues/26904
|
|
function getWebContentsIdFunction(
|
|
this: undefined,
|
|
selector: string,
|
|
): number {
|
|
return document
|
|
.querySelector<Electron.WebviewTag>(selector)!
|
|
.getWebContentsId();
|
|
}
|
|
|
|
const selector = `webview[data-tab-id="${CSS.escape(
|
|
`${props.tabIndex}`,
|
|
)}"]`;
|
|
const webContentsId: unknown =
|
|
await props.rootWebContents.executeJavaScript(
|
|
`(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`,
|
|
);
|
|
if (typeof webContentsId !== "number") {
|
|
throw new TypeError("Failed to get WebContents ID");
|
|
}
|
|
|
|
return new WebView(props, $element, webContentsId);
|
|
}
|
|
|
|
zoomFactor: number;
|
|
badgeCount: number;
|
|
loading: boolean;
|
|
customCss: string | false | null;
|
|
$webviewsContainer: DOMTokenList;
|
|
$el: HTMLElement;
|
|
webContentsId: number;
|
|
|
|
private constructor(
|
|
readonly props: WebViewProps,
|
|
$element: HTMLElement,
|
|
webContentsId: number,
|
|
) {
|
|
this.zoomFactor = 1;
|
|
this.loading = true;
|
|
this.badgeCount = 0;
|
|
this.customCss = ConfigUtil.getConfigItem("customCSS", null);
|
|
this.$webviewsContainer = document.querySelector(
|
|
"#webviews-container",
|
|
)!.classList;
|
|
this.$el = $element;
|
|
this.webContentsId = webContentsId;
|
|
|
|
this.registerListeners();
|
|
}
|
|
|
|
getWebContents(): WebContents {
|
|
return remote.webContents.fromId(this.webContentsId);
|
|
}
|
|
|
|
registerListeners(): void {
|
|
const webContents = this.getWebContents();
|
|
|
|
if (shouldSilentWebview) {
|
|
webContents.setAudioMuted(true);
|
|
}
|
|
|
|
webContents.on("page-title-updated", (_event, title) => {
|
|
this.badgeCount = this.getBadgeCount(title);
|
|
this.props.onTitleChange();
|
|
});
|
|
|
|
this.$el.addEventListener("did-navigate-in-page", () => {
|
|
this.canGoBackButton();
|
|
});
|
|
|
|
this.$el.addEventListener("did-navigate", () => {
|
|
this.canGoBackButton();
|
|
});
|
|
|
|
webContents.on("page-favicon-updated", (_event, favicons) => {
|
|
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
|
|
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
|
|
if (
|
|
favicons[0].indexOf("favicon-pms") > 0 &&
|
|
process.platform === "darwin"
|
|
) {
|
|
// This api is only supported on macOS
|
|
app.dock.setBadge("●");
|
|
// Bounce the dock
|
|
if (ConfigUtil.getConfigItem("dockBouncing", true)) {
|
|
app.dock.bounce();
|
|
}
|
|
}
|
|
});
|
|
|
|
webContents.addListener("context-menu", (event, menuParameters) => {
|
|
contextMenu(webContents, event, menuParameters);
|
|
});
|
|
|
|
this.$el.addEventListener("dom-ready", () => {
|
|
this.loading = false;
|
|
this.props.switchLoading(false, this.props.url);
|
|
this.show();
|
|
});
|
|
|
|
webContents.on("did-fail-load", (_event, _errorCode, errorDescription) => {
|
|
const hasConnectivityError =
|
|
SystemUtil.connectivityError.includes(errorDescription);
|
|
if (hasConnectivityError) {
|
|
console.error("error", errorDescription);
|
|
if (!this.props.url.includes("network.html")) {
|
|
this.props.onNetworkError(this.props.index);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.$el.addEventListener("did-start-loading", () => {
|
|
this.props.switchLoading(true, this.props.url);
|
|
});
|
|
|
|
this.$el.addEventListener("did-stop-loading", () => {
|
|
this.props.switchLoading(false, this.props.url);
|
|
});
|
|
}
|
|
|
|
getBadgeCount(title: string): number {
|
|
const messageCountInTitle = /^\((\d+)\)/.exec(title);
|
|
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
|
|
}
|
|
|
|
showNotificationSettings(): void {
|
|
this.send("show-notification-settings");
|
|
}
|
|
|
|
show(): void {
|
|
// Do not show WebView if another tab was selected and this tab should be in background.
|
|
if (!this.props.isActive()) {
|
|
return;
|
|
}
|
|
|
|
// To show or hide the loading indicator in the the active tab
|
|
this.$webviewsContainer.toggle("loaded", !this.loading);
|
|
|
|
this.$el.classList.add("active");
|
|
this.focus();
|
|
this.props.onTitleChange();
|
|
// Injecting preload css in webview to override some css rules
|
|
(async () => this.getWebContents().insertCSS(preloadCss))();
|
|
|
|
// Get customCSS again from config util to avoid warning user again
|
|
const customCss = ConfigUtil.getConfigItem("customCSS", null);
|
|
this.customCss = customCss;
|
|
if (customCss) {
|
|
if (!fs.existsSync(customCss)) {
|
|
this.customCss = null;
|
|
ConfigUtil.setConfigItem("customCSS", null);
|
|
|
|
const errorMessage = "The custom css previously set is deleted!";
|
|
dialog.showErrorBox("custom css file deleted!", errorMessage);
|
|
return;
|
|
}
|
|
|
|
(async () =>
|
|
this.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))();
|
|
}
|
|
}
|
|
|
|
focus(): void {
|
|
this.$el.focus();
|
|
// Work around https://github.com/electron/electron/issues/31918
|
|
this.$el.shadowRoot?.querySelector("iframe")?.focus();
|
|
}
|
|
|
|
hide(): void {
|
|
this.$el.classList.remove("active");
|
|
}
|
|
|
|
load(): void {
|
|
this.show();
|
|
}
|
|
|
|
zoomIn(): void {
|
|
this.zoomFactor += 0.1;
|
|
this.getWebContents().setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
zoomOut(): void {
|
|
this.zoomFactor -= 0.1;
|
|
this.getWebContents().setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
zoomActualSize(): void {
|
|
this.zoomFactor = 1;
|
|
this.getWebContents().setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
logOut(): void {
|
|
this.send("logout");
|
|
}
|
|
|
|
showKeyboardShortcuts(): void {
|
|
this.send("show-keyboard-shortcuts");
|
|
}
|
|
|
|
openDevTools(): void {
|
|
this.getWebContents().openDevTools();
|
|
}
|
|
|
|
back(): void {
|
|
if (this.getWebContents().canGoBack()) {
|
|
this.getWebContents().goBack();
|
|
this.focus();
|
|
}
|
|
}
|
|
|
|
canGoBackButton(): void {
|
|
const $backButton = document.querySelector(
|
|
"#actions-container #back-action",
|
|
)!;
|
|
$backButton.classList.toggle("disable", !this.getWebContents().canGoBack());
|
|
}
|
|
|
|
forward(): void {
|
|
if (this.getWebContents().canGoForward()) {
|
|
this.getWebContents().goForward();
|
|
}
|
|
}
|
|
|
|
reload(): void {
|
|
this.hide();
|
|
// Shows the loading indicator till the webview is reloaded
|
|
this.$webviewsContainer.remove("loaded");
|
|
this.loading = true;
|
|
this.props.switchLoading(true, this.props.url);
|
|
this.getWebContents().reload();
|
|
}
|
|
|
|
send<Channel extends keyof RendererMessage>(
|
|
channel: Channel,
|
|
...args: Parameters<RendererMessage[Channel]>
|
|
): void {
|
|
ipcRenderer.sendTo(this.webContentsId, channel, ...args);
|
|
}
|
|
}
|