xo: Fix unicorn/prevent-abbreviations.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2024-03-22 15:52:29 -07:00
parent 86e28f5b00
commit 47366b7617
37 changed files with 425 additions and 383 deletions

View File

@@ -19,23 +19,23 @@ const logger = new Logger({
file: "config-util.log", file: "config-util.log",
}); });
let db: JsonDB; let database: JsonDB;
reloadDb(); reloadDatabase();
export function getConfigItem<Key extends keyof Config>( export function getConfigItem<Key extends keyof Config>(
key: Key, key: Key,
defaultValue: Config[Key], defaultValue: Config[Key],
): z.output<(typeof configSchemata)[Key]> { ): z.output<(typeof configSchemata)[Key]> {
try { try {
db.reload(); database.reload();
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error while reloading settings.json: "); logger.error("Error while reloading settings.json: ");
logger.error(error); logger.error(error);
} }
try { try {
return configSchemata[key].parse(db.getObject<unknown>(`/${key}`)); return configSchemata[key].parse(database.getObject<unknown>(`/${key}`));
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
setConfigItem(key, defaultValue); setConfigItem(key, defaultValue);
@@ -46,13 +46,13 @@ export function getConfigItem<Key extends keyof Config>(
// This function returns whether a key exists in the configuration file (settings.json) // This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean { export function isConfigItemExists(key: string): boolean {
try { try {
db.reload(); database.reload();
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error while reloading settings.json: "); logger.error("Error while reloading settings.json: ");
logger.error(error); logger.error(error);
} }
return db.exists(`/${key}`); return database.exists(`/${key}`);
} }
export function setConfigItem<Key extends keyof Config>( export function setConfigItem<Key extends keyof Config>(
@@ -66,16 +66,16 @@ export function setConfigItem<Key extends keyof Config>(
} }
configSchemata[key].parse(value); configSchemata[key].parse(value);
db.push(`/${key}`, value, true); database.push(`/${key}`, value, true);
db.save(); database.save();
} }
export function removeConfigItem(key: string): void { export function removeConfigItem(key: string): void {
db.delete(`/${key}`); database.delete(`/${key}`);
db.save(); database.save();
} }
function reloadDb(): void { function reloadDatabase(): void {
const settingsJsonPath = path.join( const settingsJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"/config/settings.json", "/config/settings.json",
@@ -96,5 +96,5 @@ function reloadDb(): void {
} }
} }
db = new JsonDB(settingsJsonPath, true, true); database = new JsonDB(settingsJsonPath, true, true);
} }

View File

@@ -4,30 +4,30 @@ import {app} from "zulip:remote";
let setupCompleted = false; let setupCompleted = false;
const zulipDir = app.getPath("userData"); const zulipDirectory = app.getPath("userData");
const logDir = `${zulipDir}/Logs/`; const logDirectory = `${zulipDirectory}/Logs/`;
const configDir = `${zulipDir}/config/`; const configDirectory = `${zulipDirectory}/config/`;
export const initSetUp = (): void => { export const initSetUp = (): void => {
// If it is the first time the app is running // If it is the first time the app is running
// create zulip dir in userData folder to // create zulip dir in userData folder to
// avoid errors // avoid errors
if (!setupCompleted) { if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) { if (!fs.existsSync(zulipDirectory)) {
fs.mkdirSync(zulipDir); fs.mkdirSync(zulipDirectory);
} }
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDir); fs.mkdirSync(logDirectory);
} }
// Migrate config files from app data folder to config folder inside app // Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version. // data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) { if (!fs.existsSync(configDirectory)) {
fs.mkdirSync(configDir); fs.mkdirSync(configDirectory);
const domainJson = `${zulipDir}/domain.json`; const domainJson = `${zulipDirectory}/domain.json`;
const settingsJson = `${zulipDir}/settings.json`; const settingsJson = `${zulipDirectory}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`; const updatesJson = `${zulipDirectory}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`; const windowStateJson = `${zulipDirectory}/window-state.json`;
const configData = [ const configData = [
{ {
path: domainJson, path: domainJson,
@@ -44,7 +44,7 @@ export const initSetUp = (): void => {
]; ];
for (const data of configData) { for (const data of configData) {
if (fs.existsSync(data.path)) { if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName); fs.copyFileSync(data.path, configDirectory + data.fileName);
fs.unlinkSync(data.path); fs.unlinkSync(data.path);
} }
} }

View File

@@ -20,9 +20,9 @@ const logger = new Logger({
let enterpriseSettings: Partial<EnterpriseConfig>; let enterpriseSettings: Partial<EnterpriseConfig>;
let configFile: boolean; let configFile: boolean;
reloadDb(); reloadDatabase();
function reloadDb(): void { function reloadDatabase(): void {
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json"; let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
if (process.platform === "win32") { if (process.platform === "win32") {
enterpriseFile = enterpriseFile =
@@ -56,7 +56,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
key: Key, key: Key,
defaultValue: EnterpriseConfig[Key], defaultValue: EnterpriseConfig[Key],
): EnterpriseConfig[Key] { ): EnterpriseConfig[Key] {
reloadDb(); reloadDatabase();
if (!configFile) { if (!configFile) {
return defaultValue; return defaultValue;
} }
@@ -66,7 +66,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
} }
export function configItemExists(key: keyof EnterpriseConfig): boolean { export function configItemExists(key: keyof EnterpriseConfig): boolean {
reloadDb(); reloadDatabase();
if (!configFile) { if (!configFile) {
return false; return false;
} }

View File

@@ -11,8 +11,8 @@ export async function openBrowser(url: URL): Promise<void> {
} else { } else {
// For security, indirect links to non-whitelisted protocols // For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file. // through a real web browser via a local HTML file.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-")); const directory = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
const file = path.join(dir, "redirect.html"); const file = path.join(directory, "redirect.html");
fs.writeFileSync( fs.writeFileSync(
file, file,
html` html`
@@ -37,7 +37,7 @@ export async function openBrowser(url: URL): Promise<void> {
await shell.openPath(file); await shell.openPath(file);
setTimeout(() => { setTimeout(() => {
fs.unlinkSync(file); fs.unlinkSync(file);
fs.rmdirSync(dir); fs.rmdirSync(directory);
}, 15_000); }, 15_000);
} }
} }

View File

@@ -13,7 +13,7 @@ type LoggerOptions = {
initSetUp(); initSetUp();
const logDir = `${app.getPath("userData")}/Logs`; const logDirectory = `${app.getPath("userData")}/Logs`;
type Level = "log" | "debug" | "info" | "warn" | "error"; type Level = "log" | "debug" | "info" | "warn" | "error";
@@ -23,7 +23,7 @@ export default class Logger {
constructor(options: LoggerOptions = {}) { constructor(options: LoggerOptions = {}) {
let {file = "console.log"} = options; let {file = "console.log"} = options;
file = `${logDir}/${file}`; file = `${logDirectory}/${file}`;
// Trim log according to type of process // Trim log according to type of process
if (process.type === "renderer") { if (process.type === "renderer") {
@@ -38,31 +38,31 @@ export default class Logger {
this.nodeConsole = nodeConsole; this.nodeConsole = nodeConsole;
} }
_log(type: Level, ...args: unknown[]): void { _log(type: Level, ...arguments_: unknown[]): void {
args.unshift(this.getTimestamp() + " |\t"); arguments_.unshift(this.getTimestamp() + " |\t");
args.unshift(type.toUpperCase() + " |"); arguments_.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...args); this.nodeConsole[type](...arguments_);
console[type](...args); console[type](...arguments_);
} }
log(...args: unknown[]): void { log(...arguments_: unknown[]): void {
this._log("log", ...args); this._log("log", ...arguments_);
} }
debug(...args: unknown[]): void { debug(...arguments_: unknown[]): void {
this._log("debug", ...args); this._log("debug", ...arguments_);
} }
info(...args: unknown[]): void { info(...arguments_: unknown[]): void {
this._log("info", ...args); this._log("info", ...arguments_);
} }
warn(...args: unknown[]): void { warn(...arguments_: unknown[]): void {
this._log("warn", ...args); this._log("warn", ...arguments_);
} }
error(...args: unknown[]): void { error(...arguments_: unknown[]): void {
this._log("error", ...args); this._log("error", ...arguments_);
} }
getTimestamp(): string { getTimestamp(): string {

View File

@@ -1,5 +1,5 @@
import type {DndSettings} from "./dnd-util.js"; import type {DndSettings} from "./dnd-util.js";
import type {MenuProps, ServerConf} from "./types.js"; import type {MenuProperties, ServerConfig} from "./types.js";
export type MainMessage = { export type MainMessage = {
"clear-app-settings": () => void; "clear-app-settings": () => void;
@@ -21,12 +21,12 @@ export type MainMessage = {
toggleAutoLauncher: (AutoLaunchValue: boolean) => void; toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
"unread-count": (unreadCount: number) => void; "unread-count": (unreadCount: number) => void;
"update-badge": (messageCount: number) => void; "update-badge": (messageCount: number) => void;
"update-menu": (props: MenuProps) => void; "update-menu": (properties: MenuProperties) => void;
"update-taskbar-icon": (data: string, text: string) => void; "update-taskbar-icon": (data: string, text: string) => void;
}; };
export type MainCall = { export type MainCall = {
"get-server-settings": (domain: string) => ServerConf; "get-server-settings": (domain: string) => ServerConfig;
"is-online": (url: string) => boolean; "is-online": (url: string) => boolean;
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined; "poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
"save-server-icon": (iconURL: string) => string | null; "save-server-icon": (iconURL: string) => string | null;
@@ -74,7 +74,7 @@ export type RendererMessage = {
"toggle-silent": (state: boolean) => void; "toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void; "toggle-tray": (state: boolean) => void;
toggletray: () => void; toggletray: () => void;
tray: (arg: number) => void; tray: (argument: number) => void;
"update-realm-icon": (serverURL: string, iconURL: string) => void; "update-realm-icon": (serverURL: string, iconURL: string) => void;
"update-realm-name": (serverURL: string, realmName: string) => void; "update-realm-name": (serverURL: string, realmName: string) => void;
"webview-reload": () => void; "webview-reload": () => void;

View File

@@ -1,17 +1,17 @@
export type MenuProps = { export type MenuProperties = {
tabs: TabData[]; tabs: TabData[];
activeTabIndex?: number; activeTabIndex?: number;
enableMenu?: boolean; enableMenu?: boolean;
}; };
export type NavItem = export type NavigationItem =
| "General" | "General"
| "Network" | "Network"
| "AddServer" | "AddServer"
| "Organizations" | "Organizations"
| "Shortcuts"; | "Shortcuts";
export type ServerConf = { export type ServerConfig = {
url: string; url: string;
alias: string; alias: string;
icon: string; icon: string;

View File

@@ -20,7 +20,7 @@ import windowStateKeeper from "electron-window-state";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.js";
import {bundlePath, bundleUrl, publicPath} from "../common/paths.js"; import {bundlePath, bundleUrl, publicPath} from "../common/paths.js";
import type {RendererMessage} from "../common/typed-ipc.js"; import type {RendererMessage} from "../common/typed-ipc.js";
import type {MenuProps} from "../common/types.js"; import type {MenuProperties} from "../common/types.js";
import {appUpdater, shouldQuitForUpdate} from "./autoupdater.js"; import {appUpdater, shouldQuitForUpdate} from "./autoupdater.js";
import * as BadgeSettings from "./badge-settings.js"; import * as BadgeSettings from "./badge-settings.js";
@@ -424,10 +424,10 @@ ${error}`,
}, },
); );
ipcMain.on("update-menu", (_event, props: MenuProps) => { ipcMain.on("update-menu", (_event, properties: MenuProperties) => {
AppMenu.setMenu(props); AppMenu.setMenu(properties);
if (props.activeTabIndex !== undefined) { if (properties.activeTabIndex !== undefined) {
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = properties.tabs[properties.activeTabIndex];
mainWindow.setTitle(`Zulip - ${activeTab.name}`); mainWindow.setTitle(`Zulip - ${activeTab.name}`);
} }
}); });

View File

@@ -11,18 +11,18 @@ const logger = new Logger({
file: "linux-update-util.log", file: "linux-update-util.log",
}); });
let db: JsonDB; let database: JsonDB;
reloadDb(); reloadDatabase();
export function getUpdateItem( export function getUpdateItem(
key: string, key: string,
defaultValue: true | null = null, defaultValue: true | null = null,
): true | null { ): true | null {
reloadDb(); reloadDatabase();
let value: unknown; let value: unknown;
try { try {
value = db.getObject<unknown>(`/${key}`); value = database.getObject<unknown>(`/${key}`);
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
} }
@@ -36,16 +36,16 @@ export function getUpdateItem(
} }
export function setUpdateItem(key: string, value: true | null): void { export function setUpdateItem(key: string, value: true | null): void {
db.push(`/${key}`, value, true); database.push(`/${key}`, value, true);
reloadDb(); reloadDatabase();
} }
export function removeUpdateItem(key: string): void { export function removeUpdateItem(key: string): void {
db.delete(`/${key}`); database.delete(`/${key}`);
reloadDb(); reloadDatabase();
} }
function reloadDb(): void { function reloadDatabase(): void {
const linuxUpdateJsonPath = path.join( const linuxUpdateJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"/config/updates.json", "/config/updates.json",
@@ -65,5 +65,5 @@ function reloadDb(): void {
} }
} }
db = new JsonDB(linuxUpdateJsonPath, true, true); database = new JsonDB(linuxUpdateJsonPath, true, true);
} }

View File

@@ -13,7 +13,7 @@ import * as ConfigUtil from "../common/config-util.js";
import * as DNDUtil from "../common/dnd-util.js"; import * as DNDUtil from "../common/dnd-util.js";
import * as t from "../common/translation-util.js"; import * as t from "../common/translation-util.js";
import type {RendererMessage} from "../common/typed-ipc.js"; import type {RendererMessage} from "../common/typed-ipc.js";
import type {MenuProps, TabData} from "../common/types.js"; import type {MenuProperties, TabData} from "../common/types.js";
import {appUpdater} from "./autoupdater.js"; import {appUpdater} from "./autoupdater.js";
import {send} from "./typed-ipc-main.js"; import {send} from "./typed-ipc-main.js";
@@ -372,8 +372,10 @@ function getWindowSubmenu(
return initialSubmenu; return initialSubmenu;
} }
function getDarwinTpl(props: MenuProps): MenuItemConstructorOptions[] { function getDarwinTpl(
const {tabs, activeTabIndex, enableMenu = false} = props; properties: MenuProperties,
): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = properties;
return [ return [
{ {
@@ -537,8 +539,8 @@ function getDarwinTpl(props: MenuProps): MenuItemConstructorOptions[] {
]; ];
} }
function getOtherTpl(props: MenuProps): MenuItemConstructorOptions[] { function getOtherTpl(properties: MenuProperties): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props; const {tabs, activeTabIndex, enableMenu = false} = properties;
return [ return [
{ {
label: t.__("File"), label: t.__("File"),
@@ -687,7 +689,7 @@ function getOtherTpl(props: MenuProps): MenuItemConstructorOptions[] {
function sendAction<Channel extends keyof RendererMessage>( function sendAction<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
@@ -695,7 +697,7 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore(); win.restore();
} }
send(win.webContents, channel, ...args); send(win.webContents, channel, ...arguments_);
} }
async function checkForUpdate(): Promise<void> { async function checkForUpdate(): Promise<void> {
@@ -718,9 +720,11 @@ function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
return activeTabIndex; return activeTabIndex;
} }
export function setMenu(props: MenuProps): void { export function setMenu(properties: MenuProperties): void {
const tpl = const tpl =
process.platform === "darwin" ? getDarwinTpl(props) : getOtherTpl(props); process.platform === "darwin"
? getDarwinTpl(properties)
: getOtherTpl(properties);
const menu = Menu.buildFromTemplate(tpl); const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} }

View File

@@ -10,7 +10,7 @@ import {z} from "zod";
import Logger from "../common/logger-util.js"; import Logger from "../common/logger-util.js";
import * as Messages from "../common/messages.js"; import * as Messages from "../common/messages.js";
import type {ServerConf} from "../common/types.js"; import type {ServerConfig} from "../common/types.js";
/* Request: domain-util */ /* Request: domain-util */
@@ -19,7 +19,7 @@ const logger = new Logger({
}); });
const generateFilePath = (url: string): string => { const generateFilePath = (url: string): string => {
const dir = `${app.getPath("userData")}/server-icons`; const directory = `${app.getPath("userData")}/server-icons`;
const extension = path.extname(url).split("?")[0]; const extension = path.extname(url).split("?")[0];
let hash = 5381; let hash = 5381;
@@ -31,18 +31,18 @@ const generateFilePath = (url: string): string => {
} }
// Create 'server-icons' directory if not existed // Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) { if (!fs.existsSync(directory)) {
fs.mkdirSync(dir); fs.mkdirSync(directory);
} }
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return `${dir}/${hash >>> 0}${extension}`; return `${directory}/${hash >>> 0}${extension}`;
}; };
export const _getServerSettings = async ( export const _getServerSettings = async (
domain: string, domain: string,
session: Session, session: Session,
): Promise<ServerConf> => { ): Promise<ServerConfig> => {
const response = await session.fetch(domain + "/api/v1/server_settings"); const response = await session.fetch(domain + "/api/v1/server_settings");
if (!response.ok) { if (!response.ok) {
throw new Error(Messages.invalidZulipServerError(domain)); throw new Error(Messages.invalidZulipServerError(domain));

View File

@@ -12,14 +12,20 @@ import type {
} from "../common/typed-ipc.js"; } from "../common/typed-ipc.js";
type MainListener<Channel extends keyof MainMessage> = type MainListener<Channel extends keyof MainMessage> =
MainMessage[Channel] extends (...args: infer Args) => infer Return MainMessage[Channel] extends (...arguments_: infer Arguments) => infer Return
? (event: IpcMainEvent & {returnValue: Return}, ...args: Args) => void ? (
event: IpcMainEvent & {returnValue: Return},
...arguments_: Arguments
) => void
: never; : never;
type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends ( type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends (
...args: infer Args ...arguments_: infer Arguments
) => infer Return ) => infer Return
? (event: IpcMainInvokeEvent, ...args: Args) => Return | Promise<Return> ? (
event: IpcMainInvokeEvent,
...arguments_: Arguments
) => Return | Promise<Return>
: never; : never;
export const ipcMain: { export const ipcMain: {
@@ -28,7 +34,7 @@ export const ipcMain: {
listener: <Channel extends keyof RendererMessage>( listener: <Channel extends keyof RendererMessage>(
event: IpcMainEvent, event: IpcMainEvent,
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
) => void, ) => void,
): void; ): void;
on( on(
@@ -37,7 +43,7 @@ export const ipcMain: {
event: IpcMainEvent, event: IpcMainEvent,
webContentsId: number, webContentsId: number,
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
) => void, ) => void,
): void; ): void;
on<Channel extends keyof MainMessage>( on<Channel extends keyof MainMessage>(
@@ -67,16 +73,16 @@ export const ipcMain: {
export function send<Channel extends keyof RendererMessage>( export function send<Channel extends keyof RendererMessage>(
contents: WebContents, contents: WebContents,
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
contents.send(channel, ...args); contents.send(channel, ...arguments_);
} }
export function sendToFrame<Channel extends keyof RendererMessage>( export function sendToFrame<Channel extends keyof RendererMessage>(
contents: WebContents, contents: WebContents,
frameId: number | [number, number], frameId: number | [number, number],
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
contents.sendToFrame(frameId, channel, ...args); contents.sendToFrame(frameId, channel, ...arguments_);
} }

View File

@@ -20,7 +20,7 @@ export type ClipboardDecrypter = {
pasted: Promise<string>; pasted: Promise<string>;
}; };
export class ClipboardDecrypterImpl implements ClipboardDecrypter { export class ClipboardDecrypterImplementation implements ClipboardDecrypter {
version: number; version: number;
key: Uint8Array; key: Uint8Array;
pasted: Promise<string>; pasted: Promise<string>;

View File

@@ -13,11 +13,11 @@ import * as t from "../../../common/translation-util.js";
export const contextMenu = ( export const contextMenu = (
webContents: WebContents, webContents: WebContents,
event: Event, event: Event,
props: ContextMenuParams, properties: ContextMenuParams,
) => { ) => {
const isText = props.selectionText !== ""; const isText = properties.selectionText !== "";
const isLink = props.linkURL !== ""; const isLink = properties.linkURL !== "";
const linkUrl = isLink ? new URL(props.linkURL) : undefined; const linkUrl = isLink ? new URL(properties.linkURL) : undefined;
const makeSuggestion = (suggestion: string) => ({ const makeSuggestion = (suggestion: string) => ({
label: suggestion, label: suggestion,
@@ -30,19 +30,21 @@ export const contextMenu = (
let menuTemplate: MenuItemConstructorOptions[] = [ let menuTemplate: MenuItemConstructorOptions[] = [
{ {
label: t.__("Add to Dictionary"), label: t.__("Add to Dictionary"),
visible: props.isEditable && isText && props.misspelledWord.length > 0, visible:
properties.isEditable && isText && properties.misspelledWord.length > 0,
click(_item) { click(_item) {
webContents.session.addWordToSpellCheckerDictionary( webContents.session.addWordToSpellCheckerDictionary(
props.misspelledWord, properties.misspelledWord,
); );
}, },
}, },
{ {
type: "separator", type: "separator",
visible: props.isEditable && isText && props.misspelledWord.length > 0, visible:
properties.isEditable && isText && properties.misspelledWord.length > 0,
}, },
{ {
label: `${t.__("Look Up")} "${props.selectionText}"`, label: `${t.__("Look Up")} "${properties.selectionText}"`,
visible: process.platform === "darwin" && isText, visible: process.platform === "darwin" && isText,
click(_item) { click(_item) {
webContents.showDefinitionForSelection(); webContents.showDefinitionForSelection();
@@ -55,7 +57,7 @@ export const contextMenu = (
{ {
label: t.__("Cut"), label: t.__("Cut"),
visible: isText, visible: isText,
enabled: props.isEditable, enabled: properties.isEditable,
accelerator: "CommandOrControl+X", accelerator: "CommandOrControl+X",
click(_item) { click(_item) {
webContents.cut(); webContents.cut();
@@ -64,7 +66,7 @@ export const contextMenu = (
{ {
label: t.__("Copy"), label: t.__("Copy"),
accelerator: "CommandOrControl+C", accelerator: "CommandOrControl+C",
enabled: props.editFlags.canCopy, enabled: properties.editFlags.canCopy,
click(_item) { click(_item) {
webContents.copy(); webContents.copy();
}, },
@@ -72,7 +74,7 @@ export const contextMenu = (
{ {
label: t.__("Paste"), // Bug: Paste replaces text label: t.__("Paste"), // Bug: Paste replaces text
accelerator: "CommandOrControl+V", accelerator: "CommandOrControl+V",
enabled: props.isEditable, enabled: properties.isEditable,
click() { click() {
webContents.paste(); webContents.paste();
}, },
@@ -88,32 +90,34 @@ export const contextMenu = (
visible: isLink, visible: isLink,
click(_item) { click(_item) {
clipboard.write({ clipboard.write({
bookmark: props.linkText, bookmark: properties.linkText,
text: text:
linkUrl?.protocol === "mailto:" ? linkUrl.pathname : props.linkURL, linkUrl?.protocol === "mailto:"
? linkUrl.pathname
: properties.linkURL,
}); });
}, },
}, },
{ {
label: t.__("Copy Image"), label: t.__("Copy Image"),
visible: props.mediaType === "image", visible: properties.mediaType === "image",
click(_item) { click(_item) {
webContents.copyImageAt(props.x, props.y); webContents.copyImageAt(properties.x, properties.y);
}, },
}, },
{ {
label: t.__("Copy Image URL"), label: t.__("Copy Image URL"),
visible: props.mediaType === "image", visible: properties.mediaType === "image",
click(_item) { click(_item) {
clipboard.write({ clipboard.write({
bookmark: props.srcURL, bookmark: properties.srcURL,
text: props.srcURL, text: properties.srcURL,
}); });
}, },
}, },
{ {
type: "separator", type: "separator",
visible: isLink || props.mediaType === "image", visible: isLink || properties.mediaType === "image",
}, },
{ {
label: t.__("Services"), label: t.__("Services"),
@@ -122,10 +126,10 @@ export const contextMenu = (
}, },
]; ];
if (props.misspelledWord) { if (properties.misspelledWord) {
if (props.dictionarySuggestions.length > 0) { if (properties.dictionarySuggestions.length > 0) {
const suggestions: MenuItemConstructorOptions[] = const suggestions: MenuItemConstructorOptions[] =
props.dictionarySuggestions.map((suggestion: string) => properties.dictionarySuggestions.map((suggestion: string) =>
makeSuggestion(suggestion), makeSuggestion(suggestion),
); );
menuTemplate = [...suggestions, ...menuTemplate]; menuTemplate = [...suggestions, ...menuTemplate];

View File

@@ -1,24 +1,24 @@
import {type Html, html} from "../../../common/html.js"; import {type Html, html} from "../../../common/html.js";
import {generateNodeFromHtml} from "./base.js"; import {generateNodeFromHtml} from "./base.js";
import Tab, {type TabProps} from "./tab.js"; import Tab, {type TabProperties} from "./tab.js";
export type FunctionalTabProps = { export type FunctionalTabProperties = {
$view: Element; $view: Element;
} & TabProps; } & TabProperties;
export default class FunctionalTab extends Tab { export default class FunctionalTab extends Tab {
$view: Element; $view: Element;
$el: Element; $el: Element;
$closeButton?: Element; $closeButton?: Element;
constructor({$view, ...props}: FunctionalTabProps) { constructor({$view, ...properties}: FunctionalTabProperties) {
super(props); super(properties);
this.$view = $view; this.$view = $view;
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
if (this.props.name !== "Settings") { if (this.properties.name !== "Settings") {
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.$closeButton = this.$el.querySelector(".server-tab-badge")!; this.$closeButton = this.$el.querySelector(".server-tab-badge")!;
this.registerListeners(); this.registerListeners();
} }
@@ -41,12 +41,12 @@ export default class FunctionalTab extends Tab {
templateHtml(): Html { templateHtml(): Html {
return html` return html`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> <div class="tab functional-tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</div> </div>
<div class="server-tab"> <div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i> <i class="material-icons">${this.properties.materialIcon}</i>
</div> </div>
</div> </div>
`; `;
@@ -64,7 +64,7 @@ export default class FunctionalTab extends Tab {
}); });
this.$closeButton?.addEventListener("click", (event) => { this.$closeButton?.addEventListener("click", (event) => {
this.props.onDestroy?.(); this.properties.onDestroy?.();
event.stopPropagation(); event.stopPropagation();
}); });
} }

View File

@@ -4,12 +4,12 @@ import {type Html, html} from "../../../common/html.js";
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.js";
import {generateNodeFromHtml} from "./base.js"; import {generateNodeFromHtml} from "./base.js";
import Tab, {type TabProps} from "./tab.js"; import Tab, {type TabProperties} from "./tab.js";
import type WebView from "./webview.js"; import type WebView from "./webview.js";
export type ServerTabProps = { export type ServerTabProperties = {
webview: Promise<WebView>; webview: Promise<WebView>;
} & TabProps; } & TabProperties;
export default class ServerTab extends Tab { export default class ServerTab extends Tab {
webview: Promise<WebView>; webview: Promise<WebView>;
@@ -18,12 +18,12 @@ export default class ServerTab extends Tab {
$icon: HTMLImageElement; $icon: HTMLImageElement;
$badge: Element; $badge: Element;
constructor({webview, ...props}: ServerTabProps) { constructor({webview, ...properties}: ServerTabProperties) {
super(props); super(properties);
this.webview = webview; this.webview = webview;
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
this.$name = this.$el.querySelector(".server-tooltip")!; this.$name = this.$el.querySelector(".server-tooltip")!;
this.$icon = this.$el.querySelector(".server-icons")!; this.$icon = this.$el.querySelector(".server-icons")!;
@@ -47,13 +47,13 @@ export default class ServerTab extends Tab {
templateHtml(): Html { templateHtml(): Html {
return html` return html`
<div class="tab" data-tab-id="${this.props.tabIndex}"> <div class="tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tooltip" style="display:none"> <div class="server-tooltip" style="display:none">
${this.props.name} ${this.properties.name}
</div> </div>
<div class="server-tab-badge"></div> <div class="server-tab-badge"></div>
<div class="server-tab"> <div class="server-tab">
<img class="server-icons" src="${this.props.icon}" /> <img class="server-icons" src="${this.properties.icon}" />
</div> </div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div> <div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div> </div>
@@ -61,12 +61,12 @@ export default class ServerTab extends Tab {
} }
setName(name: string): void { setName(name: string): void {
this.props.name = name; this.properties.name = name;
this.$name.textContent = name; this.$name.textContent = name;
} }
setIcon(icon: string): void { setIcon(icon: string): void {
this.props.icon = icon; this.properties.icon = icon;
this.$icon.src = icon; this.$icon.src = icon;
} }
@@ -77,11 +77,11 @@ export default class ServerTab extends Tab {
generateShortcutText(): string { generateShortcutText(): string {
// Only provide shortcuts for server [0..9] // Only provide shortcuts for server [0..9]
if (this.props.index >= 9) { if (this.properties.index >= 9) {
return ""; return "";
} }
const shownIndex = this.props.index + 1; const shownIndex = this.properties.index + 1;
// Array index == Shown index - 1 // Array index == Shown index - 1
ipcRenderer.send("switch-server-tab", shownIndex - 1); ipcRenderer.send("switch-server-tab", shownIndex - 1);

View File

@@ -1,6 +1,6 @@
import type {TabRole} from "../../../common/types.js"; import type {TabRole} from "../../../common/types.js";
export type TabProps = { export type TabProperties = {
role: TabRole; role: TabRole;
icon?: string; icon?: string;
name: string; name: string;
@@ -17,17 +17,17 @@ export type TabProps = {
export default abstract class Tab { export default abstract class Tab {
abstract $el: Element; abstract $el: Element;
constructor(readonly props: TabProps) {} constructor(readonly properties: TabProperties) {}
registerListeners(): void { registerListeners(): void {
this.$el.addEventListener("click", this.props.onClick); this.$el.addEventListener("click", this.properties.onClick);
if (this.props.onHover !== undefined) { if (this.properties.onHover !== undefined) {
this.$el.addEventListener("mouseover", this.props.onHover); this.$el.addEventListener("mouseover", this.properties.onHover);
} }
if (this.props.onHoverOut !== undefined) { if (this.properties.onHoverOut !== undefined) {
this.$el.addEventListener("mouseout", this.props.onHoverOut); this.$el.addEventListener("mouseout", this.properties.onHoverOut);
} }
} }

View File

@@ -18,7 +18,7 @@ import {contextMenu} from "./context-menu.js";
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false); const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
type WebViewProps = { type WebViewProperties = {
$root: Element; $root: Element;
rootWebContents: WebContents; rootWebContents: WebContents;
index: number; index: number;
@@ -35,24 +35,24 @@ type WebViewProps = {
}; };
export default class WebView { export default class WebView {
static templateHtml(props: WebViewProps): Html { static templateHtml(properties: WebViewProperties): Html {
return html` return html`
<div class="webview-pane"> <div class="webview-pane">
<div <div
class="webview-unsupported" class="webview-unsupported"
${props.unsupportedMessage === undefined ? html`hidden` : html``} ${properties.unsupportedMessage === undefined ? html`hidden` : html``}
> >
<span class="webview-unsupported-message" <span class="webview-unsupported-message"
>${props.unsupportedMessage ?? ""}</span >${properties.unsupportedMessage ?? ""}</span
> >
<span class="webview-unsupported-dismiss">×</span> <span class="webview-unsupported-dismiss">×</span>
</div> </div>
<webview <webview
data-tab-id="${props.tabIndex}" data-tab-id="${properties.tabIndex}"
src="${props.url}" src="${properties.url}"
${props.preload === undefined ${properties.preload === undefined
? html`` ? html``
: html`preload="${props.preload}"`} : html`preload="${properties.preload}"`}
partition="persist:webviewsession" partition="persist:webviewsession"
allowpopups allowpopups
> >
@@ -61,11 +61,11 @@ export default class WebView {
`; `;
} }
static async create(props: WebViewProps): Promise<WebView> { static async create(properties: WebViewProperties): Promise<WebView> {
const $pane = generateNodeFromHtml( const $pane = generateNodeFromHtml(
WebView.templateHtml(props), WebView.templateHtml(properties),
) as HTMLElement; ) as HTMLElement;
props.$root.append($pane); properties.$root.append($pane);
const $webview: HTMLElement = $pane.querySelector(":scope > webview")!; const $webview: HTMLElement = $pane.querySelector(":scope > webview")!;
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@@ -89,17 +89,17 @@ export default class WebView {
} }
const selector = `webview[data-tab-id="${CSS.escape( const selector = `webview[data-tab-id="${CSS.escape(
`${props.tabIndex}`, `${properties.tabIndex}`,
)}"]`; )}"]`;
const webContentsId: unknown = const webContentsId: unknown =
await props.rootWebContents.executeJavaScript( await properties.rootWebContents.executeJavaScript(
`(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`, `(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`,
); );
if (typeof webContentsId !== "number") { if (typeof webContentsId !== "number") {
throw new TypeError("Failed to get WebContents ID"); throw new TypeError("Failed to get WebContents ID");
} }
return new WebView(props, $pane, $webview, webContentsId); return new WebView(properties, $pane, $webview, webContentsId);
} }
badgeCount = 0; badgeCount = 0;
@@ -112,7 +112,7 @@ export default class WebView {
private unsupportedDismissed = false; private unsupportedDismissed = false;
private constructor( private constructor(
readonly props: WebViewProps, readonly properties: WebViewProperties,
private readonly $pane: HTMLElement, private readonly $pane: HTMLElement,
private readonly $webview: HTMLElement, private readonly $webview: HTMLElement,
readonly webContentsId: number, readonly webContentsId: number,
@@ -207,7 +207,7 @@ export default class WebView {
// Shows the loading indicator till the webview is reloaded // Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove("loaded"); this.$webviewsContainer.remove("loaded");
this.loading = true; this.loading = true;
this.props.switchLoading(true, this.props.url); this.properties.switchLoading(true, this.properties.url);
this.getWebContents().reload(); this.getWebContents().reload();
} }
@@ -219,9 +219,9 @@ export default class WebView {
send<Channel extends keyof RendererMessage>( send<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
ipcRenderer.send("forward-to", this.webContentsId, channel, ...args); ipcRenderer.send("forward-to", this.webContentsId, channel, ...arguments_);
} }
private registerListeners(): void { private registerListeners(): void {
@@ -233,7 +233,7 @@ export default class WebView {
webContents.on("page-title-updated", (_event, title) => { webContents.on("page-title-updated", (_event, title) => {
this.badgeCount = this.getBadgeCount(title); this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange(); this.properties.onTitleChange();
}); });
this.$webview.addEventListener("did-navigate-in-page", () => { this.$webview.addEventListener("did-navigate-in-page", () => {
@@ -266,7 +266,7 @@ export default class WebView {
this.$webview.addEventListener("dom-ready", () => { this.$webview.addEventListener("dom-ready", () => {
this.loading = false; this.loading = false;
this.props.switchLoading(false, this.props.url); this.properties.switchLoading(false, this.properties.url);
this.show(); this.show();
}); });
@@ -275,18 +275,18 @@ export default class WebView {
SystemUtil.connectivityError.includes(errorDescription); SystemUtil.connectivityError.includes(errorDescription);
if (hasConnectivityError) { if (hasConnectivityError) {
console.error("error", errorDescription); console.error("error", errorDescription);
if (!this.props.url.includes("network.html")) { if (!this.properties.url.includes("network.html")) {
this.props.onNetworkError(this.props.index); this.properties.onNetworkError(this.properties.index);
} }
} }
}); });
this.$webview.addEventListener("did-start-loading", () => { this.$webview.addEventListener("did-start-loading", () => {
this.props.switchLoading(true, this.props.url); this.properties.switchLoading(true, this.properties.url);
}); });
this.$webview.addEventListener("did-stop-loading", () => { this.$webview.addEventListener("did-stop-loading", () => {
this.props.switchLoading(false, this.props.url); this.properties.switchLoading(false, this.properties.url);
}); });
this.$unsupportedDismiss.addEventListener("click", () => { this.$unsupportedDismiss.addEventListener("click", () => {
@@ -307,7 +307,7 @@ export default class WebView {
private show(): void { private show(): void {
// Do not show WebView if another tab was selected and this tab should be in background. // Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) { if (!this.properties.isActive()) {
return; return;
} }
@@ -316,7 +316,7 @@ export default class WebView {
this.$pane.classList.add("active"); this.$pane.classList.add("active");
this.focus(); this.focus();
this.props.onTitleChange(); this.properties.onTitleChange();
// Injecting preload css in webview to override some css rules // Injecting preload css in webview to override some css rules
(async () => this.getWebContents().insertCSS(preloadCss))(); (async () => this.getWebContents().insertCSS(preloadCss))();

View File

@@ -2,16 +2,16 @@ import {EventEmitter} from "node:events";
import { import {
type ClipboardDecrypter, type ClipboardDecrypter,
ClipboardDecrypterImpl, ClipboardDecrypterImplementation,
} from "./clipboard-decrypter.js"; } from "./clipboard-decrypter.js";
import {type NotificationData, newNotification} from "./notification/index.js"; import {type NotificationData, newNotification} from "./notification/index.js";
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {ipcRenderer} from "./typed-ipc-renderer.js";
type ListenerType = (...args: any[]) => void; type ListenerType = (...arguments_: any[]) => void;
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
export type ElectronBridge = { export type ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]) => boolean; send_event: (eventName: string | symbol, ...arguments_: unknown[]) => boolean;
on_event: (eventName: string, listener: ListenerType) => void; on_event: (eventName: string, listener: ListenerType) => void;
new_notification: ( new_notification: (
title: string, title: string,
@@ -36,8 +36,8 @@ export const bridgeEvents = new EventEmitter(); // eslint-disable-line unicorn/p
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
const electron_bridge: ElectronBridge = { const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]): boolean => send_event: (eventName: string | symbol, ...arguments_: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...args), bridgeEvents.emit(eventName, ...arguments_),
on_event(eventName: string, listener: ListenerType): void { on_event(eventName: string, listener: ListenerType): void {
bridgeEvents.on(eventName, listener); bridgeEvents.on(eventName, listener);
@@ -61,7 +61,7 @@ const electron_bridge: ElectronBridge = {
}, },
decrypt_clipboard: (version: number): ClipboardDecrypter => decrypt_clipboard: (version: number): ClipboardDecrypter =>
new ClipboardDecrypterImpl(version), new ClipboardDecrypterImplementation(version),
}; };
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View File

@@ -16,7 +16,11 @@ import * as LinkUtil from "../../common/link-util.js";
import Logger from "../../common/logger-util.js"; import Logger from "../../common/logger-util.js";
import * as Messages from "../../common/messages.js"; import * as Messages from "../../common/messages.js";
import {bundlePath, bundleUrl} from "../../common/paths.js"; import {bundlePath, bundleUrl} from "../../common/paths.js";
import type {NavItem, ServerConf, TabData} from "../../common/types.js"; import type {
NavigationItem,
ServerConfig,
TabData,
} from "../../common/types.js";
import defaultIcon from "../img/icon.png"; import defaultIcon from "../img/icon.png";
import FunctionalTab from "./components/functional-tab.js"; import FunctionalTab from "./components/functional-tab.js";
@@ -248,8 +252,8 @@ export class ServerManagerView {
// promise of addition resolves in both cases, but we consider it rejected // promise of addition resolves in both cases, but we consider it rejected
// if the resolved value is false // if the resolved value is false
try { try {
const serverConf = await DomainUtil.checkDomain(domain); const serverConfig = await DomainUtil.checkDomain(domain);
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConfig);
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
logger.error(error); logger.error(error);
@@ -325,11 +329,14 @@ export class ServerManagerView {
for (const [i, server] of servers.entries()) { for (const [i, server] of servers.entries()) {
const tab = this.initServer(server, i); const tab = this.initServer(server, i);
(async () => { (async () => {
const serverConf = await DomainUtil.updateSavedServer(server.url, i); const serverConfig = await DomainUtil.updateSavedServer(
tab.setName(serverConf.alias); server.url,
tab.setIcon(DomainUtil.iconAsUrl(serverConf.icon)); i,
);
tab.setName(serverConfig.alias);
tab.setIcon(DomainUtil.iconAsUrl(serverConfig.icon));
(await tab.webview).setUnsupportedMessage( (await tab.webview).setUnsupportedMessage(
DomainUtil.getUnsupportedMessage(serverConf), DomainUtil.getUnsupportedMessage(serverConfig),
); );
})(); })();
} }
@@ -364,7 +371,7 @@ export class ServerManagerView {
} }
} }
initServer(server: ServerConf, index: number): ServerTab { initServer(server: ServerConfig, index: number): ServerTab {
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
const tab = new ServerTab({ const tab = new ServerTab({
role: "server", role: "server",
@@ -398,7 +405,7 @@ export class ServerManagerView {
const tab = this.tabs[this.activeTabIndex]; const tab = this.tabs[this.activeTabIndex];
this.showLoading( this.showLoading(
tab instanceof ServerTab && tab instanceof ServerTab &&
this.loading.has((await tab.webview).props.url), this.loading.has((await tab.webview).properties.url),
); );
}, },
onNetworkError: async (index: number) => { onNetworkError: async (index: number) => {
@@ -481,7 +488,7 @@ export class ServerManagerView {
async getCurrentActiveServer(): Promise<string> { async getCurrentActiveServer(): Promise<string> {
const tab = this.tabs[this.activeTabIndex]; const tab = this.tabs[this.activeTabIndex];
return tab instanceof ServerTab ? (await tab.webview).props.url : ""; return tab instanceof ServerTab ? (await tab.webview).properties.url : "";
} }
displayInitialCharLogo($img: HTMLImageElement, index: number): void { displayInitialCharLogo($img: HTMLImageElement, index: number): void {
@@ -550,36 +557,36 @@ export class ServerManagerView {
this.$serverIconTooltip[index].style.display = "none"; this.$serverIconTooltip[index].style.display = "none";
} }
async openFunctionalTab(tabProps: { async openFunctionalTab(tabProperties: {
name: string; name: string;
materialIcon: string; materialIcon: string;
makeView: () => Promise<Element>; makeView: () => Promise<Element>;
destroyView: () => void; destroyView: () => void;
}): Promise<void> { }): Promise<void> {
if (this.functionalTabs.has(tabProps.name)) { if (this.functionalTabs.has(tabProperties.name)) {
await this.activateTab(this.functionalTabs.get(tabProps.name)!); await this.activateTab(this.functionalTabs.get(tabProperties.name)!);
return; return;
} }
const index = this.tabs.length; const index = this.tabs.length;
this.functionalTabs.set(tabProps.name, index); this.functionalTabs.set(tabProperties.name, index);
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
const $view = await tabProps.makeView(); const $view = await tabProperties.makeView();
this.$webviewsContainer.append($view); this.$webviewsContainer.append($view);
this.tabs.push( this.tabs.push(
new FunctionalTab({ new FunctionalTab({
role: "function", role: "function",
materialIcon: tabProps.materialIcon, materialIcon: tabProperties.materialIcon,
name: tabProps.name, name: tabProperties.name,
$root: this.$tabsContainer, $root: this.$tabsContainer,
index, index,
tabIndex, tabIndex,
onClick: this.activateTab.bind(this, index), onClick: this.activateTab.bind(this, index),
onDestroy: async () => { onDestroy: async () => {
await this.destroyTab(tabProps.name, index); await this.destroyTab(tabProperties.name, index);
tabProps.destroyView(); tabProperties.destroyView();
}, },
$view, $view,
}), }),
@@ -589,10 +596,12 @@ export class ServerManagerView {
// closed when the functional tab DOM is ready, handled in webview.js // closed when the functional tab DOM is ready, handled in webview.js
this.$webviewsContainer.classList.remove("loaded"); this.$webviewsContainer.classList.remove("loaded");
await this.activateTab(this.functionalTabs.get(tabProps.name)!); await this.activateTab(this.functionalTabs.get(tabProperties.name)!);
} }
async openSettings(nav: NavItem = "General"): Promise<void> { async openSettings(
navigationItem: NavigationItem = "General",
): Promise<void> {
await this.openFunctionalTab({ await this.openFunctionalTab({
name: "Settings", name: "Settings",
materialIcon: "settings", materialIcon: "settings",
@@ -607,7 +616,7 @@ export class ServerManagerView {
}, },
}); });
this.$settingsButton.classList.add("active"); this.$settingsButton.classList.add("active");
this.preferenceView!.handleNavigation(nav); this.preferenceView!.handleNavigation(navigationItem);
} }
async openAbout(): Promise<void> { async openAbout(): Promise<void> {
@@ -646,13 +655,13 @@ export class ServerManagerView {
// Returns this.tabs in an way that does // Returns this.tabs in an way that does
// not crash app when this.tabs is passed into // not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview // ipcRenderer. Something about webview, and properties.webview
// properties in ServerTab causes the app to crash. // properties in ServerTab causes the app to crash.
get tabsForIpc(): TabData[] { get tabsForIpc(): TabData[] {
return this.tabs.map((tab) => ({ return this.tabs.map((tab) => ({
role: tab.props.role, role: tab.properties.role,
name: tab.props.name, name: tab.properties.name,
index: tab.props.index, index: tab.properties.index,
})); }));
} }
@@ -670,8 +679,8 @@ export class ServerManagerView {
if (hideOldTab) { if (hideOldTab) {
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom // If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom
if ( if (
this.tabs[this.activeTabIndex].props.role === "function" && this.tabs[this.activeTabIndex].properties.role === "function" &&
this.tabs[this.activeTabIndex].props.name === "Settings" this.tabs[this.activeTabIndex].properties.name === "Settings"
) { ) {
this.$settingsButton.classList.remove("active"); this.$settingsButton.classList.remove("active");
} }
@@ -695,7 +704,7 @@ export class ServerManagerView {
this.showLoading( this.showLoading(
tab instanceof ServerTab && tab instanceof ServerTab &&
this.loading.has((await tab.webview).props.url), this.loading.has((await tab.webview).properties.url),
); );
ipcRenderer.send("update-menu", { ipcRenderer.send("update-menu", {
@@ -704,7 +713,7 @@ export class ServerManagerView {
tabs: this.tabsForIpc, tabs: this.tabsForIpc,
activeTabIndex: this.activeTabIndex, activeTabIndex: this.activeTabIndex,
// Following flag controls whether a menu item should be enabled or not // Following flag controls whether a menu item should be enabled or not
enableMenu: tab.props.role === "server", enableMenu: tab.properties.role === "server",
}); });
} }
@@ -746,7 +755,7 @@ export class ServerManagerView {
async reloadView(): Promise<void> { async reloadView(): Promise<void> {
// Save and remember the index of last active tab so that we can use it later // Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index; const lastActiveTab = this.tabs[this.activeTabIndex].properties.index;
ConfigUtil.setConfigItem("lastActiveTab", lastActiveTab); ConfigUtil.setConfigItem("lastActiveTab", lastActiveTab);
// Destroy the current view and re-initiate it // Destroy the current view and re-initiate it
@@ -946,7 +955,7 @@ export class ServerManagerView {
const webview = await tab.webview; const webview = await tab.webview;
return ( return (
webview.webContentsId === webContentsId && webview.webContentsId === webContentsId &&
webview.props.hasPermission?.(origin, permission) webview.properties.hasPermission?.(origin, permission)
); );
}), }),
) )
@@ -1092,7 +1101,7 @@ export class ServerManagerView {
(await tab.webview).webContentsId === webviewId (await tab.webview).webContentsId === webviewId
) { ) {
const concurrentTab: HTMLButtonElement = document.querySelector( const concurrentTab: HTMLButtonElement = document.querySelector(
`div[data-tab-id="${CSS.escape(`${tab.props.tabIndex}`)}"]`, `div[data-tab-id="${CSS.escape(`${tab.properties.tabIndex}`)}"]`,
)!; )!;
concurrentTab.click(); concurrentTab.click();
} }
@@ -1107,22 +1116,22 @@ export class ServerManagerView {
canvas.height = 128; canvas.height = 128;
canvas.width = 128; canvas.width = 128;
canvas.style.letterSpacing = "-5px"; canvas.style.letterSpacing = "-5px";
const ctx = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
ctx.fillStyle = "#f42020"; context.fillStyle = "#f42020";
ctx.beginPath(); context.beginPath();
ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI); context.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
ctx.fill(); context.fill();
ctx.textAlign = "center"; context.textAlign = "center";
ctx.fillStyle = "white"; context.fillStyle = "white";
if (messageCount > 99) { if (messageCount > 99) {
ctx.font = "65px Helvetica"; context.font = "65px Helvetica";
ctx.fillText("99+", 64, 85); context.fillText("99+", 64, 85);
} else if (messageCount < 10) { } else if (messageCount < 10) {
ctx.font = "90px Helvetica"; context.font = "90px Helvetica";
ctx.fillText(String(Math.min(99, messageCount)), 64, 96); context.fillText(String(Math.min(99, messageCount)), 64, 96);
} else { } else {
ctx.font = "85px Helvetica"; context.font = "85px Helvetica";
ctx.fillText(String(Math.min(99, messageCount)), 64, 90); context.fillText(String(Math.min(99, messageCount)), 64, 90);
} }
return canvas; return canvas;

View File

@@ -18,10 +18,10 @@ export function newNotification(
): NotificationData { ): NotificationData {
const notification = new Notification(title, {...options, silent: true}); const notification = new Notification(title, {...options, silent: true});
for (const type of ["click", "close", "error", "show"]) { for (const type of ["click", "close", "error", "show"]) {
notification.addEventListener(type, (ev) => { notification.addEventListener(type, (event) => {
if (type === "click") ipcRenderer.send("focus-this-webview"); if (type === "click") ipcRenderer.send("focus-this-webview");
if (!dispatch(type, ev)) { if (!dispatch(type, event)) {
ev.preventDefault(); event.preventDefault();
} }
}); });
} }

View File

@@ -2,15 +2,15 @@ import {type Html, html} from "../../../../common/html.js";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.js";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.js";
type BaseSectionProps = { type BaseSectionProperties = {
$element: HTMLElement; $element: HTMLElement;
disabled?: boolean; disabled?: boolean;
value: boolean; value: boolean;
clickHandler: () => void; clickHandler: () => void;
}; };
export function generateSettingOption(props: BaseSectionProps): void { export function generateSettingOption(properties: BaseSectionProperties): void {
const {$element, disabled, value, clickHandler} = props; const {$element, disabled, value, clickHandler} = properties;
$element.textContent = ""; $element.textContent = "";

View File

@@ -7,13 +7,13 @@ import {reloadApp} from "./base-section.js";
import {initFindAccounts} from "./find-accounts.js"; import {initFindAccounts} from "./find-accounts.js";
import {initServerInfoForm} from "./server-info-form.js"; import {initServerInfoForm} from "./server-info-form.js";
type ConnectedOrgSectionProps = { type ConnectedOrgSectionProperties = {
$root: Element; $root: Element;
}; };
export function initConnectedOrgSection({ export function initConnectedOrgSection({
$root, $root,
}: ConnectedOrgSectionProps): void { }: ConnectedOrgSectionProperties): void {
$root.textContent = ""; $root.textContent = "";
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();

View File

@@ -3,7 +3,7 @@ import * as LinkUtil from "../../../../common/link-util.js";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.js";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.js";
type FindAccountsProps = { type FindAccountsProperties = {
$root: Element; $root: Element;
}; };
@@ -19,7 +19,7 @@ async function findAccounts(url: string): Promise<void> {
await LinkUtil.openBrowser(new URL("/accounts/find", url)); await LinkUtil.openBrowser(new URL("/accounts/find", url));
} }
export function initFindAccounts(props: FindAccountsProps): void { export function initFindAccounts(properties: FindAccountsProperties): void {
const $findAccounts = generateNodeFromHtml(html` const $findAccounts = generateNodeFromHtml(html`
<div class="settings-card certificate-card"> <div class="settings-card certificate-card">
<div class="certificate-input"> <div class="certificate-input">
@@ -33,7 +33,7 @@ export function initFindAccounts(props: FindAccountsProps): void {
</div> </div>
</div> </div>
`); `);
props.$root.append($findAccounts); properties.$root.append($findAccounts);
const $findAccountsButton = $findAccounts.querySelector( const $findAccountsButton = $findAccounts.querySelector(
"#find-accounts-button", "#find-accounts-button",
)!; )!;

View File

@@ -20,11 +20,11 @@ import {generateSelectHtml, generateSettingOption} from "./base-section.js";
const currentBrowserWindow = remote.getCurrentWindow(); const currentBrowserWindow = remote.getCurrentWindow();
type GeneralSectionProps = { type GeneralSectionProperties = {
$root: Element; $root: Element;
}; };
export function initGeneralSection({$root}: GeneralSectionProps): void { export function initGeneralSection({$root}: GeneralSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__("Appearance")}</div> <div class="title">${t.__("Appearance")}</div>

View File

@@ -1,18 +1,18 @@
import {type Html, html} from "../../../../common/html.js"; import {type Html, html} from "../../../../common/html.js";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.js";
import type {NavItem} from "../../../../common/types.js"; import type {NavigationItem} from "../../../../common/types.js";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.js";
type PreferenceNavProps = { type PreferenceNavigationProperties = {
$root: Element; $root: Element;
onItemSelected: (navItem: NavItem) => void; onItemSelected: (navigationItem: NavigationItem) => void;
}; };
export default class PreferenceNav { export default class PreferenceNavigation {
navItems: NavItem[]; navigationItems: NavigationItem[];
$el: Element; $el: Element;
constructor(private readonly props: PreferenceNavProps) { constructor(private readonly properties: PreferenceNavigationProperties) {
this.navItems = [ this.navigationItems = [
"General", "General",
"Network", "Network",
"AddServer", "AddServer",
@@ -21,15 +21,17 @@ export default class PreferenceNav {
]; ];
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
} }
templateHtml(): Html { templateHtml(): Html {
const navItemsHtml = html``.join( const navigationItemsHtml = html``.join(
this.navItems.map( this.navigationItems.map(
(navItem) => html` (navigationItem) => html`
<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div> <div class="nav" id="nav-${navigationItem}">
${t.__(navigationItem)}
</div>
`, `,
), ),
); );
@@ -37,37 +39,39 @@ export default class PreferenceNav {
return html` return html`
<div> <div>
<div id="settings-header">${t.__("Settings")}</div> <div id="settings-header">${t.__("Settings")}</div>
<div id="nav-container">${navItemsHtml}</div> <div id="nav-container">${navigationItemsHtml}</div>
</div> </div>
`; `;
} }
registerListeners(): void { registerListeners(): void {
for (const navItem of this.navItems) { for (const navigationItem of this.navigationItems) {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(
`#nav-${CSS.escape(navigationItem)}`,
)!;
$item.addEventListener("click", () => { $item.addEventListener("click", () => {
this.props.onItemSelected(navItem); this.properties.onItemSelected(navigationItem);
}); });
} }
} }
select(navItemToSelect: NavItem): void { select(navigationItemToSelect: NavigationItem): void {
for (const navItem of this.navItems) { for (const navigationItem of this.navigationItems) {
if (navItem === navItemToSelect) { if (navigationItem === navigationItemToSelect) {
this.activate(navItem); this.activate(navigationItem);
} else { } else {
this.deactivate(navItem); this.deactivate(navigationItem);
} }
} }
} }
activate(navItem: NavItem): void { activate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.add("active"); $item.classList.add("active");
} }
deactivate(navItem: NavItem): void { deactivate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.remove("active"); $item.classList.remove("active");
} }
} }

View File

@@ -5,11 +5,11 @@ import {ipcRenderer} from "../../typed-ipc-renderer.js";
import {generateSettingOption} from "./base-section.js"; import {generateSettingOption} from "./base-section.js";
type NetworkSectionProps = { type NetworkSectionProperties = {
$root: Element; $root: Element;
}; };
export function initNetworkSection({$root}: NetworkSectionProps): void { export function initNetworkSection({$root}: NetworkSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__("Proxy")}</div> <div class="title">${t.__("Proxy")}</div>

View File

@@ -7,12 +7,15 @@ import {generateNodeFromHtml} from "../../components/base.js";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.js";
import * as DomainUtil from "../../utils/domain-util.js"; import * as DomainUtil from "../../utils/domain-util.js";
type NewServerFormProps = { type NewServerFormProperties = {
$root: Element; $root: Element;
onChange: () => void; onChange: () => void;
}; };
export function initNewServerForm({$root, onChange}: NewServerFormProps): void { export function initNewServerForm({
$root,
onChange,
}: NewServerFormProperties): void {
const $newServerForm = generateNodeFromHtml(html` const $newServerForm = generateNodeFromHtml(html`
<div class="server-input-container"> <div class="server-input-container">
<div class="title">${t.__("Organization URL")}</div> <div class="title">${t.__("Organization URL")}</div>
@@ -58,9 +61,9 @@ export function initNewServerForm({$root, onChange}: NewServerFormProps): void {
async function submitFormHandler(): Promise<void> { async function submitFormHandler(): Promise<void> {
$saveServerButton.textContent = "Connecting..."; $saveServerButton.textContent = "Connecting...";
let serverConf; let serverConfig;
try { try {
serverConf = await DomainUtil.checkDomain($newServerUrl.value.trim()); serverConfig = await DomainUtil.checkDomain($newServerUrl.value.trim());
} catch (error: unknown) { } catch (error: unknown) {
$saveServerButton.textContent = "Connect"; $saveServerButton.textContent = "Connect";
await dialog.showMessageBox({ await dialog.showMessageBox({
@@ -74,7 +77,7 @@ export function initNewServerForm({$root, onChange}: NewServerFormProps): void {
return; return;
} }
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConfig);
onChange(); onChange();
} }

View File

@@ -3,7 +3,7 @@ import process from "node:process";
import type {DndSettings} from "../../../../common/dnd-util.js"; import type {DndSettings} from "../../../../common/dnd-util.js";
import {bundleUrl} from "../../../../common/paths.js"; import {bundleUrl} from "../../../../common/paths.js";
import type {NavItem} from "../../../../common/types.js"; import type {NavigationItem} from "../../../../common/types.js";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.js";
import {initConnectedOrgSection} from "./connected-org-section.js"; import {initConnectedOrgSection} from "./connected-org-section.js";
@@ -26,7 +26,7 @@ export class PreferenceView {
private readonly $shadow: ShadowRoot; private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element; private readonly $settingsContainer: Element;
private readonly nav: Nav; private readonly nav: Nav;
private navItem: NavItem = "General"; private navigationItem: NavigationItem = "General";
private constructor(templateHtml: string) { private constructor(templateHtml: string) {
this.$view = document.createElement("div"); this.$view = document.createElement("div");
@@ -47,13 +47,13 @@ export class PreferenceView {
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar); ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.on("toggle-dnd", this.handleToggleDnd); ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
this.handleNavigation(this.navItem); this.handleNavigation(this.navigationItem);
} }
handleNavigation = (navItem: NavItem): void => { handleNavigation = (navigationItem: NavigationItem): void => {
this.navItem = navItem; this.navigationItem = navigationItem;
this.nav.select(navItem); this.nav.select(navigationItem);
switch (navItem) { switch (navigationItem) {
case "AddServer": { case "AddServer": {
initServersSection({ initServersSection({
$root: this.$settingsContainer, $root: this.$settingsContainer,
@@ -90,11 +90,11 @@ export class PreferenceView {
} }
default: { default: {
((n: never) => n)(navItem); ((n: never) => n)(navigationItem);
} }
} }
window.location.hash = `#${navItem}`; window.location.hash = `#${navigationItem}`;
}; };
handleToggleTray(state: boolean) { handleToggleTray(state: boolean) {

View File

@@ -3,35 +3,35 @@ import {dialog} from "@electron/remote";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.js";
import * as Messages from "../../../../common/messages.js"; import * as Messages from "../../../../common/messages.js";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.js";
import type {ServerConf} from "../../../../common/types.js"; import type {ServerConfig} from "../../../../common/types.js";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.js";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.js";
import * as DomainUtil from "../../utils/domain-util.js"; import * as DomainUtil from "../../utils/domain-util.js";
type ServerInfoFormProps = { type ServerInfoFormProperties = {
$root: Element; $root: Element;
server: ServerConf; server: ServerConfig;
index: number; index: number;
onChange: () => void; onChange: () => void;
}; };
export function initServerInfoForm(props: ServerInfoFormProps): void { export function initServerInfoForm(properties: ServerInfoFormProperties): void {
const $serverInfoForm = generateNodeFromHtml(html` const $serverInfoForm = generateNodeFromHtml(html`
<div class="settings-card"> <div class="settings-card">
<div class="server-info-left"> <div class="server-info-left">
<img <img
class="server-info-icon" class="server-info-icon"
src="${DomainUtil.iconAsUrl(props.server.icon)}" src="${DomainUtil.iconAsUrl(properties.server.icon)}"
/> />
<div class="server-info-row"> <div class="server-info-row">
<span class="server-info-alias">${props.server.alias}</span> <span class="server-info-alias">${properties.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i> <i class="material-icons open-tab-button">open_in_new</i>
</div> </div>
</div> </div>
<div class="server-info-right"> <div class="server-info-right">
<div class="server-info-row server-url"> <div class="server-info-row server-url">
<span class="server-url-info" title="${props.server.url}" <span class="server-url-info" title="${properties.server.url}"
>${props.server.url}</span >${properties.server.url}</span
> >
</div> </div>
<div class="server-info-row"> <div class="server-info-row">
@@ -48,7 +48,7 @@ export function initServerInfoForm(props: ServerInfoFormProps): void {
".server-delete-action", ".server-delete-action",
)!; )!;
const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!; const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!;
props.$root.append($serverInfoForm); properties.$root.append($serverInfoForm);
$deleteServerButton.addEventListener("click", async () => { $deleteServerButton.addEventListener("click", async () => {
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
@@ -58,11 +58,11 @@ export function initServerInfoForm(props: ServerInfoFormProps): void {
message: t.__("Are you sure you want to disconnect this organization?"), message: t.__("Are you sure you want to disconnect this organization?"),
}); });
if (response === 0) { if (response === 0) {
if (DomainUtil.removeDomain(props.index)) { if (DomainUtil.removeDomain(properties.index)) {
ipcRenderer.send("reload-full-app"); ipcRenderer.send("reload-full-app");
} else { } else {
const {title, content} = Messages.orgRemovalError( const {title, content} = Messages.orgRemovalError(
DomainUtil.getDomain(props.index).url, DomainUtil.getDomain(properties.index).url,
); );
dialog.showErrorBox(title, content); dialog.showErrorBox(title, content);
} }
@@ -70,14 +70,14 @@ export function initServerInfoForm(props: ServerInfoFormProps): void {
}); });
$openServerButton.addEventListener("click", () => { $openServerButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
$serverInfoAlias.addEventListener("click", () => { $serverInfoAlias.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
$serverIcon.addEventListener("click", () => { $serverIcon.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
} }

View File

@@ -4,11 +4,11 @@ import * as t from "../../../../common/translation-util.js";
import {reloadApp} from "./base-section.js"; import {reloadApp} from "./base-section.js";
import {initNewServerForm} from "./new-server-form.js"; import {initNewServerForm} from "./new-server-form.js";
type ServersSectionProps = { type ServersSectionProperties = {
$root: Element; $root: Element;
}; };
export function initServersSection({$root}: ServersSectionProps): void { export function initServersSection({$root}: ServersSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="add-server-modal"> <div class="add-server-modal">
<div class="modal-container"> <div class="modal-container">

View File

@@ -4,12 +4,14 @@ import {html} from "../../../../common/html.js";
import * as LinkUtil from "../../../../common/link-util.js"; import * as LinkUtil from "../../../../common/link-util.js";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.js";
type ShortcutsSectionProps = { type ShortcutsSectionProperties = {
$root: Element; $root: Element;
}; };
// eslint-disable-next-line complexity // eslint-disable-next-line complexity
export function initShortcutsSection({$root}: ShortcutsSectionProps): void { export function initShortcutsSection({
$root,
}: ShortcutsSectionProperties): void {
const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl"; const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl";
$root.innerHTML = html` $root.innerHTML = html`

View File

@@ -63,8 +63,8 @@ const config = {
thick: process.platform === "win32", thick: process.platform === "win32",
}; };
const renderCanvas = function (arg: number): HTMLCanvasElement { const renderCanvas = function (argument: number): HTMLCanvasElement {
config.unreadCount = arg; config.unreadCount = argument;
const size = config.size * config.pixelRatio; const size = config.size * config.pixelRatio;
const padding = size * 0.05; const padding = size * 0.05;
@@ -78,30 +78,34 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
const ctx = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
// Circle // Circle
// If (!config.thick || config.thick && hasCount) { // If (!config.thick || config.thick && hasCount) {
ctx.beginPath(); context.beginPath();
ctx.arc(center, center, size / 2 - padding, 0, 2 * Math.PI, false); context.arc(center, center, size / 2 - padding, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor; context.fillStyle = backgroundColor;
ctx.fill(); context.fill();
ctx.lineWidth = size / (config.thick ? 10 : 20); context.lineWidth = size / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor; context.strokeStyle = backgroundColor;
ctx.stroke(); context.stroke();
// Count or Icon // Count or Icon
if (hasCount) { if (hasCount) {
ctx.fillStyle = color; context.fillStyle = color;
ctx.textAlign = "center"; context.textAlign = "center";
if (config.unreadCount > 99) { if (config.unreadCount > 99) {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`;
ctx.fillText("99+", center, center + size * 0.15); context.fillText("99+", center, center + size * 0.15);
} else if (config.unreadCount < 10) { } else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), center, center + size * 0.2); context.fillText(String(config.unreadCount), center, center + size * 0.2);
} else { } else {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), center, center + size * 0.15); context.fillText(
String(config.unreadCount),
center,
center + size * 0.15,
);
} }
} }
@@ -113,12 +117,12 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
* @param arg: Unread count * @param arg: Unread count
* @return the native image * @return the native image
*/ */
const renderNativeImage = function (arg: number): NativeImage { const renderNativeImage = function (argument: number): NativeImage {
if (process.platform === "win32") { if (process.platform === "win32") {
return nativeImage.createFromPath(winUnreadTrayIconPath()); return nativeImage.createFromPath(winUnreadTrayIconPath());
} }
const canvas = renderCanvas(arg); const canvas = renderCanvas(argument);
const pngData = nativeImage const pngData = nativeImage
.createFromDataURL(canvas.toDataURL("image/png")) .createFromDataURL(canvas.toDataURL("image/png"))
.toPNG(); .toPNG();
@@ -129,7 +133,7 @@ const renderNativeImage = function (arg: number): NativeImage {
function sendAction<Channel extends keyof RendererMessage>( function sendAction<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
@@ -137,7 +141,7 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore(); win.restore();
} }
ipcRenderer.send("forward-to", win.webContents.id, channel, ...args); ipcRenderer.send("forward-to", win.webContents.id, channel, ...arguments_);
} }
const createTray = function (): void { const createTray = function (): void {
@@ -188,22 +192,22 @@ export function initializeTray(serverManagerView: ServerManagerView) {
} }
}); });
ipcRenderer.on("tray", (_event, arg: number): void => { ipcRenderer.on("tray", (_event, argument: number): void => {
if (!tray) { if (!tray) {
return; return;
} }
// We don't want to create tray from unread messages on macOS since it already has dock badges. // 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 (process.platform === "linux" || process.platform === "win32") {
if (arg === 0) { if (argument === 0) {
unread = arg; unread = argument;
tray.setImage(iconPath()); tray.setImage(iconPath());
tray.setToolTip("No unread messages"); tray.setToolTip("No unread messages");
} else { } else {
unread = arg; unread = argument;
const image = renderNativeImage(arg); const image = renderNativeImage(argument);
tray.setImage(image); tray.setImage(image);
tray.setToolTip(`${arg} unread messages`); tray.setToolTip(`${argument} unread messages`);
} }
} }
}); });

View File

@@ -10,8 +10,8 @@ import type {
} from "../../common/typed-ipc.js"; } from "../../common/typed-ipc.js";
type RendererListener<Channel extends keyof RendererMessage> = type RendererListener<Channel extends keyof RendererMessage> =
RendererMessage[Channel] extends (...args: infer Args) => void RendererMessage[Channel] extends (...arguments_: infer Arguments) => void
? (event: IpcRendererEvent, ...args: Args) => void ? (event: IpcRendererEvent, ...arguments_: Arguments) => void
: never; : never;
export const ipcRenderer: { export const ipcRenderer: {
@@ -35,25 +35,25 @@ export const ipcRenderer: {
send<Channel extends keyof RendererMessage>( send<Channel extends keyof RendererMessage>(
channel: "forward-message", channel: "forward-message",
rendererChannel: Channel, rendererChannel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void; ): void;
send<Channel extends keyof RendererMessage>( send<Channel extends keyof RendererMessage>(
channel: "forward-to", channel: "forward-to",
webContentsId: number, webContentsId: number,
rendererChannel: Channel, rendererChannel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void; ): void;
send<Channel extends keyof MainMessage>( send<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
...args: Parameters<MainMessage[Channel]> ...arguments_: Parameters<MainMessage[Channel]>
): void; ): void;
invoke<Channel extends keyof MainCall>( invoke<Channel extends keyof MainCall>(
channel: Channel, channel: Channel,
...args: Parameters<MainCall[Channel]> ...arguments_: Parameters<MainCall[Channel]>
): Promise<ReturnType<MainCall[Channel]>>; ): Promise<ReturnType<MainCall[Channel]>>;
sendSync<Channel extends keyof MainMessage>( sendSync<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
...args: Parameters<MainMessage[Channel]> ...arguments_: Parameters<MainMessage[Channel]>
): ReturnType<MainMessage[Channel]>; ): ReturnType<MainMessage[Channel]>;
postMessage<Channel extends keyof MainMessage>( postMessage<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
@@ -64,6 +64,6 @@ export const ipcRenderer: {
): void; ): void;
sendToHost<Channel extends keyof RendererMessage>( sendToHost<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void; ): void;
} = untypedIpcRenderer; } = untypedIpcRenderer;

View File

@@ -11,7 +11,7 @@ import * as EnterpriseUtil from "../../../common/enterprise-util.js";
import Logger from "../../../common/logger-util.js"; import Logger from "../../../common/logger-util.js";
import * as Messages from "../../../common/messages.js"; import * as Messages from "../../../common/messages.js";
import * as t from "../../../common/translation-util.js"; import * as t from "../../../common/translation-util.js";
import type {ServerConf} from "../../../common/types.js"; import type {ServerConfig} from "../../../common/types.js";
import defaultIcon from "../../img/icon.png"; import defaultIcon from "../../img/icon.png";
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.js";
@@ -23,7 +23,7 @@ const logger = new Logger({
// missing icon; it does not change with the actual icon location. // missing icon; it does not change with the actual icon location.
export const defaultIconSentinel = "../renderer/img/icon.png"; export const defaultIconSentinel = "../renderer/img/icon.png";
const serverConfSchema = z.object({ const serverConfigSchema = z.object({
url: z.string().url(), url: z.string().url(),
alias: z.string(), alias: z.string(),
icon: z.string(), icon: z.string(),
@@ -31,45 +31,49 @@ const serverConfSchema = z.object({
zulipFeatureLevel: z.number().default(0), zulipFeatureLevel: z.number().default(0),
}); });
let db!: JsonDB; let database!: JsonDB;
reloadDb(); reloadDatabase();
// Migrate from old schema // Migrate from old schema
try { try {
const oldDomain = db.getObject<unknown>("/domain"); const oldDomain = database.getObject<unknown>("/domain");
if (typeof oldDomain === "string") { if (typeof oldDomain === "string") {
(async () => { (async () => {
await addDomain({ await addDomain({
alias: "Zulip", alias: "Zulip",
url: oldDomain, url: oldDomain,
}); });
db.delete("/domain"); database.delete("/domain");
})(); })();
} }
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
} }
export function getDomains(): ServerConf[] { export function getDomains(): ServerConfig[] {
reloadDb(); reloadDatabase();
try { try {
return serverConfSchema.array().parse(db.getObject<unknown>("/domains")); return serverConfigSchema
.array()
.parse(database.getObject<unknown>("/domains"));
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
return []; return [];
} }
} }
export function getDomain(index: number): ServerConf { export function getDomain(index: number): ServerConfig {
reloadDb(); reloadDatabase();
return serverConfSchema.parse(db.getObject<unknown>(`/domains[${index}]`)); return serverConfigSchema.parse(
database.getObject<unknown>(`/domains[${index}]`),
);
} }
export function updateDomain(index: number, server: ServerConf): void { export function updateDomain(index: number, server: ServerConfig): void {
reloadDb(); reloadDatabase();
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push(`/domains[${index}]`, server, true); database.push(`/domains[${index}]`, server, true);
} }
export async function addDomain(server: { export async function addDomain(server: {
@@ -80,20 +84,20 @@ export async function addDomain(server: {
if (server.icon) { if (server.icon) {
const localIconUrl = await saveServerIcon(server.icon); const localIconUrl = await saveServerIcon(server.icon);
server.icon = localIconUrl; server.icon = localIconUrl;
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push("/domains[]", server, true); database.push("/domains[]", server, true);
reloadDb(); reloadDatabase();
} else { } else {
server.icon = defaultIconSentinel; server.icon = defaultIconSentinel;
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push("/domains[]", server, true); database.push("/domains[]", server, true);
reloadDb(); reloadDatabase();
} }
} }
export function removeDomains(): void { export function removeDomains(): void {
db.delete("/domains"); database.delete("/domains");
reloadDb(); reloadDatabase();
} }
export function removeDomain(index: number): boolean { export function removeDomain(index: number): boolean {
@@ -101,8 +105,8 @@ export function removeDomain(index: number): boolean {
return false; return false;
} }
db.delete(`/domains[${index}]`); database.delete(`/domains[${index}]`);
reloadDb(); reloadDatabase();
return true; return true;
} }
@@ -115,7 +119,7 @@ export function duplicateDomain(domain: string): boolean {
export async function checkDomain( export async function checkDomain(
domain: string, domain: string,
silent = false, silent = false,
): Promise<ServerConf> { ): Promise<ServerConfig> {
if (!silent && duplicateDomain(domain)) { if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode // Do not check duplicate in silent mode
throw new Error("This server has been added."); throw new Error("This server has been added.");
@@ -130,7 +134,7 @@ export async function checkDomain(
} }
} }
async function getServerSettings(domain: string): Promise<ServerConf> { async function getServerSettings(domain: string): Promise<ServerConfig> {
return ipcRenderer.invoke("get-server-settings", domain); return ipcRenderer.invoke("get-server-settings", domain);
} }
@@ -144,29 +148,29 @@ export async function saveServerIcon(iconURL: string): Promise<string> {
export async function updateSavedServer( export async function updateSavedServer(
url: string, url: string,
index: number, index: number,
): Promise<ServerConf> { ): Promise<ServerConfig> {
// Does not promise successful update // Does not promise successful update
const serverConf = getDomain(index); const serverConfig = getDomain(index);
const oldIcon = serverConf.icon; const oldIcon = serverConfig.icon;
try { try {
const newServerConf = await checkDomain(url, true); const newServerConfig = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConf.icon); const localIconUrl = await saveServerIcon(newServerConfig.icon);
if (!oldIcon || localIconUrl !== defaultIconSentinel) { if (!oldIcon || localIconUrl !== defaultIconSentinel) {
newServerConf.icon = localIconUrl; newServerConfig.icon = localIconUrl;
updateDomain(index, newServerConf); updateDomain(index, newServerConfig);
reloadDb(); reloadDatabase();
} }
return newServerConf; return newServerConfig;
} catch (error: unknown) { } catch (error: unknown) {
logger.log("Could not update server icon."); logger.log("Could not update server icon.");
logger.log(error); logger.log(error);
Sentry.captureException(error); Sentry.captureException(error);
return serverConf; return serverConfig;
} }
} }
function reloadDb(): void { function reloadDatabase(): void {
const domainJsonPath = path.join( const domainJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"config/domain.json", "config/domain.json",
@@ -188,7 +192,7 @@ function reloadDb(): void {
} }
} }
db = new JsonDB(domainJsonPath, true, true); database = new JsonDB(domainJsonPath, true, true);
} }
export function formatUrl(domain: string): string { export function formatUrl(domain: string): string {
@@ -203,7 +207,9 @@ export function formatUrl(domain: string): string {
return `https://${domain}`; return `https://${domain}`;
} }
export function getUnsupportedMessage(server: ServerConf): string | undefined { export function getUnsupportedMessage(
server: ServerConfig,
): string | undefined {
if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) { if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) {
const realm = new URL(server.url).hostname; const realm = new URL(server.url).hostname;
return t.__( return t.__(

View File

@@ -15,7 +15,7 @@ export default class ReconnectUtil {
fibonacciBackoff: backoff.Backoff; fibonacciBackoff: backoff.Backoff;
constructor(webview: WebView) { constructor(webview: WebView) {
this.url = webview.props.url; this.url = webview.properties.url;
this.alreadyReloaded = false; this.alreadyReloaded = false;
this.fibonacciBackoff = backoff.fibonacci({ this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000, initialDelay: 5000,

View File

@@ -10,7 +10,7 @@ const testsPkg = require("./package.json");
module.exports = { module.exports = {
createApp, createApp,
endTest, endTest,
resetTestDataDir, resetTestDataDir: resetTestDataDirectory,
}; };
// Runs Zulip Desktop. // Runs Zulip Desktop.
@@ -26,7 +26,7 @@ async function endTest(app) {
await app.close(); await app.close();
} }
function getAppDataDir() { function getAppDataDirectory() {
let base; let base;
switch (process.platform) { switch (process.platform) {
@@ -56,7 +56,7 @@ function getAppDataDir() {
} }
// Resets the test directory, containing domain.json, window-state.json, etc // Resets the test directory, containing domain.json, window-state.json, etc
function resetTestDataDir() { function resetTestDataDirectory() {
const appDataDir = getAppDataDir(); const appDataDirectory = getAppDataDirectory();
rimraf.sync(appDataDir); rimraf.sync(appDataDirectory);
} }