import type {OpenDialogOptions} from "electron/renderer"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import * as remote from "@electron/remote"; import {app, dialog, session} from "@electron/remote"; import Tagify from "@yaireo/tagify"; import ISO6391 from "iso-639-1"; import * as z from "zod"; import * as ConfigUtil from "../../../../common/config-util"; import * as EnterpriseUtil from "../../../../common/enterprise-util"; import {html} from "../../../../common/html"; import * as t from "../../../../common/translation-util"; import supportedLocales from "../../../../translations/supported-locales.json"; import {ipcRenderer} from "../../typed-ipc-renderer"; import {generateSelectHtml, generateSettingOption} from "./base-section"; const currentBrowserWindow = remote.getCurrentWindow(); interface GeneralSectionProps { $root: Element; } export function initGeneralSection({$root}: GeneralSectionProps): void { $root.innerHTML = html`
${t.__("Appearance")}
${t.__("Show app icon in system tray")}
${t.__("Show app unread badge")}
${t.__("Bounce dock on new private message")}
${t.__("Flash taskbar on new message")}
${t.__("Desktop Notifications")}
${t.__("Show desktop notifications")}
${t.__("Mute all sounds from Zulip")}
${t.__("App Updates")}
${t.__("Enable auto updates")}
${t.__("Get beta updates")}
${t.__("Functionality")}
${t.__("Start app at login")}
${t.__("Always start minimized")}
${t.__("Quit when the window is closed")}
${t.__("Enable spellchecker (requires restart)")}
${t.__("Advanced")}
${t.__("Enable error reporting (requires restart)")}
${t.__("App language (requires restart)")}
${t.__("Add custom CSS")}
${ConfigUtil.getConfigItem("customCSS", "")}
indeterminate_check_box ${t.__("Delete")}
${t.__("Default download location")}
${ConfigUtil.getConfigItem( "downloadsPath", app.getPath("downloads"), )}
${t.__("Ask where to save files before downloading")}
${t.__("Factory Reset Data")}
${t.__( "Reset the application, thus deleting all the connected organizations and accounts.", )}
`.html; updateTrayOption(); updateBadgeOption(); updateSilentOption(); autoUpdateOption(); betaUpdateOption(); updateSidebarOption(); updateStartAtLoginOption(); factoryReset(); showDesktopNotification(); enableSpellchecker(); minimizeOnStart(); addCustomCss(); showCustomCssPath(); removeCustomCss(); downloadFolder(); updateQuitOnCloseOption(); updatePromptDownloadOption(); enableErrorReporting(); setLocale(); initSpellChecker(); // Platform specific settings // Flashing taskbar on Windows if (process.platform === "win32") { updateFlashTaskbar(); } // Dock bounce on macOS if (process.platform === "darwin") { updateDockBouncing(); } // Auto hide menubar on Windows and Linux if (process.platform !== "darwin") { updateMenubarOption(); } function updateTrayOption(): void { generateSettingOption({ $element: $root.querySelector("#tray-option .setting-control")!, value: ConfigUtil.getConfigItem("trayIcon", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("trayIcon", true); ConfigUtil.setConfigItem("trayIcon", newValue); ipcRenderer.send("forward-message", "toggletray"); updateTrayOption(); }, }); } function updateMenubarOption(): void { generateSettingOption({ $element: $root.querySelector("#menubar-option .setting-control")!, value: ConfigUtil.getConfigItem("autoHideMenubar", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false); ConfigUtil.setConfigItem("autoHideMenubar", newValue); ipcRenderer.send("toggle-menubar", newValue); updateMenubarOption(); }, }); } function updateBadgeOption(): void { generateSettingOption({ $element: $root.querySelector("#badge-option .setting-control")!, value: ConfigUtil.getConfigItem("badgeOption", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("badgeOption", true); ConfigUtil.setConfigItem("badgeOption", newValue); ipcRenderer.send("toggle-badge-option", newValue); updateBadgeOption(); }, }); } function updateDockBouncing(): void { generateSettingOption({ $element: $root.querySelector("#dock-bounce-option .setting-control")!, value: ConfigUtil.getConfigItem("dockBouncing", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("dockBouncing", true); ConfigUtil.setConfigItem("dockBouncing", newValue); updateDockBouncing(); }, }); } function updateFlashTaskbar(): void { generateSettingOption({ $element: $root.querySelector("#flash-taskbar-option .setting-control")!, value: ConfigUtil.getConfigItem("flashTaskbarOnMessage", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem( "flashTaskbarOnMessage", true, ); ConfigUtil.setConfigItem("flashTaskbarOnMessage", newValue); updateFlashTaskbar(); }, }); } function autoUpdateOption(): void { generateSettingOption({ $element: $root.querySelector("#autoupdate-option .setting-control")!, disabled: EnterpriseUtil.configItemExists("autoUpdate"), value: ConfigUtil.getConfigItem("autoUpdate", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("autoUpdate", true); ConfigUtil.setConfigItem("autoUpdate", newValue); if (!newValue) { ConfigUtil.setConfigItem("betaUpdate", false); betaUpdateOption(); } autoUpdateOption(); }, }); } function betaUpdateOption(): void { generateSettingOption({ $element: $root.querySelector("#betaupdate-option .setting-control")!, value: ConfigUtil.getConfigItem("betaUpdate", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("betaUpdate", false); if (ConfigUtil.getConfigItem("autoUpdate", true)) { ConfigUtil.setConfigItem("betaUpdate", newValue); betaUpdateOption(); } }, }); } function updateSilentOption(): void { generateSettingOption({ $element: $root.querySelector("#silent-option .setting-control")!, value: ConfigUtil.getConfigItem("silent", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("silent", true); ConfigUtil.setConfigItem("silent", newValue); updateSilentOption(); ipcRenderer.sendTo( currentBrowserWindow.webContents.id, "toggle-silent", newValue, ); }, }); } function showDesktopNotification(): void { generateSettingOption({ $element: $root.querySelector( "#show-notification-option .setting-control", )!, value: ConfigUtil.getConfigItem("showNotification", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("showNotification", true); ConfigUtil.setConfigItem("showNotification", newValue); showDesktopNotification(); }, }); } function updateSidebarOption(): void { generateSettingOption({ $element: $root.querySelector("#sidebar-option .setting-control")!, value: ConfigUtil.getConfigItem("showSidebar", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("showSidebar", true); ConfigUtil.setConfigItem("showSidebar", newValue); ipcRenderer.send("forward-message", "toggle-sidebar", newValue); updateSidebarOption(); }, }); } function updateStartAtLoginOption(): void { generateSettingOption({ $element: $root.querySelector("#startAtLogin-option .setting-control")!, value: ConfigUtil.getConfigItem("startAtLogin", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("startAtLogin", false); ConfigUtil.setConfigItem("startAtLogin", newValue); ipcRenderer.send("toggleAutoLauncher", newValue); updateStartAtLoginOption(); }, }); } function updateQuitOnCloseOption(): void { generateSettingOption({ $element: $root.querySelector("#quitOnClose-option .setting-control")!, value: ConfigUtil.getConfigItem("quitOnClose", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("quitOnClose", false); ConfigUtil.setConfigItem("quitOnClose", newValue); updateQuitOnCloseOption(); }, }); } function enableSpellchecker(): void { generateSettingOption({ $element: $root.querySelector( "#enable-spellchecker-option .setting-control", )!, value: ConfigUtil.getConfigItem("enableSpellchecker", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("enableSpellchecker", true); ConfigUtil.setConfigItem("enableSpellchecker", newValue); ipcRenderer.send("configure-spell-checker"); enableSpellchecker(); const spellcheckerLanguageInput: HTMLElement = $root.querySelector("#spellcheck-langs")!; const spellcheckerNote: HTMLElement = $root.querySelector("#note")!; spellcheckerLanguageInput.style.display = spellcheckerLanguageInput.style.display === "none" ? "" : "none"; spellcheckerNote.style.display = spellcheckerNote.style.display === "none" ? "" : "none"; }, }); } function enableErrorReporting(): void { generateSettingOption({ $element: $root.querySelector( "#enable-error-reporting .setting-control", )!, value: ConfigUtil.getConfigItem("errorReporting", true), clickHandler() { const newValue = !ConfigUtil.getConfigItem("errorReporting", true); ConfigUtil.setConfigItem("errorReporting", newValue); enableErrorReporting(); }, }); } async function customCssDialog(): Promise { const showDialogOptions: OpenDialogOptions = { title: "Select file", properties: ["openFile"], filters: [{name: "CSS file", extensions: ["css"]}], }; const {filePaths, canceled} = await dialog.showOpenDialog( showDialogOptions, ); if (!canceled) { ConfigUtil.setConfigItem("customCSS", filePaths[0]); ipcRenderer.send("forward-message", "hard-reload"); } } function setLocale(): void { const langDiv: HTMLSelectElement = $root.querySelector(".lang-div")!; const langListHtml = generateSelectHtml(supportedLocales, "lang-menu"); langDiv.innerHTML += langListHtml.html; // `langMenu` is the select-option dropdown menu formed after executing the previous command const langMenu: HTMLSelectElement = $root.querySelector(".lang-menu")!; // The next three lines set the selected language visible on the dropdown button let language = ConfigUtil.getConfigItem("appLanguage", "en"); language = language && langMenu.options.namedItem(language) ? language : "en"; langMenu.options.namedItem(language)!.selected = true; langMenu.addEventListener("change", () => { ConfigUtil.setConfigItem("appLanguage", langMenu.value); }); } function minimizeOnStart(): void { generateSettingOption({ $element: $root.querySelector("#start-minimize-option .setting-control")!, value: ConfigUtil.getConfigItem("startMinimized", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("startMinimized", false); ConfigUtil.setConfigItem("startMinimized", newValue); minimizeOnStart(); }, }); } function addCustomCss(): void { const customCssButton = $root.querySelector( "#add-custom-css .custom-css-button", )!; customCssButton.addEventListener("click", async () => { await customCssDialog(); }); } function showCustomCssPath(): void { if (!ConfigUtil.getConfigItem("customCSS", null)) { const cssPath: HTMLElement = $root.querySelector("#remove-custom-css")!; cssPath.style.display = "none"; } } function removeCustomCss(): void { const removeCssButton = $root.querySelector("#css-delete-action")!; removeCssButton.addEventListener("click", () => { ConfigUtil.setConfigItem("customCSS", ""); ipcRenderer.send("forward-message", "hard-reload"); }); } async function downloadFolderDialog(): Promise { const showDialogOptions: OpenDialogOptions = { title: "Select Download Location", properties: ["openDirectory"], }; const {filePaths, canceled} = await dialog.showOpenDialog( showDialogOptions, ); if (!canceled) { ConfigUtil.setConfigItem("downloadsPath", filePaths[0]); const downloadFolderPath: HTMLElement = $root.querySelector( ".download-folder-path", )!; downloadFolderPath.textContent = filePaths[0]; } } function downloadFolder(): void { const downloadFolder = $root.querySelector( "#download-folder .download-folder-button", )!; downloadFolder.addEventListener("click", async () => { await downloadFolderDialog(); }); } function updatePromptDownloadOption(): void { generateSettingOption({ $element: $root.querySelector("#prompt-download .setting-control")!, value: ConfigUtil.getConfigItem("promptDownload", false), clickHandler() { const newValue = !ConfigUtil.getConfigItem("promptDownload", false); ConfigUtil.setConfigItem("promptDownload", newValue); updatePromptDownloadOption(); }, }); } async function factoryResetSettings(): Promise { const clearAppDataMessage = "When the application restarts, it will be as if you have just downloaded Zulip app."; const getAppPath = path.join(app.getPath("appData"), app.name); const {response} = await dialog.showMessageBox({ type: "warning", buttons: ["YES", "NO"], defaultId: 0, message: "Are you sure?", detail: clearAppDataMessage, }); if (response === 0) { await fs.promises.rmdir(getAppPath, {recursive: true}); setTimeout(() => { ipcRenderer.send("clear-app-settings"); }, 1000); } } function factoryReset(): void { const factoryResetButton = $root.querySelector( "#factory-reset-option .factory-reset-button", )!; factoryResetButton.addEventListener("click", async () => { await factoryResetSettings(); }); } function initSpellChecker(): void { // The elctron API is a no-op on macOS and macOS default spellchecker is used. if (process.platform === "darwin") { const note: HTMLElement = $root.querySelector("#note")!; note.append(t.__("On macOS, the OS spellchecker is used.")); note.append(document.createElement("br")); note.append( t.__( "Change the language from System Preferences → Keyboard → Text → Spelling.", ), ); } else { const note: HTMLElement = $root.querySelector("#note")!; note.append( t.__("You can select a maximum of 3 languages for spellchecking."), ); const spellDiv: HTMLElement = $root.querySelector("#spellcheck-langs")!; spellDiv.innerHTML += html`
${t.__("Spellchecker Languages")}
`.html; const availableLanguages = session.fromPartition( "persist:webviewsession", ).availableSpellCheckerLanguages; let languagePairs: Map = new Map(); for (const l of availableLanguages) { if (ISO6391.validate(l)) { languagePairs.set(ISO6391.getName(l), l); } } // Manually set names for languages not available in ISO6391 languagePairs.set("English (AU)", "en-AU"); languagePairs.set("English (CA)", "en-CA"); languagePairs.set("English (GB)", "en-GB"); languagePairs.set("English (US)", "en-US"); languagePairs.set("Spanish (Latin America)", "es-419"); languagePairs.set("Spanish (Argentina)", "es-AR"); languagePairs.set("Spanish (Mexico)", "es-MX"); languagePairs.set("Spanish (US)", "es-US"); languagePairs.set("Portuguese (Brazil)", "pt-BR"); languagePairs.set("Portuguese (Portugal)", "pt-PT"); languagePairs.set("Serbo-Croatian", "sh"); languagePairs = new Map( [...languagePairs].sort((a, b) => (a[0] < b[0] ? -1 : 1)), ); const tagField: HTMLInputElement = $root.querySelector( "input[name=spellcheck]", )!; const tagify = new Tagify(tagField, { whitelist: [...languagePairs.keys()], enforceWhitelist: true, maxTags: 3, dropdown: { enabled: 0, maxItems: Number.POSITIVE_INFINITY, closeOnSelect: false, highlightFirst: true, }, }); const configuredLanguages: string[] = ( ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [] ).map( (code: string) => [...languagePairs].find((pair) => pair[1] === code)![0], ); tagify.addTags(configuredLanguages); tagField.addEventListener("change", () => { if (tagField.value.length === 0) { ConfigUtil.setConfigItem("spellcheckerLanguages", []); ipcRenderer.send("configure-spell-checker"); } else { const data: unknown = JSON.parse(tagField.value); const spellLangs: string[] = z .array(z.object({value: z.string()})) .parse(data) .map((elt) => languagePairs.get(elt.value)!); ConfigUtil.setConfigItem("spellcheckerLanguages", spellLangs); ipcRenderer.send("configure-spell-checker"); } }); } // Do not display the spellchecker input and note if it is disabled if (!ConfigUtil.getConfigItem("enableSpellchecker", true)) { const spellcheckerLanguageInput: HTMLElement = $root.querySelector("#spellcheck-langs")!; const spellcheckerNote: HTMLElement = $root.querySelector("#note")!; spellcheckerLanguageInput.style.display = "none"; spellcheckerNote.style.display = "none"; } } }