Compare commits

..

16 Commits

Author SHA1 Message Date
Anders Kaseorg
6d27cf8c7d release: New release v5.9.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:17:32 -08:00
Anders Kaseorg
1ac2483cc4 Upgrade dependencies, including Electron 22.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:14:43 -08:00
Anders Kaseorg
4d3420dcd0 vite: Externalize gatemaker.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:13:23 -08:00
Anders Kaseorg
38450a9aed vite: Don’t externalize dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 19:14:19 -08:00
Anders Kaseorg
24de7ebb97 webview: Remove did-navigate workaround
The Electron bug seems to have been fixed upstream.  Meanwhile, the
workaround had been causing the app to hang if it can’t connect to an
organization at startup.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:40 -08:00
Anders Kaseorg
5a571d66d0 Enable Chromium sandboxing for remote webviews.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
0ae998a51e Move clipboard decryption to main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
447dd18b8b Read injected.js from main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
9a200dc40c Replace remote wrapper module with Vite alias.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
d42b752ac1 Bundle with Vite.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
2f4103248d Move icons and sounds to public/resources.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
985d731d2b Move translations to public/translations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
032f95150c renderer: Add async constructors for functional tabs.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
d1aa5778c3 renderer: Set the icon src to a data: URL.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
13ce24b75e webview: Remove unnecessary __dirname resolution of customCss.
We’ve already checked that the file exists without resolving via
__dirname.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
fwcd
c89ec2faf1 Update installation instructions for macOS 2023-02-01 21:50:19 -08:00
90 changed files with 1447 additions and 7960 deletions

6
.gitignore vendored
View File

@@ -8,7 +8,8 @@
.transifexrc
# Compiled binary build directory
dist/
/dist/
/dist-electron/
#snap generated files
snap/parts
@@ -39,6 +40,3 @@ config.gypi
# tests/package.json
.python-version
# Ignore all the typescript compiled files
app/**/*.js

View File

@@ -1,3 +1,3 @@
/app/**/*.js
/app/translations/*.json
/dist
/dist-electron
/public/translations/*.json

View File

@@ -2,8 +2,8 @@
host = https://www.transifex.com
[zulip.desktopjson]
file_filter = app/translations/<lang>.json
file_filter = public/translations/<lang>.json
minimum_perc = 0
source_file = app/translations/en.json
source_file = public/translations/en.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -5,14 +5,14 @@ import * as Sentry from "@sentry/electron";
import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors";
import type * as z from "zod";
import {app, dialog} from "zulip:remote";
import {configSchemata} from "./config-schemata.js";
import * as EnterpriseUtil from "./enterprise-util.js";
import Logger from "./logger-util.js";
import {app, dialog} from "./remote.js";
export type Config = {
[Key in keyof typeof configSchemata]: z.output<typeof configSchemata[Key]>;
[Key in keyof typeof configSchemata]: z.output<(typeof configSchemata)[Key]>;
};
const logger = new Logger({
@@ -26,7 +26,7 @@ reloadDb();
export function getConfigItem<Key extends keyof Config>(
key: Key,
defaultValue: Config[Key],
): z.output<typeof configSchemata[Key]> {
): z.output<(typeof configSchemata)[Key]> {
try {
db.reload();
} catch (error: unknown) {

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import {app} from "./remote.js";
import {app} from "zulip:remote";
let setupCompleted = false;

View File

@@ -7,7 +7,7 @@ import * as ConfigUtil from "./config-util.js";
export type DndSettings = {
[Key in keyof typeof dndSettingsSchemata]: z.output<
typeof dndSettingsSchemata[Key]
(typeof dndSettingsSchemata)[Key]
>;
};

View File

@@ -9,7 +9,7 @@ import Logger from "./logger-util.js";
type EnterpriseConfig = {
[Key in keyof typeof enterpriseConfigSchemata]: z.output<
typeof enterpriseConfigSchemata[Key]
(typeof enterpriseConfigSchemata)[Key]
>;
};

View File

@@ -3,8 +3,9 @@ import fs from "node:fs";
import os from "node:os";
import process from "node:process";
import {app} from "zulip:remote";
import {initSetUp} from "./default-util.js";
import {app} from "./remote.js";
type LoggerOptions = {
file?: string;

15
app/common/paths.ts Normal file
View File

@@ -0,0 +1,15 @@
import path from "node:path";
import process from "node:process";
import url from "node:url";
export const bundlePath = __dirname;
export const publicPath = import.meta.env.DEV
? path.join(bundlePath, "../public")
: bundlePath;
export const bundleUrl = import.meta.env.DEV
? process.env.VITE_DEV_SERVER_URL
: url.pathToFileURL(__dirname).href + "/";
export const publicUrl = bundleUrl;

View File

@@ -1,8 +0,0 @@
import process from "node:process";
export const {app, dialog} =
process.type === "renderer"
? // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
(require("@electron/remote") as typeof import("@electron/remote"))
: // eslint-disable-next-line @typescript-eslint/no-require-imports
require("electron/main");

View File

@@ -3,9 +3,10 @@ import path from "node:path";
import i18n from "i18n";
import * as ConfigUtil from "./config-util.js";
import {publicPath} from "./paths.js";
i18n.configure({
directory: path.join(__dirname, "../translations/"),
directory: path.join(publicPath, "translations/"),
updateFiles: false,
});

View File

@@ -7,6 +7,8 @@ export type MainMessage = {
"fetch-user-agent": () => string;
"focus-app": () => void;
"focus-this-webview": () => void;
"get-injected-js": () => string;
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
"quit-app": () => void;
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
@@ -27,6 +29,7 @@ export type MainMessage = {
export type MainCall = {
"get-server-settings": (domain: string) => ServerConf;
"is-online": (url: string) => boolean;
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
"save-server-icon": (iconURL: string) => string;
};

View File

@@ -1,5 +1,9 @@
import {clipboard} from "electron/common";
import type {IpcMainEvent, WebContents} from "electron/main";
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
import {Buffer} from "node:buffer";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
@@ -7,6 +11,7 @@ import * as remoteMain from "@electron/remote/main";
import windowStateKeeper from "electron-window-state";
import * as ConfigUtil from "../common/config-util.js";
import {bundlePath, bundleUrl, publicPath} from "../common/paths.js";
import type {RendererMessage} from "../common/typed-ipc.js";
import type {MenuProps} from "../common/types.js";
@@ -35,13 +40,13 @@ let badgeCount: number;
let isQuitting = false;
// Load this url in main window
const mainUrl = "file://" + path.join(__dirname, "../renderer", "main.html");
// Load this file in main window
const mainUrl = new URL("app/renderer/main.html", bundleUrl).href;
const permissionCallbacks = new Map<number, (grant: boolean) => void>();
let nextPermissionCallbackId = 0;
const appIcon = path.join(__dirname, "../resources", "Icon");
const appIcon = path.join(publicPath, "resources/Icon");
const iconPath = (): string =>
appIcon + (process.platform === "win32" ? ".ico" : ".png");
@@ -74,7 +79,7 @@ function createMainWindow(): BrowserWindow {
minWidth: 500,
minHeight: 400,
webPreferences: {
preload: require.resolve("../renderer/js/main"),
preload: path.join(bundlePath, "renderer.js"),
sandbox: false,
webviewTag: true,
},
@@ -201,6 +206,49 @@ function createMainWindow(): BrowserWindow {
configureSpellChecker();
ipcMain.on("configure-spell-checker", configureSpellChecker);
ipcMain.on("get-injected-js", (event) => {
event.returnValue = fs.readFileSync(
path.join(bundlePath, "injected.js"),
"utf8",
);
});
const clipboardSigKey = crypto.randomBytes(32);
ipcMain.on("new-clipboard-key", (event) => {
const key = crypto.randomBytes(32);
const hmac = crypto.createHmac("sha256", clipboardSigKey);
hmac.update(key);
event.returnValue = {key, sig: hmac.digest()};
});
ipcMain.handle("poll-clipboard", (event, key, sig) => {
// Check that the key was generated here.
const hmac = crypto.createHmac("sha256", clipboardSigKey);
hmac.update(key);
if (!crypto.timingSafeEqual(sig, hmac.digest())) return;
try {
// Check that the data on the clipboard was encrypted to the key.
const data = Buffer.from(clipboard.readText(), "hex");
const iv = data.slice(0, 12);
const ciphertext = data.slice(12, -16);
const authTag = data.slice(-16);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
authTagLength: 16,
});
decipher.setAuthTag(authTag);
return (
decipher.update(ciphertext, undefined, "utf8") + decipher.final("utf8")
);
} catch {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return undefined;
}
});
AppMenu.setMenu({
tabs: [],
});

View File

@@ -1,6 +1,6 @@
import {app} from "electron/main";
import * as Sentry from "@sentry/electron";
import * as Sentry from "@sentry/electron/main"; // eslint-disable-line n/file-extension-in-import
import {getConfigItem} from "../common/config-util.js";

26
app/renderer/about.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/about.css" />
<!-- Initially hidden to prevent FOUC -->
<div class="about" hidden>
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version"></p>
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a href="https://zulip.com" target="_blank" rel="noopener noreferrer"
>Zulip</a
>
</p>
<p class="detail license">
Available under the
<a
href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>Apache 2.0 License</a
>
</p>
</div>
</div>

View File

@@ -1,6 +1,4 @@
import {clipboard} from "electron/common";
import {Buffer} from "node:buffer";
import crypto from "node:crypto";
import {ipcRenderer} from "./typed-ipc-renderer.js";
// This helper is exposed via electron_bridge for use in the social
// login flow.
@@ -30,7 +28,8 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
constructor(_: number) {
// At this time, the only version is 1.
this.version = 1;
this.key = crypto.randomBytes(32);
const {key, sig} = ipcRenderer.sendSync("new-clipboard-key");
this.key = key;
this.pasted = new Promise((resolve) => {
let interval: NodeJS.Timeout | null = null;
const startPolling = () => {
@@ -38,7 +37,7 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
interval = setInterval(poll, 1000);
}
poll();
void poll();
};
const stopPolling = () => {
@@ -48,30 +47,9 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
}
};
const poll = () => {
let plaintext;
try {
const data = Buffer.from(clipboard.readText(), "hex");
const iv = data.slice(0, 12);
const ciphertext = data.slice(12, -16);
const authTag = data.slice(-16);
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
this.key,
iv,
{authTagLength: 16},
);
decipher.setAuthTag(authTag);
plaintext =
decipher.update(ciphertext, undefined, "utf8") +
decipher.final("utf8");
} catch {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return;
}
const poll = async () => {
const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig);
if (plaintext === undefined) return;
window.removeEventListener("focus", startPolling);
window.removeEventListener("blur", stopPolling);

View File

@@ -1,6 +1,5 @@
import type {WebContents} from "electron/main";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import * as remote from "@electron/remote";
@@ -11,6 +10,7 @@ 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";
@@ -42,7 +42,7 @@ export default class WebView {
src="${props.url}"
${props.preload === undefined
? html``
: html`preload="${props.preload}" webpreferences="sandbox=no"`}
: html`preload="${props.preload}"`}
partition="persist:webviewsession"
allowpopups
>
@@ -56,11 +56,9 @@ export default class WebView {
) 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",
"did-attach",
() => {
resolve();
},
@@ -210,10 +208,7 @@ export default class WebView {
this.focus();
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
(async () =>
this.getWebContents().insertCSS(
fs.readFileSync(path.join(__dirname, "/../../css/preload.css"), "utf8"),
))();
(async () => this.getWebContents().insertCSS(preloadCss))();
// Get customCSS again from config util to avoid warning user again
const customCss = ConfigUtil.getConfigItem("customCSS", null);
@@ -229,9 +224,7 @@ export default class WebView {
}
(async () =>
this.getWebContents().insertCSS(
fs.readFileSync(path.resolve(__dirname, customCss), "utf8"),
))();
this.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))();
}
}

View File

@@ -1,4 +1,4 @@
import {EventEmitter} from "node:events";
import {EventEmitter} from "events"; // eslint-disable-line unicorn/prefer-node-protocol
import type {ClipboardDecrypter} from "./clipboard-decrypter.js";
import {ClipboardDecrypterImpl} from "./clipboard-decrypter.js";

View File

@@ -1,6 +1,8 @@
import {clipboard} from "electron/common";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import url from "node:url";
import {Menu, app, dialog, session} from "@electron/remote";
import * as remote from "@electron/remote";
@@ -14,6 +16,7 @@ import * as EnterpriseUtil from "../../common/enterprise-util.js";
import * as LinkUtil from "../../common/link-util.js";
import Logger from "../../common/logger-util.js";
import * as Messages from "../../common/messages.js";
import {bundlePath, bundleUrl} from "../../common/paths.js";
import type {NavItem, ServerConf, TabData} from "../../common/types.js";
import FunctionalTab from "./components/functional-tab.js";
@@ -44,12 +47,13 @@ const logger = new Logger({
file: "errors.log",
});
const rendererDirectory = path.resolve(__dirname, "..");
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
const rootWebContents = remote.getCurrentWebContents();
const dingSound = new Audio("../resources/sounds/ding.ogg");
const dingSound = new Audio(
new URL("resources/sounds/ding.ogg", bundleUrl).href,
);
export class ServerManagerView {
$addServerButton: HTMLButtonElement;
@@ -362,7 +366,10 @@ export class ServerManagerView {
this.tabs.push(
new ServerTab({
role: "server",
icon: server.icon,
icon: `data:application/octet-stream;base64,${fs.readFileSync(
server.icon,
"base64",
)}`,
name: server.alias,
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
@@ -397,7 +404,7 @@ export class ServerManagerView {
await this.openNetworkTroubleshooting(index);
},
onTitleChange: this.updateBadge.bind(this),
preload: "js/preload.js",
preload: url.pathToFileURL(path.join(bundlePath, "preload.js")).href,
}),
}),
);
@@ -543,7 +550,7 @@ export class ServerManagerView {
async openFunctionalTab(tabProps: {
name: string;
materialIcon: string;
makeView: () => Element;
makeView: () => Promise<Element>;
destroyView: () => void;
}): Promise<void> {
if (this.functionalTabs.has(tabProps.name)) {
@@ -555,7 +562,7 @@ export class ServerManagerView {
this.functionalTabs.set(tabProps.name, index);
const tabIndex = this.getTabIndex();
const $view = tabProps.makeView();
const $view = await tabProps.makeView();
this.$webviewsContainer.append($view);
this.tabs.push(
@@ -586,8 +593,8 @@ export class ServerManagerView {
await this.openFunctionalTab({
name: "Settings",
materialIcon: "settings",
makeView: () => {
this.preferenceView = new PreferenceView();
makeView: async () => {
this.preferenceView = await PreferenceView.create();
this.preferenceView.$view.classList.add("functional-view");
return this.preferenceView.$view;
},
@@ -605,8 +612,8 @@ export class ServerManagerView {
await this.openFunctionalTab({
name: "About",
materialIcon: "sentiment_very_satisfied",
makeView() {
aboutView = new AboutView();
async makeView() {
aboutView = await AboutView.create();
aboutView.$view.classList.add("functional-view");
return aboutView.$view;
},
@@ -624,7 +631,7 @@ export class ServerManagerView {
reconnectUtil.pollInternetAndReload();
await webview
.getWebContents()
.loadURL(`file://${rendererDirectory}/network.html`);
.loadURL(new URL("app/renderer/network.html", bundleUrl).href);
}
async activateLastTab(index: number): Promise<void> {

View File

@@ -1,41 +1,21 @@
import {app} from "@electron/remote";
import {html} from "../../../common/html.js";
import {bundleUrl} from "../../../common/paths.js";
export class AboutView {
static async create(): Promise<AboutView> {
return new AboutView(
await (await fetch(new URL("app/renderer/about.html", bundleUrl))).text(),
);
}
readonly $view: HTMLElement;
constructor() {
private constructor(templateHtml: string) {
this.$view = document.createElement("div");
const $shadow = this.$view.attachShadow({mode: "open"});
$shadow.innerHTML = html`
<link rel="stylesheet" href="css/about.css" />
<!-- Initially hidden to prevent FOUC -->
<div class="about" hidden>
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">v${app.getVersion()}</p>
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a
href="https://zulip.com"
target="_blank"
rel="noopener noreferrer"
>Zulip</a
>
</p>
<p class="detail license">
Available under the
<a
href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>Apache 2.0 License</a
>
</p>
</div>
</div>
`.html;
$shadow.innerHTML = templateHtml;
$shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`;
}
destroy() {

View File

@@ -9,11 +9,11 @@ import Tagify from "@yaireo/tagify";
import ISO6391 from "iso-639-1";
import * as z from "zod";
import supportedLocales from "../../../../../public/translations/supported-locales.json";
import * as ConfigUtil from "../../../../common/config-util.js";
import * as EnterpriseUtil from "../../../../common/enterprise-util.js";
import {html} from "../../../../common/html.js";
import * as t from "../../../../common/translation-util.js";
import supportedLocales from "../../../../translations/supported-locales.json";
import {ipcRenderer} from "../../typed-ipc-renderer.js";
import {generateSelectHtml, generateSettingOption} from "./base-section.js";

View File

@@ -1,7 +1,7 @@
import process from "node:process";
import type {DndSettings} from "../../../../common/dnd-util.js";
import {html} from "../../../../common/html.js";
import {bundleUrl} from "../../../../common/paths.js";
import type {NavItem} from "../../../../common/types.js";
import {ipcRenderer} from "../../typed-ipc-renderer.js";
@@ -13,34 +13,24 @@ import {initServersSection} from "./servers-section.js";
import {initShortcutsSection} from "./shortcuts-section.js";
export class PreferenceView {
static async create(): Promise<PreferenceView> {
return new PreferenceView(
await (
await fetch(new URL("app/renderer/preference.html", bundleUrl))
).text(),
);
}
readonly $view: HTMLElement;
private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element;
private readonly nav: Nav;
private navItem: NavItem = "General";
constructor() {
private constructor(templateHtml: string) {
this.$view = document.createElement("div");
this.$shadow = this.$view.attachShadow({mode: "open"});
this.$shadow.innerHTML = html`
<link
rel="stylesheet"
href="${require.resolve("../../../css/fonts.css")}"
/>
<link
rel="stylesheet"
href="${require.resolve("../../../css/preference.css")}"
/>
<link
rel="stylesheet"
href="${require.resolve("@yaireo/tagify/dist/tagify.css")}"
/>
<!-- Initially hidden to prevent FOUC -->
<div id="content" hidden>
<div id="sidebar"></div>
<div id="settings-container"></div>
</div>
`.html;
this.$shadow.innerHTML = templateHtml;
const $sidebarContainer = this.$shadow.querySelector("#sidebar")!;
this.$settingsContainer = this.$shadow.querySelector(

View File

@@ -1,5 +1,4 @@
import {contextBridge, webFrame} from "electron/renderer";
import fs from "node:fs";
import electron_bridge, {bridgeEvents} from "./electron-bridge.js";
import * as NetworkError from "./pages/network.js";
@@ -76,6 +75,4 @@ window.addEventListener("load", () => {
});
(async () =>
webFrame.executeJavaScript(
fs.readFileSync(require.resolve("./injected"), "utf8"),
))();
webFrame.executeJavaScript(ipcRenderer.sendSync("get-injected-js")))();

View File

@@ -7,6 +7,7 @@ import process from "node:process";
import {BrowserWindow, Menu, Tray} from "@electron/remote";
import * as ConfigUtil from "../../common/config-util.js";
import {publicPath} from "../../common/paths.js";
import type {RendererMessage} from "../../common/typed-ipc.js";
import type {ServerManagerView} from "./main.js";
@@ -14,11 +15,7 @@ import {ipcRenderer} from "./typed-ipc-renderer.js";
let tray: ElectronTray | null = null;
const iconDir = "../../resources/tray";
const traySuffix = "tray";
const appIcon = path.join(__dirname, iconDir, traySuffix);
const appIcon = path.join(publicPath, "resources/tray/tray");
const iconPath = (): string => {
if (process.platform === "linux") {

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width" />
<title>Zulip</title>
<link rel="stylesheet" href="css/fonts.css" />
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen" />
<link rel="stylesheet" href="css/main.css" />
</head>
<body>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/fonts.css" />
<link rel="stylesheet" href="css/preference.css" />
<script type="module">
import "@yaireo/tagify/dist/tagify.css";
</script>
<!-- Initially hidden to prevent FOUC -->
<div id="content" hidden>
<div id="sidebar"></div>
<div id="settings-container"></div>
</div>

View File

@@ -2,6 +2,20 @@
All notable changes to the Zulip desktop app are documented in this file.
### v5.9.5 --2023-02-06
**Fixes**:
- Fixed a hang on startup when an organization cannot be connected at startup.
**Enhancements**:
- Enabled Chromium sandboxing in remote renderer processes for improved security hardening.
**Dependencies**:
- Upgraded all dependencies, including Electron 22.2.0.
### v5.9.4 --2023-01-04
**Fixes**:

View File

@@ -38,7 +38,7 @@ You'll want Transifex's CLI client, `tx`.
Run `tx push -s`.
This uploads from `app/translations/en.json` to the
This uploads from `public/translations/en.json` to the
set of strings Transifex shows for contributors to translate.
(See `.tx/config` for how that's configured.)
@@ -46,7 +46,7 @@ set of strings Transifex shows for contributors to translate.
Run `tools/tx-pull`.
This writes to files `app/translations/<lang>.json`.
This writes to files `public/translations/<lang>.json`.
(See `.tx/config` for how that's configured.)
Then look at the following sections to see if further updates are
@@ -59,7 +59,7 @@ language. This happens when we've opened up a new language for people
to contribute translations into in the Zulip project on Transifex,
which we do when someone expresses interest in contributing them.
The locales for supported languages are stored in `app/translations/supported-locales.json`
The locales for supported languages are stored in `public/translations/supported-locales.json`
So, when a new language is added, update the `supported-locales` module.

View File

@@ -7,7 +7,7 @@
[lr]: https://github.com/zulip/zulip-desktop/releases
## OS X
## macOS
**DMG or zip**:
@@ -17,7 +17,7 @@
**Using brew**:
1. Run `brew cask install zulip` in your terminal
1. Run `brew install --cask zulip` in your terminal
2. The app will be installed in your `Applications`
3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`)

8851
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "5.9.4",
"main": "./app/main",
"version": "5.9.5",
"main": "./dist-electron",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
@@ -21,8 +21,7 @@
"node": ">=16.13.2"
},
"scripts": {
"start": "tsc && electron .",
"clean-ts-files": "git clean \"app/*.js\" -xf",
"start": "vite",
"watch-ts": "tsc -w",
"reinstall": "rimraf node_modules && npm install",
"postinstall": "electron-builder install-app-deps",
@@ -30,11 +29,11 @@
"lint-html": "htmlhint \"app/**/*.html\"",
"lint-js": "xo",
"prettier-non-js": "prettier --check --loglevel=warn . \"!**/*.{js,ts}\"",
"test": "tsc --noEmit && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js",
"test-e2e": "tsc && tape \"tests/**/*.js\"",
"pack": "tsc && electron-builder --dir",
"dist": "tsc && electron-builder",
"mas": "tsc && electron-builder --mac mas"
"test": "tsc && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js",
"test-e2e": "vite build && tape \"tests/**/*.js\"",
"pack": "vite build && electron-builder --dir",
"dist": "vite build && electron-builder",
"mas": "vite build && electron-builder --mac mas"
},
"pre-commit": [
"test"
@@ -47,7 +46,7 @@
"**/*.node"
],
"files": [
"app/**/*"
"dist-electron/**/*"
],
"copyright": "©2020 Kandra Labs, Inc.",
"mac": {
@@ -142,25 +141,12 @@
"InstantMessaging"
],
"dependencies": {
"@electron/remote": "^2.0.8",
"@sentry/electron": "^4.1.2",
"@yaireo/tagify": "^4.5.0",
"adm-zip": "^0.5.5",
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"electron-log": "^4.3.5",
"electron-updater": "^5.0.1",
"electron-window-state": "^5.0.3",
"escape-goat": "^3.0.0",
"gatemaker": "^1.0.0",
"get-stream": "^6.0.1",
"i18n": "^0.15.1",
"iso-639-1": "^2.1.9",
"node-json-db": "^1.3.0",
"semver": "^7.3.5",
"zod": "^3.5.1"
"gatemaker": "^1.0.0"
},
"devDependencies": {
"@electron/notarize": "^1.2.3",
"@electron/remote": "^2.0.8",
"@sentry/electron": "^4.1.2",
"@types/adm-zip": "^0.5.0",
"@types/auto-launch": "^5.0.2",
"@types/backoff": "^2.5.2",
@@ -168,22 +154,37 @@
"@types/node": "^16.11.26",
"@types/requestidlecallback": "^0.3.4",
"@types/yaireo__tagify": "^4.3.2",
"@yaireo/tagify": "^4.5.0",
"adm-zip": "^0.5.5",
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"dotenv": "^16.0.0",
"electron": "^22.0.0",
"electron-builder": "^23.0.3",
"electron-notarize": "^1.0.0",
"electron-log": "^4.3.5",
"electron-updater": "^5.0.1",
"electron-window-state": "^5.0.3",
"escape-goat": "^4.0.0",
"get-stream": "^6.0.1",
"htmlhint": "^1.1.2",
"i18n": "^0.15.1",
"iso-639-1": "^2.1.9",
"medium": "^1.2.0",
"node-json-db": "^1.3.0",
"playwright-core": "^1.30.0-alpha-jan-3-2023",
"pre-commit": "^1.2.2",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"rimraf": "^4.1.2",
"semver": "^7.3.5",
"stylelint": "^14.5.3",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^29.0.0",
"tape": "^5.2.2",
"typescript": "^4.3.5",
"xo": "^0.53.1"
"vite": "^4.1.1",
"vite-plugin-electron": "^0.11.1",
"xo": "^0.53.1",
"zod": "^3.5.1"
},
"prettier": {
"bracketSpacing": false,
@@ -203,8 +204,7 @@
"target": "./app/common",
"from": "./app",
"except": [
"./common",
"./translations"
"./common"
]
},
{
@@ -212,8 +212,7 @@
"from": "./app",
"except": [
"./common",
"./main",
"./translations"
"./main"
]
},
{
@@ -222,7 +221,7 @@
"except": [
"./common",
"./renderer",
"./translations"
"./resources"
]
}
]

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 321 B

View File

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

View File

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 932 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -2,8 +2,8 @@
const path = require("node:path");
const process = require("node:process");
const {notarize} = require("@electron/notarize");
const dotenv = require("dotenv");
const {notarize} = require("electron-notarize");
dotenv.config({path: path.join(__dirname, "/../.env")});

View File

@@ -1,5 +1,5 @@
{
"version": "5.9.3",
"productName": "ZulipTest",
"main": "../app/main/index.js"
"main": "../dist-electron"
}

View File

@@ -1,10 +1,13 @@
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"noEmit": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true,
"noImplicitOverride": true
"noImplicitOverride": true,
"types": ["vite/client"]
}
}

7
typings.d.ts vendored
View File

@@ -3,3 +3,10 @@ declare namespace Electron {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IncomingMessage extends NodeJS.ReadableStream {}
}
declare module "zulip:remote" {
export const {
app,
dialog,
}: typeof import("electron/main") | typeof import("@electron/remote");
}

116
vite.config.ts Normal file
View File

@@ -0,0 +1,116 @@
/* eslint-disable @typescript-eslint/naming-convention */
import * as path from "node:path";
import {defineConfig} from "vite";
import electron from "vite-plugin-electron";
let resolveInjected: () => void;
let resolvePreload: () => void;
let resolveRenderer: () => void;
const whenInjected = new Promise<void>((resolve) => {
resolveInjected = resolve;
});
const whenPreload = new Promise<void>((resolve) => {
resolvePreload = resolve;
});
const whenRenderer = new Promise<void>((resolve) => {
resolveRenderer = resolve;
});
export default defineConfig({
plugins: [
electron([
{
entry: {
index: "app/main",
},
async onstart({startup}) {
await whenInjected;
await whenPreload;
await whenRenderer;
await startup();
},
vite: {
build: {
sourcemap: true,
rollupOptions: {
external: ["electron", /^electron\//, /^gatemaker\//],
},
ssr: true,
},
resolve: {
alias: {
"zulip:remote": "electron/main",
},
},
ssr: {
noExternal: true,
},
},
},
{
entry: {
injected: "app/renderer/js/injected.ts",
},
onstart() {
resolveInjected();
},
vite: {
build: {
sourcemap: "inline",
},
},
},
{
entry: {
preload: "app/renderer/js/preload.ts",
},
onstart() {
resolvePreload();
},
vite: {
build: {
sourcemap: "inline",
rollupOptions: {
external: ["electron", /^electron\//],
},
},
},
},
{
entry: {
renderer: "app/renderer/js/main.ts",
},
onstart() {
resolveRenderer();
},
vite: {
build: {
sourcemap: true,
rollupOptions: {
external: ["electron", /^electron\//],
},
},
resolve: {
alias: {
"zulip:remote": "@electron/remote",
},
},
},
},
]),
],
build: {
outDir: "dist-electron",
sourcemap: true,
rollupOptions: {
input: {
renderer: path.join(__dirname, "app/renderer/main.html"),
network: path.join(__dirname, "app/renderer/network.html"),
about: path.join(__dirname, "app/renderer/about.html"),
preference: path.join(__dirname, "app/renderer/preference.html"),
},
},
},
});