mirror of
				https://github.com/zulip/zulip-desktop.git
				synced 2025-11-04 05:53:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			238 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			238 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import type {WebviewTag, NativeImage} from "electron";
 | 
						|
import {ipcRenderer, remote} from "electron";
 | 
						|
import path from "path";
 | 
						|
 | 
						|
import * as ConfigUtil from "../../common/config-util";
 | 
						|
 | 
						|
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
 | 
						|
 | 
						|
let tray: Electron.Tray;
 | 
						|
 | 
						|
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(action: string): void {
 | 
						|
  const win = BrowserWindow.getAllWindows()[0];
 | 
						|
 | 
						|
  if (process.platform === "darwin") {
 | 
						|
    win.restore();
 | 
						|
  }
 | 
						|
 | 
						|
  win.webContents.send(action);
 | 
						|
}
 | 
						|
 | 
						|
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): Event => {
 | 
						|
    if (!tray) {
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    tray.destroy();
 | 
						|
    if (tray.isDestroyed()) {
 | 
						|
      tray = null;
 | 
						|
    } else {
 | 
						|
      throw new Error("Tray icon not properly destroyed.");
 | 
						|
    }
 | 
						|
 | 
						|
    return event;
 | 
						|
  },
 | 
						|
);
 | 
						|
 | 
						|
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);
 | 
						|
  const webContents = remote.webContents.fromId(webview.getWebContentsId());
 | 
						|
  webContents.send("toggletray", state);
 | 
						|
}
 | 
						|
 | 
						|
ipcRenderer.on("toggletray", toggleTray);
 | 
						|
 | 
						|
if (ConfigUtil.getConfigItem("trayIcon", true)) {
 | 
						|
  createTray();
 | 
						|
}
 | 
						|
 | 
						|
export {};
 |