Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d27cf8c7d | ||
|
|
1ac2483cc4 | ||
|
|
4d3420dcd0 | ||
|
|
38450a9aed | ||
|
|
24de7ebb97 | ||
|
|
5a571d66d0 | ||
|
|
0ae998a51e | ||
|
|
447dd18b8b | ||
|
|
9a200dc40c | ||
|
|
d42b752ac1 | ||
|
|
2f4103248d | ||
|
|
985d731d2b | ||
|
|
032f95150c | ||
|
|
d1aa5778c3 | ||
|
|
13ce24b75e | ||
|
|
c89ec2faf1 |
6
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/app/**/*.js
|
||||
/app/translations/*.json
|
||||
/dist
|
||||
/dist-electron
|
||||
/public/translations/*.json
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import {app} from "./remote.js";
|
||||
import {app} from "zulip:remote";
|
||||
|
||||
let setupCompleted = false;
|
||||
|
||||
|
||||
@@ -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]
|
||||
>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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]
|
||||
>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 hasn’t been copied yet; try
|
||||
// again next time.
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
AppMenu.setMenu({
|
||||
tabs: [],
|
||||
});
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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 hasn’t 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);
|
||||
|
||||
@@ -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")))();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")))();
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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>
|
||||
|
||||
13
app/renderer/preference.html
Normal 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>
|
||||
14
changelog.md
@@ -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**:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
69
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
@@ -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")});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "5.9.3",
|
||||
"productName": "ZulipTest",
|
||||
"main": "../app/main/index.js"
|
||||
"main": "../dist-electron"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||