Files
zulip-desktop/app/renderer/js/tray.ts
Anders Kaseorg 79f9362736 Strongly type IPC messages.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 20:44:05 -07:00

238 lines
5.9 KiB
TypeScript

import type {NativeImage, WebviewTag} from "electron";
import {remote} from "electron";
import path from "path";
import * as ConfigUtil from "../../common/config-util";
import type {RendererMessage} from "../../common/typed-ipc";
import {ipcRenderer} from "./typed-ipc-renderer";
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
let tray: Electron.Tray | null = null;
const ICON_DIR = "../../resources/tray";
const TRAY_SUFFIX = "tray";
const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX);
const iconPath = (): string => {
if (process.platform === "linux") {
return APP_ICON + "linux.png";
}
return (
APP_ICON + (process.platform === "win32" ? "win.ico" : "macOSTemplate.png")
);
};
const winUnreadTrayIconPath = (): string => APP_ICON + "unread.ico";
let unread = 0;
const trayIconSize = (): number => {
switch (process.platform) {
case "darwin":
return 20;
case "win32":
return 100;
case "linux":
return 100;
default:
return 80;
}
};
// Default config for Icon we might make it OS specific if needed like the size
const config = {
pixelRatio: window.devicePixelRatio,
unreadCount: 0,
showUnreadCount: true,
unreadColor: "#000000",
readColor: "#000000",
unreadBackgroundColor: "#B9FEEA",
readBackgroundColor: "#B9FEEA",
size: trayIconSize(),
thick: process.platform === "win32",
};
const renderCanvas = function (arg: number): HTMLCanvasElement {
config.unreadCount = arg;
const SIZE = config.size * config.pixelRatio;
const PADDING = SIZE * 0.05;
const CENTER = SIZE / 2;
const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const color = config.unreadCount ? config.unreadColor : config.readColor;
const backgroundColor = config.unreadCount
? config.unreadBackgroundColor
: config.readBackgroundColor;
const canvas = document.createElement("canvas");
canvas.width = SIZE;
canvas.height = SIZE;
const ctx = canvas.getContext("2d")!;
// Circle
// If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath();
ctx.arc(CENTER, CENTER, SIZE / 2 - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor;
ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor;
ctx.stroke();
// Count or Icon
if (HAS_COUNT) {
ctx.fillStyle = color;
ctx.textAlign = "center";
if (config.unreadCount > 99) {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.4}px Helvetica`;
ctx.fillText("99+", CENTER, CENTER + SIZE * 0.15);
} else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.2);
} else {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.15);
}
}
return canvas;
};
/**
* Renders the tray icon as a native image
* @param arg: Unread count
* @return the native image
*/
const renderNativeImage = function (arg: number): NativeImage {
if (process.platform === "win32") {
return nativeImage.createFromPath(winUnreadTrayIconPath());
}
const canvas = renderCanvas(arg);
const pngData = nativeImage
.createFromDataURL(canvas.toDataURL("image/png"))
.toPNG();
return nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio,
});
};
function sendAction<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void {
const win = BrowserWindow.getAllWindows()[0];
if (process.platform === "darwin") {
win.restore();
}
ipcRenderer.sendTo(win.webContents.id, channel, ...args);
}
const createTray = function (): void {
const contextMenu = Menu.buildFromTemplate([
{
label: "Zulip",
click() {
ipcRenderer.send("focus-app");
},
},
{
label: "Settings",
click() {
ipcRenderer.send("focus-app");
sendAction("open-settings");
},
},
{
type: "separator",
},
{
label: "Quit",
click() {
ipcRenderer.send("quit-app");
},
},
]);
tray = new Tray(iconPath());
tray.setContextMenu(contextMenu);
if (process.platform === "linux" || process.platform === "win32") {
tray.on("click", () => {
ipcRenderer.send("toggle-app");
});
}
};
ipcRenderer.on("destroytray", (_event: Event) => {
if (!tray) {
return;
}
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
throw new Error("Tray icon not properly destroyed.");
}
});
ipcRenderer.on("tray", (_event: Event, arg: number): void => {
if (!tray) {
return;
}
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === "linux" || process.platform === "win32") {
if (arg === 0) {
unread = arg;
tray.setImage(iconPath());
tray.setToolTip("No unread messages");
} else {
unread = arg;
const image = renderNativeImage(arg);
tray.setImage(image);
tray.setToolTip(`${arg} unread messages`);
}
}
});
function toggleTray(): void {
let state;
if (tray) {
state = false;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
}
ConfigUtil.setConfigItem("trayIcon", false);
} else {
state = true;
createTray();
if (process.platform === "linux" || process.platform === "win32") {
const image = renderNativeImage(unread);
tray!.setImage(image);
tray!.setToolTip(`${unread} unread messages`);
}
ConfigUtil.setConfigItem("trayIcon", true);
}
const selector = "webview:not([class*=disabled])";
const webview: WebviewTag = document.querySelector(selector)!;
ipcRenderer.sendTo(webview.getWebContentsId(), "toggle-tray", state);
}
ipcRenderer.on("toggletray", toggleTray);
if (ConfigUtil.getConfigItem("trayIcon", true)) {
createTray();
}
export {};