Files
zulip-desktop/app/renderer/js/pages/preference/general-section.ts
2021-04-02 18:20:21 -07:00

693 lines
24 KiB
TypeScript

import type {OpenDialogOptions} from "electron";
import {ipcRenderer, remote} from "electron";
import fs from "fs";
import path from "path";
import Tagify from "@yaireo/tagify";
import ISO6391 from "iso-639-1";
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 {generateSelectHTML, generateSettingOption} from "./base-section";
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
interface GeneralSectionProps {
$root: Element;
}
export function initGeneralSection(props: GeneralSectionProps): void {
props.$root.innerHTML = html`
<div class="settings-pane">
<div class="title">${t.__("Appearance")}</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="tray-option">
<div class="setting-description">
${t.__("Show app icon in system tray")}
</div>
<div class="setting-control"></div>
</div>
<div
class="setting-row"
id="menubar-option"
style="display:${process.platform === "darwin" ? "none" : ""}"
>
<div class="setting-description">
${t.__("Auto hide menu bar (Press Alt key to display)")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="sidebar-option">
<div class="setting-description">
${t.__("Show sidebar")} (<span class="code"
>${process.platform === "darwin"
? "Cmd+Shift+S"
: "Ctrl+Shift+S"}</span
>)
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">
${t.__("Show app unread badge")}
</div>
<div class="setting-control"></div>
</div>
<div
class="setting-row"
id="dock-bounce-option"
style="display:${process.platform === "darwin" ? "" : "none"}"
>
<div class="setting-description">
${t.__("Bounce dock on new private message")}
</div>
<div class="setting-control"></div>
</div>
<div
class="setting-row"
id="flash-taskbar-option"
style="display:${process.platform === "win32" ? "" : "none"}"
>
<div class="setting-description">
${t.__("Flash taskbar on new message")}
</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">${t.__("Desktop Notifications")}</div>
<div class="settings-card">
<div class="setting-row" id="show-notification-option">
<div class="setting-description">
${t.__("Show desktop notifications")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="silent-option">
<div class="setting-description">
${t.__("Mute all sounds from Zulip")}
</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">${t.__("App Updates")}</div>
<div class="settings-card">
<div class="setting-row" id="autoupdate-option">
<div class="setting-description">${t.__("Enable auto updates")}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="betaupdate-option">
<div class="setting-description">${t.__("Get beta updates")}</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">${t.__("Functionality")}</div>
<div class="settings-card">
<div class="setting-row" id="startAtLogin-option">
<div class="setting-description">${t.__("Start app at login")}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="start-minimize-option">
<div class="setting-description">
${t.__("Always start minimized")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="quitOnClose-option">
<div class="setting-description">
${t.__("Quit when the window is closed")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">
${t.__("Enable spellchecker (requires restart)")}
</div>
<div class="setting-control"></div>
</div>
<div
class="setting-row"
id="spellcheck-langs"
style="display:${process.platform === "darwin" ? "none" : ""}"
></div>
<div class="setting-row" id="note"></div>
</div>
<div class="title">${t.__("Advanced")}</div>
<div class="settings-card">
<div class="setting-row" id="enable-error-reporting">
<div class="setting-description">
${t.__("Enable error reporting (requires restart)")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="app-language">
<div class="setting-description">
${t.__("App language (requires restart)")}
</div>
<div id="lang-div" class="lang-div"></div>
</div>
<div class="setting-row" id="add-custom-css">
<div class="setting-description">${t.__("Add custom CSS")}</div>
<button class="custom-css-button green">${t.__("Upload")}</button>
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">
${ConfigUtil.getConfigString("customCSS", "")}
</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>${t.__("Delete")}</span>
</div>
</div>
<div class="setting-row" id="download-folder">
<div class="setting-description">
${t.__("Default download location")}
</div>
<button class="download-folder-button green">
${t.__("Change")}
</button>
</div>
<div class="setting-row">
<div class="setting-description">
<div class="download-folder-path">
${ConfigUtil.getConfigString(
"downloadsPath",
app.getPath("downloads"),
)}
</div>
</div>
</div>
<div class="setting-row" id="prompt-download">
<div class="setting-description">
${t.__("Ask where to save files before downloading")}
</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">${t.__("Factory Reset Data")}</div>
<div class="settings-card">
<div class="setting-row" id="factory-reset-option">
<div class="setting-description">
${t.__(
"Reset the application, thus deleting all the connected organizations and accounts.",
)}
</div>
<button class="factory-reset-button red w-150">
${t.__("Factory Reset")}
</button>
</div>
</div>
</div>
`.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: document.querySelector("#tray-option .setting-control")!,
value: ConfigUtil.getConfigItem("trayIcon", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("trayIcon");
ConfigUtil.setConfigItem("trayIcon", newValue);
ipcRenderer.send("forward-message", "toggletray");
updateTrayOption();
},
});
}
function updateMenubarOption(): void {
generateSettingOption({
$element: document.querySelector("#menubar-option .setting-control")!,
value: ConfigUtil.getConfigItem("autoHideMenubar", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar");
ConfigUtil.setConfigItem("autoHideMenubar", newValue);
ipcRenderer.send("toggle-menubar", newValue);
updateMenubarOption();
},
});
}
function updateBadgeOption(): void {
generateSettingOption({
$element: document.querySelector("#badge-option .setting-control")!,
value: ConfigUtil.getConfigItem("badgeOption", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("badgeOption");
ConfigUtil.setConfigItem("badgeOption", newValue);
ipcRenderer.send("toggle-badge-option", newValue);
updateBadgeOption();
},
});
}
function updateDockBouncing(): void {
generateSettingOption({
$element: document.querySelector("#dock-bounce-option .setting-control")!,
value: ConfigUtil.getConfigItem("dockBouncing", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("dockBouncing");
ConfigUtil.setConfigItem("dockBouncing", newValue);
updateDockBouncing();
},
});
}
function updateFlashTaskbar(): void {
generateSettingOption({
$element: document.querySelector(
"#flash-taskbar-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("flashTaskbarOnMessage");
ConfigUtil.setConfigItem("flashTaskbarOnMessage", newValue);
updateFlashTaskbar();
},
});
}
function autoUpdateOption(): void {
generateSettingOption({
$element: document.querySelector("#autoupdate-option .setting-control")!,
disabled: EnterpriseUtil.configItemExists("autoUpdate"),
value: ConfigUtil.getConfigItem("autoUpdate", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("autoUpdate");
ConfigUtil.setConfigItem("autoUpdate", newValue);
if (!newValue) {
ConfigUtil.setConfigItem("betaUpdate", false);
betaUpdateOption();
}
autoUpdateOption();
},
});
}
function betaUpdateOption(): void {
generateSettingOption({
$element: document.querySelector("#betaupdate-option .setting-control")!,
value: ConfigUtil.getConfigItem("betaUpdate", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("betaUpdate");
if (ConfigUtil.getConfigItem("autoUpdate")) {
ConfigUtil.setConfigItem("betaUpdate", newValue);
betaUpdateOption();
}
},
});
}
function updateSilentOption(): void {
generateSettingOption({
$element: document.querySelector("#silent-option .setting-control")!,
value: ConfigUtil.getConfigItem("silent", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("silent", true);
ConfigUtil.setConfigItem("silent", newValue);
updateSilentOption();
currentBrowserWindow.webContents.send("toggle-silent", newValue);
},
});
}
function showDesktopNotification(): void {
generateSettingOption({
$element: document.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: document.querySelector("#sidebar-option .setting-control")!,
value: ConfigUtil.getConfigItem("showSidebar", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("showSidebar");
ConfigUtil.setConfigItem("showSidebar", newValue);
ipcRenderer.send("forward-message", "toggle-sidebar", newValue);
updateSidebarOption();
},
});
}
function updateStartAtLoginOption(): void {
generateSettingOption({
$element: document.querySelector(
"#startAtLogin-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("startAtLogin", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("startAtLogin");
ConfigUtil.setConfigItem("startAtLogin", newValue);
ipcRenderer.send("toggleAutoLauncher", newValue);
updateStartAtLoginOption();
},
});
}
function updateQuitOnCloseOption(): void {
generateSettingOption({
$element: document.querySelector("#quitOnClose-option .setting-control")!,
value: ConfigUtil.getConfigItem("quitOnClose", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("quitOnClose");
ConfigUtil.setConfigItem("quitOnClose", newValue);
updateQuitOnCloseOption();
},
});
}
function enableSpellchecker(): void {
generateSettingOption({
$element: document.querySelector(
"#enable-spellchecker-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("enableSpellchecker", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("enableSpellchecker");
ConfigUtil.setConfigItem("enableSpellchecker", newValue);
enableSpellchecker();
const spellcheckerLanguageInput: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
const spellcheckerNote: HTMLElement = document.querySelector("#note")!;
spellcheckerLanguageInput.style.display =
spellcheckerLanguageInput.style.display === "none" ? "" : "none";
spellcheckerNote.style.display =
spellcheckerNote.style.display === "none" ? "" : "none";
},
});
}
function enableErrorReporting(): void {
generateSettingOption({
$element: document.querySelector(
"#enable-error-reporting .setting-control",
)!,
value: ConfigUtil.getConfigItem("errorReporting", true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("errorReporting");
ConfigUtil.setConfigItem("errorReporting", newValue);
enableErrorReporting();
},
});
}
async function customCssDialog(): Promise<void> {
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 = document.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 = document.querySelector(".lang-menu")!;
// The next three lines set the selected language visible on the dropdown button
let language = ConfigUtil.getConfigItem("appLanguage");
language =
language && langMenu.options.namedItem(language) ? language : "en";
langMenu.options.namedItem(language)!.selected = true;
langMenu.addEventListener("change", (event: Event) => {
ConfigUtil.setConfigItem(
"appLanguage",
(event.target as HTMLSelectElement).value,
);
});
}
function minimizeOnStart(): void {
generateSettingOption({
$element: document.querySelector(
"#start-minimize-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("startMinimized", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("startMinimized");
ConfigUtil.setConfigItem("startMinimized", newValue);
minimizeOnStart();
},
});
}
function addCustomCSS(): void {
const customCSSButton = document.querySelector(
"#add-custom-css .custom-css-button",
)!;
customCSSButton.addEventListener("click", async () => {
await customCssDialog();
});
}
function showCustomCSSPath(): void {
if (!ConfigUtil.getConfigItem("customCSS")) {
const cssPATH: HTMLElement = document.querySelector(
"#remove-custom-css",
)!;
cssPATH.style.display = "none";
}
}
function removeCustomCSS(): void {
const removeCSSButton = document.querySelector("#css-delete-action")!;
removeCSSButton.addEventListener("click", () => {
ConfigUtil.setConfigItem("customCSS", "");
ipcRenderer.send("forward-message", "hard-reload");
});
}
async function downloadFolderDialog(): Promise<void> {
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 = document.querySelector(
".download-folder-path",
)!;
downloadFolderPath.textContent = filePaths[0];
}
}
function downloadFolder(): void {
const downloadFolder = document.querySelector(
"#download-folder .download-folder-button",
)!;
downloadFolder.addEventListener("click", async () => {
await downloadFolderDialog();
});
}
function updatePromptDownloadOption(): void {
generateSettingOption({
$element: document.querySelector("#prompt-download .setting-control")!,
value: ConfigUtil.getConfigItem("promptDownload", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("promptDownload");
ConfigUtil.setConfigItem("promptDownload", newValue);
updatePromptDownloadOption();
},
});
}
async function factoryResetSettings(): Promise<void> {
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 = document.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 = document.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 = document.querySelector("#note")!;
note.append(
t.__("You can select a maximum of 3 languages for spellchecking."),
);
const spellDiv: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
spellDiv.innerHTML += html`
<div class="setting-description">${t.__("Spellchecker Languages")}</div>
<input name="spellcheck" placeholder="Enter Languages" />
`.html;
const availableLanguages = session.fromPartition("persist:webviewsession")
.availableSpellCheckerLanguages;
let languagePairs: Map<string, string> = 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 = document.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",
).map(
(code: string) =>
[...languagePairs].find((pair) => pair[1] === code)![0],
);
tagify.addTags(configuredLanguages);
tagField.addEventListener("change", (event) => {
if ((event.target as HTMLInputElement).value.length === 0) {
ConfigUtil.setConfigItem("spellcheckerLanguages", []);
ipcRenderer.send("set-spellcheck-langs");
} else {
const spellLangs: string[] = [
...JSON.parse((event.target as HTMLInputElement).value).values(),
].map((elt) => languagePairs.get(elt.value)!);
ConfigUtil.setConfigItem("spellcheckerLanguages", spellLangs);
ipcRenderer.send("set-spellcheck-langs");
}
});
}
// Do not display the spellchecker input and note if it is disabled
if (!ConfigUtil.getConfigItem("enableSpellchecker")) {
const spellcheckerLanguageInput: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
const spellcheckerNote: HTMLElement = document.querySelector("#note")!;
spellcheckerLanguageInput.style.display = "none";
spellcheckerNote.style.display = "none";
}
}
}