├── public ├── translations │ ├── enm.json │ ├── he.json │ ├── hr.json │ ├── no.json │ ├── uz.json │ ├── lt.json │ ├── gl.json │ ├── id.json │ ├── bqi.json │ ├── sk.json │ ├── te.json │ ├── eu.json │ ├── README.md │ ├── supported-locales.json │ ├── ca.json │ ├── bn.json │ ├── ar.json │ ├── si.json │ ├── hi.json │ ├── ko.json │ ├── mn.json │ ├── ja.json │ ├── sv.json │ ├── ml.json │ ├── bg.json │ └── nl.json └── resources │ ├── Icon.ico │ ├── Icon.png │ ├── sounds │ └── ding.ogg │ └── tray │ ├── traywin.ico │ ├── traylinux.png │ ├── trayunread.ico │ ├── traymacOSTemplate.png │ ├── traymacOSTemplate@2x.png │ ├── traymacOSTemplate@3x.png │ └── traymacOSTemplate@4x.png ├── .npmrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── node.js.yml └── PULL_REQUEST_TEMPLATE.md ├── app ├── resources │ └── zulip.png ├── renderer │ ├── img │ │ ├── icon.png │ │ ├── zulip_network.png │ │ ├── ic_server_tab_default.png │ │ └── ic_loading.svg │ ├── fonts │ │ ├── Montserrat-Regular.ttf │ │ └── MaterialIcons-Regular.ttf │ ├── css │ │ ├── preload.css │ │ ├── fonts.css │ │ ├── feedback.css │ │ ├── network.css │ │ └── about.css │ ├── js │ │ ├── zod-config.ts │ │ ├── components │ │ │ ├── base.ts │ │ │ ├── tab.ts │ │ │ ├── functional-tab.ts │ │ │ ├── server-tab.ts │ │ │ └── context-menu.ts │ │ ├── pages │ │ │ ├── network.ts │ │ │ ├── preference │ │ │ │ ├── servers-section.ts │ │ │ │ ├── find-accounts.ts │ │ │ │ ├── connected-org-section.ts │ │ │ │ ├── base-section.ts │ │ │ │ ├── nav.ts │ │ │ │ ├── server-info-form.ts │ │ │ │ ├── new-server-form.ts │ │ │ │ ├── preference.ts │ │ │ │ └── network-section.ts │ │ │ └── about.ts │ │ ├── utils │ │ │ ├── system-util.ts │ │ │ └── reconnect-util.ts │ │ ├── preload.ts │ │ ├── notification │ │ │ └── index.ts │ │ ├── clipboard-decrypter.ts │ │ ├── typed-ipc-renderer.ts │ │ └── electron-bridge.ts │ ├── about.html │ ├── preference.html │ ├── main.html │ └── network.html ├── common │ ├── translation-util.ts │ ├── paths.ts │ ├── types.ts │ ├── html.ts │ ├── messages.ts │ ├── config-schemata.ts │ ├── link-util.ts │ ├── dnd-util.ts │ ├── default-util.ts │ ├── logger-util.ts │ ├── enterprise-util.ts │ ├── config-util.ts │ └── typed-ipc.ts └── main │ ├── sentry.ts │ ├── startup.ts │ ├── badge-settings.ts │ ├── linuxupdater.ts │ ├── linux-update-util.ts │ ├── typed-ipc-main.ts │ ├── request.ts │ ├── autoupdater.ts │ └── handle-external-link.ts ├── .prettierignore ├── packaging ├── deb-release-upgrades.cfg ├── deb-apt.sources ├── deb-after-install.sh └── deb-apt.asc ├── tests ├── zulip-test │ └── package.json ├── index.ts ├── test-new-organization.ts ├── test-add-organization.ts └── setup.ts ├── typings.d.ts ├── .editorconfig ├── .gitattributes ├── .mailmap ├── .stylelintrc ├── .gitignore ├── tools ├── fetch-pull-request ├── fetch-rebase-pull-request ├── reset-to-pull-request ├── fetch-pull-request.cmd ├── fetch-rebase-pull-request.cmd └── push-to-pull-request ├── i18next.config.ts ├── patches ├── gatemaker.patch └── i18n.patch ├── .htmlhintrc ├── pnpm-workspace.yaml ├── tsconfig.json ├── docs ├── howto │ └── translations.md └── Enterprise.md ├── electron.vite.config.ts ├── README.md ├── CONTRIBUTING.md ├── development.md └── xo.config.ts /public/translations/enm.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/translations/he.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/translations/hr.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/translations/no.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/translations/uz.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-options=--experimental-strip-types 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zulip 2 | patreon: zulip 3 | open_collective: zulip 4 | -------------------------------------------------------------------------------- /app/resources/zulip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/resources/zulip.png -------------------------------------------------------------------------------- /app/renderer/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/renderer/img/icon.png -------------------------------------------------------------------------------- /public/resources/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/Icon.ico -------------------------------------------------------------------------------- /public/resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/Icon.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /out 3 | /pnpm-lock.yaml 4 | /pnpm-workspace.yaml 5 | /public/translations/*.json 6 | -------------------------------------------------------------------------------- /packaging/deb-release-upgrades.cfg: -------------------------------------------------------------------------------- 1 | [ThirdPartyMirrors] 2 | zulip-desktop=https://download.zulip.com/desktop/apt 3 | -------------------------------------------------------------------------------- /app/renderer/img/zulip_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/renderer/img/zulip_network.png -------------------------------------------------------------------------------- /public/resources/sounds/ding.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/sounds/ding.ogg -------------------------------------------------------------------------------- /public/resources/tray/traywin.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traywin.ico -------------------------------------------------------------------------------- /public/translations/lt.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cancel": "Atšaukti", 3 | "Close": "Uždaryti", 4 | "Settings": "Nustatymai" 5 | } 6 | -------------------------------------------------------------------------------- /public/resources/tray/traylinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traylinux.png -------------------------------------------------------------------------------- /public/resources/tray/trayunread.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/trayunread.ico -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | --- 5 | -------------------------------------------------------------------------------- /app/renderer/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/renderer/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /app/renderer/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/renderer/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /app/renderer/img/ic_server_tab_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/app/renderer/img/ic_server_tab_default.png -------------------------------------------------------------------------------- /public/resources/tray/traymacOSTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traymacOSTemplate.png -------------------------------------------------------------------------------- /public/resources/tray/traymacOSTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traymacOSTemplate@2x.png -------------------------------------------------------------------------------- /public/resources/tray/traymacOSTemplate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traymacOSTemplate@3x.png -------------------------------------------------------------------------------- /public/resources/tray/traymacOSTemplate@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulip/zulip-desktop/HEAD/public/resources/tray/traymacOSTemplate@4x.png -------------------------------------------------------------------------------- /public/translations/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cancel": "Cancelar", 3 | "Edit": "Editar", 4 | "OK": "Aceptar", 5 | "Save": "Gardar", 6 | "Settings": "Configuración" 7 | } 8 | -------------------------------------------------------------------------------- /tests/zulip-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.9.3", 3 | "productName": "ZulipTest", 4 | "type": "module", 5 | "main": "../../out/main/index.js" 6 | } 7 | -------------------------------------------------------------------------------- /packaging/deb-apt.sources: -------------------------------------------------------------------------------- 1 | Types: deb 2 | URIs: https://download.zulip.com/desktop/apt/ 3 | Suites: stable 4 | Components: main 5 | Signed-By: /usr/share/keyrings/zulip-desktop.asc 6 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "zulip:remote" { 2 | export const { 3 | app, 4 | dialog, 5 | }: typeof import("electron/main") | typeof import("@electron/remote"); 6 | } 7 | -------------------------------------------------------------------------------- /app/renderer/css/preload.css: -------------------------------------------------------------------------------- 1 | /* Override css rules */ 2 | 3 | .portico-wrap > .header { 4 | display: none; 5 | } 6 | 7 | .portico-container > .footer { 8 | display: none; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{*.cjs,*.css,*.html,*.js,*.json,*.ts}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.gif binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.eot binary 7 | *.woff binary 8 | *.woff2 binary 9 | *.svg binary 10 | *.ttf binary 11 | *.png binary 12 | *.otf binary 13 | *.tif binary 14 | *.ogg binary 15 | -------------------------------------------------------------------------------- /app/renderer/js/zod-config.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | // In an Electron preload script, Content-Security-Policy only takes effect 4 | // after the page has loaded, which breaks Zod's detection of whether eval is 5 | // allowed. 6 | z.config({jitless: true}); 7 | -------------------------------------------------------------------------------- /public/translations/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "Tentang Zulip", 3 | "Cancel": "Batal", 4 | "Change": "Ubah", 5 | "Close": "Tutup", 6 | "Find accounts": "Temukan akun", 7 | "Organization URL": "URL Organisasi", 8 | "Save": "Simpan", 9 | "Settings": "Pengaturan" 10 | } 11 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Anders Kaseorg 2 | Rishi Gupta 3 | Rishi Gupta 4 | Tim Abbott 5 | Tim Abbott 6 | -------------------------------------------------------------------------------- /app/renderer/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /public/translations/bqi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cancel": "raď kerdên", 3 | "Change": "ālêštkâri", 4 | "Close": "bastên", 5 | "Delete": "pāk kerdên", 6 | "Edit": "ālêšt", 7 | "File": "fāyl", 8 | "Find accounts": "jostên hêsāvā mêntori", 9 | "OK": "xā", 10 | "Save": "zaft kerdên", 11 | "Settings": "sāmovā" 12 | } 13 | -------------------------------------------------------------------------------- /app/renderer/preference.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "rules": { 4 | "color-named": "never", 5 | "color-no-hex": true, 6 | "font-family-no-missing-generic-family-keyword": [ 7 | true, 8 | {"ignoreFontFamilies": ["Material Icons"]} 9 | ], 10 | "selector-type-no-unknown": [true, {"ignoreTypes": ["webview"]}] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | /node_modules/ 3 | 4 | # Compiled binary build directory 5 | /dist/ 6 | /out/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | 12 | // osx garbage 13 | *.DS_Store 14 | .DS_Store 15 | 16 | # dotenv environment variables file 17 | .env 18 | 19 | # miscellaneous 20 | .idea 21 | config.gypi 22 | 23 | # Test generated files 24 | # tests/package.json 25 | 26 | .python-version 27 | -------------------------------------------------------------------------------- /app/renderer/js/components/base.ts: -------------------------------------------------------------------------------- 1 | import type {Html} from "../../../common/html.ts"; 2 | 3 | export function generateNodeFromHtml(html: Html): Element { 4 | const wrapper = document.createElement("div"); 5 | wrapper.innerHTML = html.html; 6 | 7 | if (wrapper.firstElementChild === null) { 8 | throw new Error("No element found in HTML"); 9 | } 10 | 11 | return wrapper.firstElementChild; 12 | } 13 | -------------------------------------------------------------------------------- /app/renderer/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Material Icons"; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: 6 | local("Material Icons"), 7 | local("MaterialIcons-Regular"), 8 | url("../fonts/MaterialIcons-Regular.ttf") format("truetype"); 9 | } 10 | 11 | @font-face { 12 | font-family: Montserrat; 13 | src: url("../fonts/Montserrat-Regular.ttf") format("truetype"); 14 | } 15 | -------------------------------------------------------------------------------- /app/renderer/css/feedback.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --button-color: rgb(69 166 149); 3 | } 4 | 5 | button { 6 | background-color: var(--button-color); 7 | border-color: var(--button-color); 8 | } 9 | 10 | button:hover, 11 | button:focus { 12 | border-color: var(--button-color); 13 | color: var(--button-color); 14 | } 15 | 16 | button:active { 17 | background-color: rgb(241 241 241); 18 | color: var(--button-color); 19 | } 20 | -------------------------------------------------------------------------------- /public/translations/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "Delete": "Delete", 3 | "Desktop Notifications": "Desktop Notifications", 4 | "File": "File", 5 | "General": "General", 6 | "Help": "Help", 7 | "Manual proxy configuration": "Manual proxy configuration", 8 | "Network": "Network", 9 | "OR": "OR", 10 | "Proxy": "Proxy", 11 | "Release Notes": "Release Notes", 12 | "Tip": "Tip", 13 | "Upload": "Upload", 14 | "View": "View", 15 | "Zoom In": "Zoom In" 16 | } 17 | -------------------------------------------------------------------------------- /tools/fetch-pull-request: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | if ! git diff-index --quiet HEAD; then 6 | set +x 7 | echo "There are uncommitted changes:" 8 | git status --short 9 | echo "Doing nothing to avoid losing your work." 10 | exit 1 11 | fi 12 | request_id="$1" 13 | remote=${2:-"upstream"} 14 | git fetch "$remote" "pull/$request_id/head" 15 | git checkout -B "review-original-${request_id}" 16 | git reset --hard FETCH_HEAD 17 | -------------------------------------------------------------------------------- /app/renderer/js/pages/network.ts: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from "../typed-ipc-renderer.ts"; 2 | 3 | export function init( 4 | $reconnectButton: Element, 5 | $settingsButton: Element, 6 | ): void { 7 | $reconnectButton.addEventListener("click", () => { 8 | ipcRenderer.send("forward-message", "reload-viewer"); 9 | }); 10 | $settingsButton.addEventListener("click", () => { 11 | ipcRenderer.send("forward-message", "open-settings"); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /tools/fetch-rebase-pull-request: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | if ! git diff-index --quiet HEAD; then 6 | set +x 7 | echo "There are uncommitted changes:" 8 | git status --short 9 | echo "Doing nothing to avoid losing your work." 10 | exit 1 11 | fi 12 | request_id="$1" 13 | remote=${2:-"upstream"} 14 | git fetch "$remote" "pull/$request_id/head" 15 | git checkout -B "review-${request_id}" $remote/main 16 | git reset --hard FETCH_HEAD 17 | git pull --rebase 18 | -------------------------------------------------------------------------------- /app/common/translation-util.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import i18n from "i18n"; 4 | 5 | import * as ConfigUtil from "./config-util.ts"; 6 | import {publicPath} from "./paths.ts"; 7 | 8 | i18n.configure({ 9 | directory: path.join(publicPath, "translations/"), 10 | updateFiles: false, 11 | }); 12 | 13 | /* Fetches the current appLocale from settings.json */ 14 | i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en"); 15 | 16 | export {__, __mf} from "i18n"; 17 | -------------------------------------------------------------------------------- /app/renderer/js/utils/system-util.ts: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from "../typed-ipc-renderer.ts"; 2 | 3 | export const connectivityError: string[] = [ 4 | "ERR_INTERNET_DISCONNECTED", 5 | "ERR_PROXY_CONNECTION_FAILED", 6 | "ERR_CONNECTION_RESET", 7 | "ERR_NOT_CONNECTED", 8 | "ERR_NAME_NOT_RESOLVED", 9 | "ERR_NETWORK_CHANGED", 10 | ]; 11 | 12 | const userAgent = ipcRenderer.sendSync("fetch-user-agent"); 13 | 14 | export function getUserAgent(): string { 15 | return userAgent; 16 | } 17 | -------------------------------------------------------------------------------- /tools/reset-to-pull-request: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if ! git diff-index --quiet HEAD; then 5 | set +x 6 | echo "There are uncommitted changes:" 7 | git status --short 8 | echo "Doing nothing to avoid losing your work." 9 | exit 1 10 | fi 11 | 12 | remote_default="$(git config zulip.zulipRemote || echo upstream)" 13 | 14 | request_id="$1" 15 | remote=${2:-"$remote_default"} 16 | 17 | set -x 18 | git fetch "$remote" "pull/$request_id/head" 19 | git reset --hard FETCH_HEAD 20 | -------------------------------------------------------------------------------- /app/common/paths.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import process from "node:process"; 3 | import url from "node:url"; 4 | 5 | export const bundlePath = import.meta.dirname; 6 | 7 | export const publicPath = import.meta.env.DEV 8 | ? path.join(bundlePath, "../../public") 9 | : path.join(bundlePath, "../renderer"); 10 | 11 | export const bundleUrl = import.meta.env.DEV 12 | ? process.env.ELECTRON_RENDERER_URL + "/" 13 | : url.pathToFileURL(publicPath + "/").href; 14 | 15 | export const publicUrl = bundleUrl; 16 | -------------------------------------------------------------------------------- /i18next.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | import {defineConfig} from "i18next-cli"; 4 | 5 | export default defineConfig({ 6 | locales: ["en"], 7 | extract: { 8 | input: ["app/**/*.ts"], 9 | output: "public/translations/{{language}}.json", 10 | functions: ["t.__", "t.__mf"], 11 | defaultNS: false, 12 | keySeparator: false, 13 | nsSeparator: false, 14 | sort: (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0), 15 | indentation: "\t", 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /patches/gatemaker.patch: -------------------------------------------------------------------------------- 1 | diff --git a/electron-observer.js b/electron-observer.js 2 | index 993f390f83a374be61a7cf018cdef1ebb9c02415..57bf2868101af1be14408cec4a595322ebb3bb10 100644 3 | --- a/electron-observer.js 4 | +++ b/electron-observer.js 5 | @@ -23,7 +23,7 @@ function sessionWillDownload(event, download) { 6 | 7 | function downloadDone(event, state) { 8 | if (state != "completed") return 9 | - observer.emit("download-completed", event.sender) 10 | + observer.emit("download-completed", this) 11 | } 12 | 13 | module.exports = observer 14 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "attr-value-not-empty": false, 3 | "attr-no-duplication": true, 4 | "doctype-first": true, 5 | "spec-char-escape": true, 6 | "id-unique": true, 7 | "src-not-empty": true, 8 | "title-require": true, 9 | "alt-require": false, 10 | "doctype-html5": true, 11 | "id-class-value": "dash", 12 | "style-disabled": false, 13 | "inline-style-disabled": false, 14 | "inline-script-disabled": false, 15 | "id-class-ad-disabled": false, 16 | "href-abs-or-rel": false, 17 | "attr-unsafe-chars": true, 18 | "head-script-disabled": true 19 | } 20 | -------------------------------------------------------------------------------- /tools/fetch-pull-request.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | git diff-index --quiet HEAD 3 | if %ERRORLEVEL% NEQ 0 ( 4 | echo "There are uncommitted changes:" 5 | git status --short 6 | echo "Doing nothing to avoid losing your work." 7 | exit /B 1 8 | ) 9 | 10 | if "%~1"=="" ( 11 | echo "Error you must specify the PR number" 12 | ) 13 | 14 | if "%~2"=="" ( 15 | set remote="upstream" 16 | ) else ( 17 | set remote=%2 18 | ) 19 | 20 | set request_id="%1" 21 | git fetch "%remote%" "pull/%request_id%/head" 22 | git checkout -B "review-%request_id%" 23 | git reset --hard FETCH_HEAD 24 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | ignoredBuiltDependencies: 2 | - '@swc/core' 3 | - electron-winstaller 4 | - esbuild 5 | - fs-xattr 6 | - unrs-resolver 7 | 8 | onlyBuiltDependencies: 9 | - electron 10 | 11 | overrides: 12 | i18n>@messageformat/core: '-' 13 | 14 | packageExtensions: 15 | i18n: 16 | dependencies: 17 | intl-messageformat: ^10.7.18 18 | 19 | patchedDependencies: 20 | gatemaker: patches/gatemaker.patch # https://github.com/javan/gatemaker/pull/4 21 | i18n: patches/i18n.patch # https://github.com/mashpie/i18n-node/pull/546, https://github.com/mashpie/i18n-node/pull/547 22 | -------------------------------------------------------------------------------- /packaging/deb-after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' 5 | 6 | # SUID chrome-sandbox for Electron 5+ 7 | chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || true 8 | 9 | update-mime-database /usr/share/mime || true 10 | update-desktop-database /usr/share/applications || true 11 | 12 | # Clean up configuration for old Bintray repository 13 | rm -f /etc/apt/zulip.list 14 | 15 | # Clean up legacy APT configuration 16 | rm -f /etc/apt/sources.list.d/zulip-desktop.list /etc/apt/trusted.gpg.d/zulip-desktop.asc 17 | -------------------------------------------------------------------------------- /public/translations/te.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "జులిప్ గురించి", 3 | "Actual Size": "వాస్తవ పరిమాణం", 4 | "Add Organization": "సంస్థను జోడించు", 5 | "Add a Zulip organization": "జులిప్ సంస్థను జోడించు", 6 | "Add custom CSS": "అనుకూల CSS ను జోడించు", 7 | "Advanced": "ఉన్నతస్థాయి", 8 | "All the connected organizations will appear here.": "కనెక్ట్ చేయబడిన అన్ని సంస్థలు ఇక్కడ కనిపిస్తాయి.", 9 | "Always start minimized": "ఎల్లప్పుడూ తగ్గించబడి ప్రారంభించండి", 10 | "App Updates": "యాప్ అప్‌డేట్‌లు", 11 | "App language (requires restart)": "యాప్ భాష (పునఃప్రారంభం అవసరం)", 12 | "Appearance": "ప్రదర్శన" 13 | } 14 | -------------------------------------------------------------------------------- /tools/fetch-rebase-pull-request.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | git diff-index --quiet HEAD 3 | if %errorlevel% neq 0 ( 4 | echo "There are uncommitted changes:" 5 | git status --short 6 | echo "Doing nothing to avoid losing your work." 7 | exit \B 1 8 | ) 9 | 10 | if "%~1"=="" ( 11 | echo "Error you must specify the PR number" 12 | ) 13 | 14 | if "%~2"=="" ( 15 | set remote="upstream" 16 | ) else ( 17 | set remote=%2 18 | ) 19 | 20 | set request_id="%1" 21 | git fetch "%remote%" "pull/%request_id%/head" 22 | git checkout -B "review-%request_id%" %remote%/main 23 | git reset --hard FETCH_HEAD 24 | git pull --rebase 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "allowImportingTsExtensions": true, 8 | "esModuleInterop": true, 9 | "paths": { 10 | // https://github.com/getsentry/sentry-electron/issues/957 11 | "@sentry/node/build/types/integrations/anr/common": [ 12 | "./node_modules/@sentry/node/build/types/integrations/anr/common" 13 | ] 14 | }, 15 | "resolveJsonModule": true, 16 | "strict": true, 17 | "noImplicitOverride": true, 18 | "types": ["vite/client"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Problem Description** 7 | 8 | 9 | 10 | **Proposed Solution** 11 | 12 | 13 | 14 | **Describe alternatives you've considered** 15 | 16 | 17 | 18 | **Additional context** 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/renderer/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Zulip 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: pnpm/action-setup@v4 21 | with: 22 | run_install: false 23 | - uses: actions/setup-node@v6 24 | with: 25 | node-version: lts/* 26 | cache: pnpm 27 | - run: pnpm install --frozen-lockfile 28 | - run: node --run test 29 | -------------------------------------------------------------------------------- /app/common/types.ts: -------------------------------------------------------------------------------- 1 | export type MenuProperties = { 2 | tabs: TabData[]; 3 | activeTabIndex?: number; 4 | enableMenu?: boolean; 5 | }; 6 | 7 | export type NavigationItem = 8 | | "General" 9 | | "Network" 10 | | "AddServer" 11 | | "Organizations" 12 | | "Shortcuts"; 13 | 14 | export type ServerConfig = { 15 | url: string; 16 | alias: string; 17 | icon: string; 18 | zulipVersion: string; 19 | zulipFeatureLevel: number; 20 | }; 21 | 22 | export type TabRole = "server" | "function"; 23 | export type TabPage = "Settings" | "About"; 24 | 25 | export type TabData = { 26 | role: TabRole; 27 | page?: TabPage; 28 | label: string; 29 | index: number; 30 | }; 31 | -------------------------------------------------------------------------------- /app/common/html.ts: -------------------------------------------------------------------------------- 1 | import {htmlEscape} from "escape-goat"; 2 | 3 | export class Html { 4 | html: string; 5 | 6 | constructor({html}: {html: string}) { 7 | this.html = html; 8 | } 9 | 10 | join(htmls: readonly Html[]): Html { 11 | return new Html({html: htmls.map((html) => html.html).join(this.html)}); 12 | } 13 | } 14 | 15 | export function html( 16 | template: TemplateStringsArray, 17 | ...values: unknown[] 18 | ): Html { 19 | let html = template[0]; 20 | for (const [index, value] of values.entries()) { 21 | html += value instanceof Html ? value.html : htmlEscape(String(value)); 22 | html += template[index + 1]; 23 | } 24 | 25 | return new Html({html}); 26 | } 27 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import Fifo from "p-fifo"; 2 | import type {Page} from "playwright-core"; 3 | import test from "tape"; 4 | 5 | import * as setup from "./setup.ts"; 6 | 7 | test("app runs", async (t) => { 8 | t.timeoutAfter(10e3); 9 | setup.resetTestDataDirectory(); 10 | const app = await setup.createApp(); 11 | try { 12 | const windows = new Fifo(); 13 | for (const win of app.windows()) void windows.push(win); 14 | app.on("window", async (win) => windows.push(win)); 15 | 16 | const mainWindow = await windows.shift(); 17 | t.equal(await mainWindow.title(), "Zulip"); 18 | 19 | await mainWindow.waitForSelector("#connect"); 20 | } finally { 21 | await setup.endTest(app); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /tests/test-new-organization.ts: -------------------------------------------------------------------------------- 1 | import Fifo from "p-fifo"; 2 | import type {Page} from "playwright-core"; 3 | import test from "tape"; 4 | 5 | import * as setup from "./setup.ts"; 6 | 7 | // Create new org link should open in the default browser [WIP] 8 | 9 | test("new-org-link", async (t) => { 10 | t.timeoutAfter(50e3); 11 | setup.resetTestDataDirectory(); 12 | const app = await setup.createApp(); 13 | try { 14 | const windows = new Fifo(); 15 | for (const win of app.windows()) void windows.push(win); 16 | app.on("window", async (win) => windows.push(win)); 17 | 18 | const mainWindow = await windows.shift(); 19 | t.equal(await mainWindow.title(), "Zulip"); 20 | 21 | await mainWindow.click("#open-create-org-link"); 22 | } finally { 23 | await setup.endTest(app); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | 8 | 9 | 10 | **To Reproduce** 11 | 12 | 13 | 14 | **Expected behavior** 15 | 16 | 17 | 18 | **Screenshots** 19 | 20 | 21 | 22 | **Desktop (please complete the following information):** 23 | 24 | - Operating System: 25 | 26 | - Zulip Desktop Version: 27 | 28 | 29 | **Additional context** 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/main/sentry.ts: -------------------------------------------------------------------------------- 1 | import {app} from "electron/main"; 2 | 3 | import * as Sentry from "@sentry/electron/main"; 4 | 5 | import {getConfigItem} from "../common/config-util.ts"; 6 | 7 | export const sentryInit = (): void => { 8 | Sentry.init({ 9 | dsn: "https://628dc2f2864243a08ead72e63f94c7b1@o48127.ingest.sentry.io/204668", 10 | 11 | // Don't report errors in development or if disabled by the user. 12 | beforeSend: (event) => 13 | app.isPackaged && getConfigItem("errorReporting", true) ? event : null, 14 | 15 | // We should ignore this error since it's harmless and we know the reason behind this 16 | // This error mainly comes from the console logs. 17 | // This is a temp solution until Sentry supports disabling the console logs 18 | ignoreErrors: ["does not appear to be a valid Zulip server"], 19 | 20 | /// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /tests/test-add-organization.ts: -------------------------------------------------------------------------------- 1 | import Fifo from "p-fifo"; 2 | import type {Page} from "playwright-core"; 3 | import test from "tape"; 4 | 5 | import * as setup from "./setup.ts"; 6 | 7 | test("add-organization", async (t) => { 8 | t.timeoutAfter(50e3); 9 | setup.resetTestDataDirectory(); 10 | const app = await setup.createApp(); 11 | try { 12 | const windows = new Fifo(); 13 | for (const win of app.windows()) void windows.push(win); 14 | app.on("window", async (win) => windows.push(win)); 15 | 16 | const mainWindow = await windows.shift(); 17 | t.equal(await mainWindow.title(), "Zulip"); 18 | 19 | await mainWindow.fill( 20 | ".setting-input-value", 21 | "zulip-desktop-test.zulipchat.com", 22 | ); 23 | await mainWindow.click("#connect"); 24 | 25 | const orgWebview = await windows.shift(); 26 | await orgWebview.waitForSelector("#id_username"); 27 | } finally { 28 | await setup.endTest(app); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/servers-section.ts: -------------------------------------------------------------------------------- 1 | import {html} from "../../../../common/html.ts"; 2 | import * as t from "../../../../common/translation-util.ts"; 3 | 4 | import {reloadApp} from "./base-section.ts"; 5 | import {initNewServerForm} from "./new-server-form.ts"; 6 | 7 | type ServersSectionProperties = { 8 | $root: Element; 9 | }; 10 | 11 | export function initServersSection({$root}: ServersSectionProperties): void { 12 | $root.innerHTML = html` 13 |
14 | 20 |
21 | `.html; 22 | const $newServerContainer = $root.querySelector("#new-server-container")!; 23 | 24 | initNewServerForm({ 25 | $root: $newServerContainer, 26 | onChange: reloadApp, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /app/main/startup.ts: -------------------------------------------------------------------------------- 1 | import {app} from "electron/main"; 2 | import process from "node:process"; 3 | 4 | import AutoLaunch from "auto-launch"; 5 | 6 | import * as ConfigUtil from "../common/config-util.ts"; 7 | 8 | export const setAutoLaunch = async ( 9 | AutoLaunchValue: boolean, 10 | ): Promise => { 11 | // Don't run this in development 12 | if (!app.isPackaged) { 13 | return; 14 | } 15 | 16 | const autoLaunchOption = ConfigUtil.getConfigItem( 17 | "startAtLogin", 18 | AutoLaunchValue, 19 | ); 20 | 21 | // `setLoginItemSettings` doesn't support linux 22 | if (process.platform === "linux") { 23 | const zulipAutoLauncher = new AutoLaunch({ 24 | name: "Zulip", 25 | isHidden: false, 26 | }); 27 | await (autoLaunchOption 28 | ? zulipAutoLauncher.enable() 29 | : zulipAutoLauncher.disable()); 30 | } else { 31 | app.setLoginItemSettings({ 32 | openAtLogin: autoLaunchOption, 33 | openAsHidden: false, 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/renderer/img/ic_loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/translations/eu.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "Zulip-i buruz", 3 | "Actual Size": "Egungo tamaina", 4 | "Add Organization": "Gehitu erakundea", 5 | "Add a Zulip organization": "Gehitu Zulip erakunde bat", 6 | "Advanced": "Aurreratua", 7 | "Appearance": "Itxura", 8 | "Back": "Itzuli", 9 | "Change": "Aldatu", 10 | "Close": "Itxi", 11 | "Connect": "Konektatu", 12 | "Copy": "Kopiatu", 13 | "Copy Zulip URL": "Kopiatu Zulip URL-a", 14 | "Create a new organization": "Sortu erakunde berriia", 15 | "Cut": "Moztu", 16 | "Delete": "Desegin", 17 | "Desktop Notifications": "Mahaigaineko jakinarazpenak", 18 | "Disconnect": "Deskonektatu", 19 | "Edit": "Editatu", 20 | "General": "Orokorra", 21 | "Help": "Laguntza", 22 | "Help Center": "Laguntza gunea", 23 | "History": "Historia", 24 | "Network": "Sarea", 25 | "OR": "EDO", 26 | "Organization URL": "Erakundearen URL-a", 27 | "Organizations": "Erakundeak", 28 | "Paste": "Itsatsi", 29 | "Quit": "Irten", 30 | "Save": "Gorde", 31 | "Undo": "Desegin", 32 | "Upload": "Igo", 33 | "View": "Ikusi" 34 | } 35 | -------------------------------------------------------------------------------- /app/renderer/js/preload.ts: -------------------------------------------------------------------------------- 1 | import {contextBridge} from "electron/renderer"; 2 | 3 | import electron_bridge, {BridgeEvent, bridgeEvents} from "./electron-bridge.ts"; 4 | import * as NetworkError from "./pages/network.ts"; 5 | import {ipcRenderer} from "./typed-ipc-renderer.ts"; 6 | 7 | contextBridge.exposeInMainWorld("electron_bridge", electron_bridge); 8 | 9 | ipcRenderer.on("logout", () => { 10 | bridgeEvents.dispatchEvent(new BridgeEvent("logout")); 11 | }); 12 | 13 | ipcRenderer.on("show-keyboard-shortcuts", () => { 14 | bridgeEvents.dispatchEvent(new BridgeEvent("show-keyboard-shortcuts")); 15 | }); 16 | 17 | ipcRenderer.on("show-notification-settings", () => { 18 | bridgeEvents.dispatchEvent(new BridgeEvent("show-notification-settings")); 19 | }); 20 | 21 | window.addEventListener("load", () => { 22 | if (!location.href.includes("app/renderer/network.html")) { 23 | return; 24 | } 25 | 26 | const $reconnectButton = document.querySelector("#reconnect")!; 27 | const $settingsButton = document.querySelector("#settings")!; 28 | NetworkError.init($reconnectButton, $settingsButton); 29 | }); 30 | -------------------------------------------------------------------------------- /public/translations/README.md: -------------------------------------------------------------------------------- 1 | # How to help translate Zulip Desktop 2 | 3 | These are _generated_ files (\*) that contain translations of the strings in 4 | the app. 5 | 6 | You can help translate Zulip Desktop into your language! We do our 7 | translations in Weblate, which is a nice web app for collaborating on 8 | translations; a maintainer then syncs those translations into this repo. 9 | To help out, [join the Zulip project on 10 | Weblate](https://hosted.weblate.org/projects/zulip/) and enter translations 11 | there. More details in the [Zulip contributor docs](https://zulip.readthedocs.io/en/latest/translating/translating.html#translators-workflow). 12 | 13 | Within that Weblate project, if you'd like to focus on Zulip Desktop, look 14 | at the **Desktop** component. The other components are for the Zulip web/mobile 15 | app, where translations are also very welcome. 16 | 17 | (\*) One file is an exception: `en.json` is maintained by `i18next-cli extract` as a 18 | list of (English) messages in the source code, and is used by Weblate as 19 | a list of strings to be translated. It doesn't contain any 20 | translations. 21 | -------------------------------------------------------------------------------- /app/renderer/css/network.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | cursor: default; 5 | font-size: 14px; 6 | color: rgb(51 51 51 / 100%); 7 | background: rgb(255 255 255 / 100%); 8 | user-select: none; 9 | } 10 | 11 | #content { 12 | display: flex; 13 | flex-direction: column; 14 | font-family: "Trebuchet MS", Helvetica, sans-serif; 15 | margin: 100px 200px; 16 | text-align: center; 17 | } 18 | 19 | #title { 20 | text-align: left; 21 | font-size: 24px; 22 | font-weight: bold; 23 | margin: 20px 0; 24 | } 25 | 26 | #subtitle { 27 | font-size: 20px; 28 | text-align: left; 29 | margin: 12px 0; 30 | } 31 | 32 | #description { 33 | text-align: left; 34 | font-size: 16px; 35 | list-style-position: inside; 36 | } 37 | 38 | #reconnect { 39 | float: left; 40 | } 41 | 42 | #settings { 43 | margin-left: 116px; 44 | } 45 | 46 | .button { 47 | font-size: 16px; 48 | background: rgb(0 150 136 / 100%); 49 | color: rgb(255 255 255 / 100%); 50 | width: 96px; 51 | height: 32px; 52 | border-radius: 5px; 53 | line-height: 32px; 54 | cursor: pointer; 55 | } 56 | 57 | .button:hover { 58 | opacity: 0.8; 59 | } 60 | -------------------------------------------------------------------------------- /app/renderer/js/notification/index.ts: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from "../typed-ipc-renderer.ts"; 2 | 3 | export type NotificationData = { 4 | close: () => void; 5 | title: string; 6 | dir: NotificationDirection; 7 | lang: string; 8 | body: string; 9 | tag: string; 10 | icon: string; 11 | data: unknown; 12 | }; 13 | 14 | export function newNotification( 15 | title: string, 16 | options: NotificationOptions, 17 | dispatch: (type: string, eventInit: EventInit) => boolean, 18 | ): NotificationData { 19 | const notification = new Notification(title, {...options, silent: true}); 20 | for (const type of ["click", "close", "error", "show"]) { 21 | notification.addEventListener(type, (event) => { 22 | if (type === "click") ipcRenderer.send("focus-this-webview"); 23 | if (!dispatch(type, event)) { 24 | event.preventDefault(); 25 | } 26 | }); 27 | } 28 | 29 | return { 30 | close() { 31 | notification.close(); 32 | }, 33 | title: notification.title, 34 | dir: notification.dir, 35 | lang: notification.lang, 36 | body: notification.body, 37 | tag: notification.tag, 38 | icon: notification.icon, 39 | data: notification.data, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /app/renderer/css/about.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: strict; 3 | display: flow-root; 4 | background: rgb(250 250 250 / 100%); 5 | font-family: menu, "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: subpixel-antialiased; 7 | } 8 | 9 | .logo { 10 | display: block; 11 | margin: -40px auto; 12 | } 13 | 14 | #version { 15 | color: rgb(68 67 67 / 100%); 16 | font-size: 1.3em; 17 | padding-top: 40px; 18 | } 19 | 20 | .about { 21 | display: block !important; 22 | margin: 25vh auto; 23 | height: 25vh; 24 | text-align: center; 25 | } 26 | 27 | .about p { 28 | font-size: 20px; 29 | color: rgb(0 0 0 / 62%); 30 | } 31 | 32 | .about img { 33 | width: 150px; 34 | } 35 | 36 | .detail { 37 | text-align: center; 38 | } 39 | 40 | .detail.maintainer { 41 | font-size: 1.2em; 42 | font-weight: 500; 43 | } 44 | 45 | .detail.license { 46 | font-size: 0.8em; 47 | } 48 | 49 | .maintenance-info { 50 | position: absolute; 51 | width: 100%; 52 | left: 0; 53 | color: rgb(68 68 68 / 100%); 54 | } 55 | 56 | .maintenance-info p { 57 | margin: 0; 58 | font-size: 1em; 59 | width: 100%; 60 | } 61 | 62 | p.detail a { 63 | color: rgb(53 95 76 / 100%); 64 | } 65 | 66 | p.detail a:hover { 67 | text-decoration: underline; 68 | } 69 | -------------------------------------------------------------------------------- /app/renderer/js/components/tab.ts: -------------------------------------------------------------------------------- 1 | import type {TabPage, TabRole} from "../../../common/types.ts"; 2 | 3 | export type TabProperties = { 4 | role: TabRole; 5 | page?: TabPage; 6 | icon?: string; 7 | label: string; 8 | $root: Element; 9 | onClick: () => void; 10 | index: number; 11 | tabIndex: number; 12 | onHover?: () => void; 13 | onHoverOut?: () => void; 14 | materialIcon?: string; 15 | onDestroy?: () => void; 16 | }; 17 | 18 | export default abstract class Tab { 19 | abstract $el: Element; 20 | 21 | constructor(readonly properties: TabProperties) {} 22 | 23 | registerListeners(): void { 24 | this.$el.addEventListener("click", this.properties.onClick); 25 | 26 | if (this.properties.onHover !== undefined) { 27 | this.$el.addEventListener("mouseover", this.properties.onHover); 28 | } 29 | 30 | if (this.properties.onHoverOut !== undefined) { 31 | this.$el.addEventListener("mouseout", this.properties.onHoverOut); 32 | } 33 | } 34 | 35 | async activate(): Promise { 36 | this.$el.classList.add("active"); 37 | } 38 | 39 | async deactivate(): Promise { 40 | this.$el.classList.remove("active"); 41 | } 42 | 43 | async destroy(): Promise { 44 | this.$el.remove(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/translations/supported-locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "bqi": "Bakhtiari", 3 | "ca": "Català", 4 | "cs": "Čeština", 5 | "cy": "Cymraeg", 6 | "da": "Dansk", 7 | "de": "Deutsch", 8 | "en_GB": "English (United Kingdom)", 9 | "en": "English (United States)", 10 | "es": "Español", 11 | "eu": "Euskara", 12 | "fr": "Français", 13 | "gl": "Galego", 14 | "id": "Indonesia", 15 | "it": "Italiano", 16 | "lv": "Latviešu", 17 | "lt": "Lietuvių", 18 | "hu": "Magyar", 19 | "nl": "Nederlands", 20 | "pl": "Polski", 21 | "pt": "Português", 22 | "pt_PT": "Português (Portugal)", 23 | "ro": "Română", 24 | "sk": "Slovenčina", 25 | "sl": "Slovenščina", 26 | "fi": "Suomi", 27 | "sv": "Svenska", 28 | "vi": "Tiếng Việt", 29 | "tr": "Türkçe", 30 | "el": "Ελληνικά", 31 | "be": "Беларуская", 32 | "bg": "Български", 33 | "mn": "Монгол", 34 | "ru": "Русский", 35 | "sr": "Српски", 36 | "uk": "Українська", 37 | "ar": "العربية", 38 | "fa": "فارسی", 39 | "hi": "हिन्दी", 40 | "bn": "বাংলা", 41 | "gu": "ગુજરાતી", 42 | "ta": "தமிழ்", 43 | "te": "తెలుగు", 44 | "ml": "മലയാളം", 45 | "si": "සිංහල", 46 | "ko": "한국어", 47 | "zh_TW": "中文 (台灣)", 48 | "zh-Hans": "中文 (简体)", 49 | "ja": "日本語" 50 | } 51 | -------------------------------------------------------------------------------- /docs/howto/translations.md: -------------------------------------------------------------------------------- 1 | # Managing translations 2 | 3 | A person using the Zulip app can choose from a large number of 4 | languages for the app to present its UI in. 5 | 6 | Within the running app, we use the library `i18n` to get the 7 | appropriate translation for a given string ("message") used in the UI. 8 | 9 | To manage the set of UI messages and translations for them, and 10 | provide a nice workflow for people to contribute translations, we use 11 | (along with the rest of the Zulip project) a service called Weblate. 12 | 13 | ### Updating the languages supported in the code 14 | 15 | Sometimes when downloading translated strings we get a file for a new 16 | language. This happens when we've opened up a new language for people 17 | to contribute translations into in the Zulip project on Weblate, 18 | which we do when someone expresses interest in contributing them. 19 | 20 | The locales for supported languages are stored in `public/translations/supported-locales.json` 21 | 22 | So, when a new language is added, update the `supported-locales` module. 23 | 24 | ### Updating the languages offered in the UI 25 | 26 | The `supported-locales.json` module is also responsible for the language dropsdown ont he settings page. 27 | Maintainers/contributors only need to add the locale to the new language which would result in addition of it to the dropdown automatically. 28 | -------------------------------------------------------------------------------- /app/renderer/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Zulip - Network Troubleshooting 11 | 17 | 18 | 19 |
20 |
21 |
We can't connect to this organization
22 |
This could be because
23 |
    24 |
  • You're not online or your proxy is misconfigured.
  • 25 |
  • There is no Zulip organization hosted at this URL.
  • 26 |
  • This Zulip organization is temporarily unavailable.
  • 27 |
  • This Zulip organization has been moved or deleted.
  • 28 |
29 |
30 |
Reconnect
31 |
Settings
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | import {defineConfig} from "electron-vite"; 4 | 5 | export default defineConfig({ 6 | main: { 7 | build: { 8 | sourcemap: true, 9 | rollupOptions: { 10 | input: { 11 | index: "app/main/index.ts", 12 | }, 13 | external: ["electron", /^electron\//, /^gatemaker\//], 14 | }, 15 | }, 16 | resolve: { 17 | alias: { 18 | "zulip:remote": "electron/main", 19 | }, 20 | }, 21 | }, 22 | preload: { 23 | build: { 24 | sourcemap: "inline", 25 | rollupOptions: { 26 | input: { 27 | preload: "app/renderer/js/preload.ts", 28 | renderer: "app/renderer/js/main.ts", 29 | }, 30 | output: { 31 | format: "cjs", 32 | }, 33 | external: ["electron", /^electron\//], 34 | }, 35 | isolatedEntries: true, 36 | }, 37 | resolve: { 38 | alias: { 39 | "zulip:remote": "@electron/remote", 40 | }, 41 | }, 42 | }, 43 | renderer: { 44 | build: { 45 | sourcemap: true, 46 | rollupOptions: { 47 | input: { 48 | renderer: "app/renderer/main.html", 49 | network: "app/renderer/network.html", 50 | about: "app/renderer/about.html", 51 | preference: "app/renderer/preference.html", 52 | }, 53 | }, 54 | }, 55 | root: ".", 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /app/common/messages.ts: -------------------------------------------------------------------------------- 1 | import * as t from "./translation-util.ts"; 2 | 3 | type DialogBoxError = { 4 | title: string; 5 | content: string; 6 | }; 7 | 8 | export function invalidZulipServerError(domain: string): string { 9 | return `${domain} does not appear to be a valid Zulip server. Make sure that 10 | • You can connect to that URL in a web browser. 11 | • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings. 12 | • It's a Zulip server. (The oldest supported version is 1.6). 13 | • The server has a valid certificate. 14 | • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide - 15 | https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`; 16 | } 17 | 18 | export function enterpriseOrgError(domains: string[]): DialogBoxError { 19 | let domainList = ""; 20 | for (const domain of domains) { 21 | domainList += `• ${domain}\n`; 22 | } 23 | 24 | return { 25 | title: t.__mf( 26 | "{number, plural, one {Could not add # organization} other {Could not add # organizations}}", 27 | {number: domains.length}, 28 | ), 29 | content: `${domainList}\n${t.__("Please contact your system administrator.")}`, 30 | }; 31 | } 32 | 33 | export function orgRemovalError(url: string): DialogBoxError { 34 | return { 35 | title: t.__("Removing {{{url}}} is a restricted operation.", {url}), 36 | content: t.__("Please contact your system administrator."), 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /docs/Enterprise.md: -------------------------------------------------------------------------------- 1 | # Configuring Zulip Desktop for multiple users 2 | 3 | If you're a system admin and want to add certain organizations to the Zulip app for 4 | all users of your system, you can do so by creating an enterprise config file. 5 | The file should be placed at `/etc/zulip-desktop-config` for Linux and macOS computers 6 | and inside `C:\Program Files\Zulip-Desktop-Config` on Windows. 7 | It must be named `global_config.json` in both cases. 8 | 9 | To specify the preset organization you want to add for other users, you will need to 10 | add the `json` shown below to the `global_config.json`. Replace `https://chat.zulip.org` with the 11 | organization you want to add. You can also specify multiple organizations. 12 | 13 | ```json 14 | { 15 | "presetOrganizations": ["https://chat.zulip.org"], 16 | "autoUpdate": false 17 | } 18 | ``` 19 | 20 | The above example adds [Zulip Community](https://chat.zulip.org) to Zulip every time the app is loaded. 21 | Users can add new organizations at all times, but cannot remove any organizations listed under `presetOrganizations`. 22 | 23 | If you'd like to remove organizations and have admin access, you'll need to change the config file and remove the concerned URL from the `value` field. 24 | 25 | It also turns off automatic updates for every Zulip user on the same machine. 26 | 27 | Currently, we only support `presetOrganizations` and `autoUpdate` settings. We are working on other settings as well, and will update this page when we add support for more. 28 | -------------------------------------------------------------------------------- /app/common/config-schemata.ts: -------------------------------------------------------------------------------- 1 | import {z} from "zod"; 2 | 3 | export const dndSettingsSchemata = { 4 | showNotification: z.boolean(), 5 | silent: z.boolean(), 6 | flashTaskbarOnMessage: z.boolean(), 7 | }; 8 | 9 | export const configSchemata = { 10 | ...dndSettingsSchemata, 11 | appLanguage: z.string().nullable(), 12 | autoHideMenubar: z.boolean(), 13 | autoUpdate: z.boolean(), 14 | badgeOption: z.boolean(), 15 | betaUpdate: z.boolean(), 16 | // eslint-disable-next-line @typescript-eslint/naming-convention 17 | customCSS: z.string().or(z.literal(false)).nullable(), 18 | dnd: z.boolean(), 19 | dndPreviousSettings: z.object(dndSettingsSchemata).partial(), 20 | dockBouncing: z.boolean(), 21 | downloadsPath: z.string(), 22 | enableSpellchecker: z.boolean(), 23 | errorReporting: z.boolean(), 24 | lastActiveTab: z.number(), 25 | promptDownload: z.boolean(), 26 | proxyBypass: z.string(), 27 | // eslint-disable-next-line @typescript-eslint/naming-convention 28 | proxyPAC: z.string(), 29 | proxyRules: z.string(), 30 | quitOnClose: z.boolean(), 31 | showSidebar: z.boolean(), 32 | spellcheckerLanguages: z.string().array().nullable(), 33 | startAtLogin: z.boolean(), 34 | startMinimized: z.boolean(), 35 | trayIcon: z.boolean(), 36 | useManualProxy: z.boolean(), 37 | useProxy: z.boolean(), 38 | useSystemProxy: z.boolean(), 39 | }; 40 | export type ConfigSchemata = typeof configSchemata; 41 | 42 | export const enterpriseConfigSchemata = { 43 | ...configSchemata, 44 | presetOrganizations: z.string().array(), 45 | }; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zulip Desktop Client 2 | 3 | [![Build Status](https://github.com/zulip/zulip-desktop/actions/workflows/node.js.yml/badge.svg)](https://github.com/zulip/zulip-desktop/actions/workflows/node.js.yml?query=branch%3Amain) 4 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 5 | [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org) 6 | 7 | Desktop client for Zulip. Available for Mac, Linux, and Windows. 8 | 9 | ![screenshot](https://i.imgur.com/s1o6TRA.png) 10 | ![screenshot](https://i.imgur.com/vekKnW4.png) 11 | 12 | # Download 13 | 14 | Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide). 15 | 16 | # Features 17 | 18 | - Sign in to multiple organizations 19 | - Desktop notifications with inline reply 20 | - Tray/dock integration 21 | - Multi-language spell checker 22 | - Automatic updates 23 | 24 | # Reporting issues 25 | 26 | This desktop client shares most of its code with the Zulip web app. 27 | Issues in an individual organization's Zulip window should be reported 28 | in the [Zulip server and web app 29 | project](https://github.com/zulip/zulip/issues/new). Other 30 | issues in the desktop app and its settings should be reported [in this 31 | project](https://github.com/zulip/zulip-desktop/issues/new). 32 | 33 | # Contribute 34 | 35 | First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)! 36 | Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md). 37 | 38 | # License 39 | 40 | Released under the [Apache-2.0](./LICENSE) license. 41 | -------------------------------------------------------------------------------- /app/common/link-util.ts: -------------------------------------------------------------------------------- 1 | import {shell} from "electron/common"; 2 | import fs from "node:fs"; 3 | import os from "node:os"; 4 | import path from "node:path"; 5 | 6 | import {Html, html} from "./html.ts"; 7 | import * as t from "./translation-util.ts"; 8 | 9 | export async function openBrowser(url: URL): Promise { 10 | if (["http:", "https:", "mailto:"].includes(url.protocol)) { 11 | await shell.openExternal(url.href); 12 | } else { 13 | // For security, indirect links to non-whitelisted protocols 14 | // through a real web browser via a local HTML file. 15 | const directory = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-")); 16 | const file = path.join(directory, "redirect.html"); 17 | fs.writeFileSync( 18 | file, 19 | html` 20 | 21 | 22 | 23 | 24 | 25 | ${t.__("Redirecting")} 26 | 31 | 32 | 33 |

34 | ${new Html({ 35 | html: t.__("Opening {{{link}}}…", { 36 | link: html`${url.href}`.html, 37 | }), 38 | })} 39 |

40 | 41 | 42 | `.html, 43 | ); 44 | await shell.openPath(file); 45 | setTimeout(() => { 46 | fs.unlinkSync(file); 47 | fs.rmdirSync(directory); 48 | }, 15_000); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/main/badge-settings.ts: -------------------------------------------------------------------------------- 1 | import {nativeImage} from "electron/common"; 2 | import {type BrowserWindow, app} from "electron/main"; 3 | import process from "node:process"; 4 | 5 | import * as ConfigUtil from "../common/config-util.ts"; 6 | 7 | import {send} from "./typed-ipc-main.ts"; 8 | 9 | function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void { 10 | if (process.platform === "win32") { 11 | updateOverlayIcon(messageCount, mainWindow); 12 | } else { 13 | app.badgeCount = messageCount; 14 | } 15 | } 16 | 17 | function hideBadgeCount(mainWindow: BrowserWindow): void { 18 | if (process.platform === "win32") { 19 | mainWindow.setOverlayIcon(null, ""); 20 | } else { 21 | app.badgeCount = 0; 22 | } 23 | } 24 | 25 | export function updateBadge( 26 | badgeCount: number, 27 | mainWindow: BrowserWindow, 28 | ): void { 29 | if (ConfigUtil.getConfigItem("badgeOption", true)) { 30 | showBadgeCount(badgeCount, mainWindow); 31 | } else { 32 | hideBadgeCount(mainWindow); 33 | } 34 | } 35 | 36 | function updateOverlayIcon( 37 | messageCount: number, 38 | mainWindow: BrowserWindow, 39 | ): void { 40 | if (!mainWindow.isFocused()) { 41 | mainWindow.flashFrame( 42 | ConfigUtil.getConfigItem("flashTaskbarOnMessage", true), 43 | ); 44 | } 45 | 46 | if (messageCount === 0) { 47 | mainWindow.setOverlayIcon(null, ""); 48 | } else { 49 | send(mainWindow.webContents, "render-taskbar-icon", messageCount); 50 | } 51 | } 52 | 53 | export function updateTaskbarIcon( 54 | data: string, 55 | text: string, 56 | mainWindow: BrowserWindow, 57 | ): void { 58 | const img = nativeImage.createFromDataURL(data); 59 | mainWindow.setOverlayIcon(img, text); 60 | } 61 | -------------------------------------------------------------------------------- /app/common/dnd-util.ts: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | 3 | import type {z} from "zod"; 4 | 5 | import type {dndSettingsSchemata} from "./config-schemata.ts"; 6 | import * as ConfigUtil from "./config-util.ts"; 7 | 8 | export type DndSettings = { 9 | [Key in keyof typeof dndSettingsSchemata]: z.output< 10 | (typeof dndSettingsSchemata)[Key] 11 | >; 12 | }; 13 | 14 | type SettingName = keyof DndSettings; 15 | 16 | type Toggle = { 17 | dnd: boolean; 18 | newSettings: Partial; 19 | }; 20 | 21 | export function toggle(): Toggle { 22 | const dnd = !ConfigUtil.getConfigItem("dnd", false); 23 | const dndSettingList: SettingName[] = ["showNotification", "silent"]; 24 | if (process.platform === "win32") { 25 | dndSettingList.push("flashTaskbarOnMessage"); 26 | } 27 | 28 | let newSettings: Partial; 29 | if (dnd) { 30 | const oldSettings: Partial = {}; 31 | newSettings = {}; 32 | 33 | // Iterate through the dndSettingList. 34 | for (const settingName of dndSettingList) { 35 | // Store the current value of setting. 36 | oldSettings[settingName] = ConfigUtil.getConfigItem( 37 | settingName, 38 | settingName !== "silent", 39 | ); 40 | // New value of setting. 41 | newSettings[settingName] = settingName === "silent"; 42 | } 43 | 44 | // Store old value in oldSettings. 45 | ConfigUtil.setConfigItem("dndPreviousSettings", oldSettings); 46 | } else { 47 | newSettings = ConfigUtil.getConfigItem("dndPreviousSettings", { 48 | showNotification: true, 49 | silent: false, 50 | ...(process.platform === "win32" && {flashTaskbarOnMessage: true}), 51 | }); 52 | } 53 | 54 | for (const settingName of dndSettingList) { 55 | ConfigUtil.setConfigItem(settingName, newSettings[settingName]!); 56 | } 57 | 58 | ConfigUtil.setConfigItem("dnd", dnd); 59 | return {dnd, newSettings}; 60 | } 61 | -------------------------------------------------------------------------------- /app/renderer/js/pages/about.ts: -------------------------------------------------------------------------------- 1 | import {app} from "@electron/remote"; 2 | 3 | import {Html, html} from "../../../common/html.ts"; 4 | import {bundleUrl} from "../../../common/paths.ts"; 5 | import * as t from "../../../common/translation-util.ts"; 6 | import {generateNodeFromHtml} from "../components/base.ts"; 7 | 8 | export class AboutView { 9 | static async create(): Promise { 10 | return new AboutView( 11 | await (await fetch(new URL("app/renderer/about.html", bundleUrl))).text(), 12 | ); 13 | } 14 | 15 | readonly $view: HTMLElement; 16 | 17 | private constructor(templateHtml: string) { 18 | this.$view = document.createElement("div"); 19 | const $shadow = this.$view.attachShadow({mode: "open"}); 20 | $shadow.innerHTML = templateHtml; 21 | $shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`; 22 | const maintenanceInfoHtml = html` 23 |
24 |

25 | ${new Html({ 26 | html: t.__("Maintained by {{{link}}}Zulip{{{endLink}}}", { 27 | link: '', 28 | endLink: "", 29 | }), 30 | })} 31 |

32 |

33 | ${new Html({ 34 | html: t.__( 35 | "Available under the {{{link}}}Apache 2.0 License{{{endLink}}}", 36 | { 37 | link: '', 38 | endLink: "", 39 | }, 40 | ), 41 | })} 42 |

43 |
44 | `; 45 | $shadow 46 | .querySelector(".about")! 47 | .append(generateNodeFromHtml(maintenanceInfoHtml)); 48 | } 49 | 50 | destroy() { 51 | // Do nothing. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/translations/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "A new update {{{version}}} has been downloaded.": "S'ha descarregat una nova actualització {{{version}}}.", 3 | "A new version {{{version}}} of Zulip Desktop is available.": "Hi ha disponible una nova versió de Zulip Escriptori {{{version}}}.", 4 | "About": "Sobre", 5 | "About Zulip": "Quant a Zulip", 6 | "Actual Size": "Mida actual", 7 | "Add Organization": "Afegir organització", 8 | "Add a Zulip organization": "Afegir una organització de Zulip", 9 | "Advanced": "Avançat", 10 | "Are you sure?": "Esteu segur/a?", 11 | "CSS file": "Arxiu CSS", 12 | "Cancel": "Cancel·la", 13 | "Certificate error": "Error de certificat", 14 | "Close": "Tancar", 15 | "Copy": "Copia", 16 | "Create a new organization": "Crea una nova organització", 17 | "Delete": "Elimina", 18 | "Desktop Settings": "Configuració d'escriptori", 19 | "Do Not Disturb": "No molesteu", 20 | "Edit": "Edita", 21 | "Emoji & Symbols": "Emojis i símbols", 22 | "Enter Full Screen": "Entreu a pantalla sencera", 23 | "Error saving new organization": "Error en guardar la nova organització", 24 | "File": "Fitxer", 25 | "General": "General", 26 | "Hard Reload": "Recàrrega forçada", 27 | "Help Center": "Centre d'ajuda", 28 | "History": "Historial", 29 | "History Shortcuts": "Dreceres d'historial", 30 | "Log Out": "Tanca la sessió", 31 | "Log Out of Organization": "Tanca la sessió de l'organització", 32 | "Mute all sounds from Zulip": "Silencia tots els sons de Zulip", 33 | "OK": "D'acord", 34 | "Organization URL": "URL d'organització", 35 | "Reload": "Recarrega", 36 | "Reset App Settings": "Reinicia la configuració de l'aplicació", 37 | "Save": "Guardar", 38 | "Settings": "Configuració", 39 | "Unable to check for updates.": "No ha estat possible comprovar les actualitzacions.", 40 | "Unable to download the update.": "No ha estat possible descarregar l'actualització.", 41 | "Unknown error": "Error desconegut", 42 | "Upload": "Pujada" 43 | } 44 | -------------------------------------------------------------------------------- /app/main/linuxupdater.ts: -------------------------------------------------------------------------------- 1 | import {Notification, type Session, app} from "electron/main"; 2 | 3 | import * as semver from "semver"; 4 | import {z} from "zod"; 5 | 6 | import * as ConfigUtil from "../common/config-util.ts"; 7 | import Logger from "../common/logger-util.ts"; 8 | import * as t from "../common/translation-util.ts"; 9 | 10 | import * as LinuxUpdateUtil from "./linux-update-util.ts"; 11 | 12 | const logger = new Logger({ 13 | file: "linux-update-util.log", 14 | }); 15 | 16 | export async function linuxUpdateNotification(session: Session): Promise { 17 | let url = "https://api.github.com/repos/zulip/zulip-desktop/releases"; 18 | url = ConfigUtil.getConfigItem("betaUpdate", false) ? url : url + "/latest"; 19 | 20 | try { 21 | const response = await session.fetch(url); 22 | if (!response.ok) { 23 | logger.log("Linux update response status: ", response.status); 24 | return; 25 | } 26 | 27 | const data: unknown = await response.json(); 28 | /* eslint-disable @typescript-eslint/naming-convention */ 29 | const latestVersion = ConfigUtil.getConfigItem("betaUpdate", false) 30 | ? z.array(z.object({tag_name: z.string()})).parse(data)[0].tag_name 31 | : z.object({tag_name: z.string()}).parse(data).tag_name; 32 | /* eslint-enable @typescript-eslint/naming-convention */ 33 | 34 | if (semver.gt(latestVersion, app.getVersion())) { 35 | const notified = LinuxUpdateUtil.getUpdateItem(latestVersion); 36 | if (notified === null) { 37 | new Notification({ 38 | title: t.__("Zulip Update"), 39 | body: t.__( 40 | "A new version {{{version}}} is available. Please update using your package manager.", 41 | {version: latestVersion}, 42 | ), 43 | }).show(); 44 | LinuxUpdateUtil.setUpdateItem(latestVersion, true); 45 | } 46 | } 47 | } catch (error: unknown) { 48 | logger.error("Linux update error."); 49 | logger.error(error); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/main/linux-update-util.ts: -------------------------------------------------------------------------------- 1 | import {app, dialog} from "electron/main"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import {JsonDB} from "node-json-db"; 6 | import {DataError} from "node-json-db/dist/lib/Errors.js"; 7 | 8 | import Logger from "../common/logger-util.ts"; 9 | import * as t from "../common/translation-util.ts"; 10 | 11 | const logger = new Logger({ 12 | file: "linux-update-util.log", 13 | }); 14 | 15 | let database: JsonDB; 16 | 17 | reloadDatabase(); 18 | 19 | export function getUpdateItem( 20 | key: string, 21 | defaultValue: true | null = null, 22 | ): true | null { 23 | reloadDatabase(); 24 | let value: unknown; 25 | try { 26 | value = database.getObject(`/${key}`); 27 | } catch (error: unknown) { 28 | if (!(error instanceof DataError)) throw error; 29 | } 30 | 31 | if (value !== true && value !== null) { 32 | setUpdateItem(key, defaultValue); 33 | return defaultValue; 34 | } 35 | 36 | return value; 37 | } 38 | 39 | export function setUpdateItem(key: string, value: true | null): void { 40 | database.push(`/${key}`, value, true); 41 | reloadDatabase(); 42 | } 43 | 44 | export function removeUpdateItem(key: string): void { 45 | database.delete(`/${key}`); 46 | reloadDatabase(); 47 | } 48 | 49 | function reloadDatabase(): void { 50 | const linuxUpdateJsonPath = path.join( 51 | app.getPath("userData"), 52 | "/config/updates.json", 53 | ); 54 | try { 55 | const file = fs.readFileSync(linuxUpdateJsonPath, "utf8"); 56 | JSON.parse(file); 57 | } catch (error: unknown) { 58 | if (fs.existsSync(linuxUpdateJsonPath)) { 59 | fs.unlinkSync(linuxUpdateJsonPath); 60 | dialog.showErrorBox( 61 | t.__("Error saving update notifications"), 62 | t.__("We encountered an error while saving the update notifications."), 63 | ); 64 | logger.error("Error while JSON parsing updates.json: "); 65 | logger.error(error); 66 | } 67 | } 68 | 69 | database = new JsonDB(linuxUpdateJsonPath, true, true); 70 | } 71 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import os from "node:os"; 3 | import path from "node:path"; 4 | import process from "node:process"; 5 | 6 | import {type ElectronApplication, _electron} from "playwright-core"; 7 | import z from "zod"; 8 | 9 | const testsPackage = z 10 | .object({productName: z.string()}) 11 | .parse( 12 | JSON.parse( 13 | fs.readFileSync( 14 | new URL("zulip-test/package.json", import.meta.url), 15 | "utf8", 16 | ), 17 | ), 18 | ); 19 | 20 | // Runs Zulip Desktop. 21 | // Returns a promise that resolves to an Electron Application once the app has loaded. 22 | export async function createApp(): Promise { 23 | return _electron.launch({ 24 | args: [path.join(import.meta.dirname, "zulip-test")], // Ensure this dir has a package.json file with a 'main' entry point 25 | }); 26 | } 27 | 28 | // Quit the app, end the test 29 | export async function endTest(app: ElectronApplication): Promise { 30 | await app.close(); 31 | } 32 | 33 | function getAppDataDirectory(): string { 34 | let base; 35 | 36 | switch (process.platform) { 37 | case "darwin": { 38 | base = path.join(os.homedir(), "Library", "Application Support"); 39 | break; 40 | } 41 | 42 | case "linux": { 43 | base = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"); 44 | break; 45 | } 46 | 47 | case "win32": { 48 | base = process.env.APPDATA; 49 | if (base === undefined) 50 | throw new Error("Missing APPDATA environment variable."); 51 | break; 52 | } 53 | 54 | default: { 55 | throw new Error("Could not detect app data dir base."); 56 | } 57 | } 58 | 59 | console.log("Detected App Data Dir base:", base); 60 | return path.join(base, testsPackage.productName); 61 | } 62 | 63 | // Resets the test directory, containing domain.json, window-state.json, etc 64 | export function resetTestDataDirectory(): void { 65 | const appDataDirectory = getAppDataDirectory(); 66 | fs.rmSync(appDataDirectory, {force: true, recursive: true}); 67 | } 68 | -------------------------------------------------------------------------------- /app/common/default-util.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import {app} from "zulip:remote"; 4 | 5 | let setupCompleted = false; 6 | 7 | const zulipDirectory = app.getPath("userData"); 8 | const logDirectory = `${zulipDirectory}/Logs/`; 9 | const configDirectory = `${zulipDirectory}/config/`; 10 | export const initSetUp = (): void => { 11 | // If it is the first time the app is running 12 | // create zulip dir in userData folder to 13 | // avoid errors 14 | if (!setupCompleted) { 15 | if (!fs.existsSync(zulipDirectory)) { 16 | fs.mkdirSync(zulipDirectory); 17 | } 18 | 19 | if (!fs.existsSync(logDirectory)) { 20 | fs.mkdirSync(logDirectory); 21 | } 22 | 23 | // Migrate config files from app data folder to config folder inside app 24 | // data folder. This will be done once when a user updates to the new version. 25 | if (!fs.existsSync(configDirectory)) { 26 | fs.mkdirSync(configDirectory); 27 | const domainJson = `${zulipDirectory}/domain.json`; 28 | const settingsJson = `${zulipDirectory}/settings.json`; 29 | const updatesJson = `${zulipDirectory}/updates.json`; 30 | const windowStateJson = `${zulipDirectory}/window-state.json`; 31 | const configData = [ 32 | { 33 | path: domainJson, 34 | fileName: "domain.json", 35 | }, 36 | { 37 | path: settingsJson, 38 | fileName: "settings.json", 39 | }, 40 | { 41 | path: updatesJson, 42 | fileName: "updates.json", 43 | }, 44 | ]; 45 | for (const data of configData) { 46 | if (fs.existsSync(data.path)) { 47 | fs.copyFileSync(data.path, configDirectory + data.fileName); 48 | fs.unlinkSync(data.path); 49 | } 50 | } 51 | 52 | // `window-state.json` is only deleted not moved, as the electron-window-state 53 | // package will recreate the file in the config folder. 54 | if (fs.existsSync(windowStateJson)) { 55 | fs.unlinkSync(windowStateJson); 56 | } 57 | } 58 | 59 | setupCompleted = true; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packaging/deb-apt.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFmdzvQBCADJ4BFlK+4ymIWa3jrNL0WfGPV3dVkZ1Ghy5MsgRIs81CpVS83m 4 | kyBLULY551GNwuHZaeXbkaA+cTDyhEPBFr0MTF0gO514escnjwcL7U1UCLA4I0WP 5 | 0yETXLHp7HFh4g+MZpObkgmLP55aV3jqgNK/p05umrhECBl1HJo+8T+0VNi2x1Pm 6 | LoJVvA7uJHcsNaQVWQF4RP0MaI4TLyjHZAJlpthQfbmq0AbZMEjDu8Th5G9KTsqE 7 | WRyFoAj/SWwKQK2U4xpnA6jEraMcvsYYQMrCXlG+MOV7zVknLrH5tfk7JlmWB4DV 8 | cs+QP5Z/UrVu+YpTpaoJoZV6LlEU1kNGjtq9ABEBAAG0TVp1bGlwIEFQVCBSZXBv 9 | c2l0b3J5IFNpZ25pbmcgS2V5IEJpbnRyYXkgKFByb2R1Y3Rpb24pIDxzdXBwb3J0 10 | QHp1bGlwY2hhdC5jb20+iQGSBBMBCACGBYJonATwBAsJCAcJECQkvlrpvRDZRxQA 11 | AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ5to4d/e2Ts692fK 12 | y5pjZ8XOKTBvXckVdjBe8cSiLIkvAxUICgQWAgMBAheAAhsDAh4BFiEEaa0ScE5x 13 | pIA9yjpoJCS+Wum9ENkAAP7XCACjGUAzUgOAbf1BJTbbR1Np4BNy31++93TNj+/3 14 | gYPbNwSJBb99yZfI6J4KwT1WepIXRx2Ikx0ChxEU5oOjEcPoM8Xslg3/vTV76dcJ 15 | CYtQdvIvLUBKN7MkDp6+H1LVu9AnzMYoAF8HiKk6NZNI2LjMMv1znYwod2Pp3EL7 16 | q/TPwiaOuNVDlaRSCsmbWYNPWLXAna7PU/yZ7FYwaCAKeC079+5rY59RvA/3oOmG 17 | nUAcADyuMaNkPPnkYW5adNfCHWEPUrIUJxyJ+yVf/E/mHoUKkqYhOs60WFPXpgpX 18 | cnYYw8E/1kXM+kAfWIOi7dGlCFiWLyQF0wjwn/sehBXZy8yquQENBFmdzvQBCACv 19 | 7VNQ6x3hfaRl8YF8bbrWXN2ZWxEa353p4QryHODsa7wHtsoNR3P30TILyafjjcV8 20 | P6dzyDw6TpfRqqQDKLY6FtznT2HdceQSffGTXB4CRV7KURBqh81PX/Jodz0NwkNr 21 | d0NWqkk6BnLX6U5tGuYiqC3vLpjOHmVQezJ41xpf85ElJ2nBW0rEcmfkfwQthJU7 22 | BbqWKd6nbt2G+xWkCVoN6q+CWLXtK0laHMKBGQnoiQpldotsKM8UnDeQXPqrEi28 23 | ksjVW8tBStCkLwV2hCxk49zdTvRjrhBTQ1Ff/kenuEwqbSERiKfA7I8omlqulSiJ 24 | 6rYdDnGjNcoRgnHb50hTABEBAAGJAX4EGAEIAHIFgmicBPAJECQkvlrpvRDZRxQA 25 | AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ2yYQ1NoS1Il7WjP 26 | HCfqbeXJc9dm9yLgL46FmSMjScRXAhsMFiEEaa0ScE5xpIA9yjpoJCS+Wum9ENkA 27 | AMOECACo0hRteH+CWZDLKaufkxQvfqd0/zq+uGJ2VYOrIUkuuaA0YBe+uGaoFwgT 28 | hxVs0UiOpMOzSyl+zC+7ShQu9t/jIm5sTmvHsgzmO11w4b1Td7Ow8dgAnAXKcbmA 29 | O1yaMi1C40YUI1zHRt0xkrnTJB57q+8Hclum59UXiSIgU5bKVeJhsX4LVpxi67Qg 30 | vIHgg6pL+kDzObjRuBw+8Qx/Cugf4W35IGLD6BGzLjZM98YhbaX52sFvuHj+8gAs 31 | xFOefLGRjZNdcp3IViTcVeR41Y9mA1Pjtlvthqrq70yra+EWjR7hUFxE9/BWjb18 32 | fQZRjlB5JKC69SdOMa5C2UTSWNbA 33 | =5JdK 34 | -----END PGP PUBLIC KEY BLOCK----- 35 | -------------------------------------------------------------------------------- /app/renderer/js/utils/reconnect-util.ts: -------------------------------------------------------------------------------- 1 | import * as backoff from "backoff"; 2 | 3 | import {html} from "../../../common/html.ts"; 4 | import Logger from "../../../common/logger-util.ts"; 5 | import * as t from "../../../common/translation-util.ts"; 6 | import type WebView from "../components/webview.ts"; 7 | import {ipcRenderer} from "../typed-ipc-renderer.ts"; 8 | 9 | const logger = new Logger({ 10 | file: "domain-util.log", 11 | }); 12 | 13 | export default class ReconnectUtil { 14 | url: string; 15 | alreadyReloaded: boolean; 16 | fibonacciBackoff: backoff.Backoff; 17 | 18 | constructor(webview: WebView) { 19 | this.url = webview.properties.url; 20 | this.alreadyReloaded = false; 21 | this.fibonacciBackoff = backoff.fibonacci({ 22 | initialDelay: 5000, 23 | maxDelay: 300_000, 24 | }); 25 | } 26 | 27 | async isOnline(): Promise { 28 | return ipcRenderer.invoke("is-online", this.url); 29 | } 30 | 31 | pollInternetAndReload(): void { 32 | this.fibonacciBackoff.backoff(); 33 | this.fibonacciBackoff.on("ready", async () => { 34 | if (await this._checkAndReload()) { 35 | this.fibonacciBackoff.reset(); 36 | } else { 37 | this.fibonacciBackoff.backoff(); 38 | } 39 | }); 40 | } 41 | 42 | async _checkAndReload(): Promise { 43 | if (this.alreadyReloaded) { 44 | return true; 45 | } 46 | 47 | if (await this.isOnline()) { 48 | ipcRenderer.send("forward-message", "reload-viewer"); 49 | logger.log("You're back online."); 50 | return true; 51 | } 52 | 53 | logger.log( 54 | "There is no internet connection, try checking network cables, modem and router.", 55 | ); 56 | const errorMessageHolder = document.querySelector("#description"); 57 | if (errorMessageHolder) { 58 | errorMessageHolder.innerHTML = html` 59 |
60 | ${t.__("Your internet connection doesn't seem to work properly!")} 61 |
62 |
${t.__("Verify that it works and then click Reconnect.")}
63 | `.html; 64 | } 65 | 66 | return false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fixes: 4 | 5 | 9 | 10 | **Screenshots and screen captures:** 11 | 12 | **Platforms this PR was tested on:** 13 | 14 | - [ ] Windows 15 | - [ ] macOS 16 | - [ ] Linux (specify distro) 17 | 18 |
19 | Self-review checklist 20 | 21 | 23 | 24 | 26 | 27 | - [ ] [Self-reviewed](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#how-to-review-code) the changes for clarity and maintainability 28 | (variable names, code reuse, readability, etc.). 29 | 30 | Communicate decisions, questions, and potential concerns. 31 | 32 | - [ ] Explains differences from previous plans (e.g., issue description). 33 | - [ ] Highlights technical choices and bugs encountered. 34 | - [ ] Calls out remaining decisions and concerns. 35 | - [ ] Automated tests verify logic where appropriate. 36 | 37 | Individual commits are ready for review (see [commit discipline](https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html)). 38 | 39 | - [ ] Each commit is a coherent idea. 40 | - [ ] Commit message(s) explain reasoning and motivation for changes. 41 | 42 | Completed manual review and testing of the following: 43 | 44 | - [ ] Visual appearance of the changes. 45 | - [ ] Responsiveness and internationalization. 46 | - [ ] Strings and tooltips. 47 | - [ ] End-to-end functionality of buttons, interactions and flows. 48 | - [ ] Corner cases, error conditions, and easily imagined bugs. 49 |
50 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/find-accounts.ts: -------------------------------------------------------------------------------- 1 | import {html} from "../../../../common/html.ts"; 2 | import * as LinkUtil from "../../../../common/link-util.ts"; 3 | import * as t from "../../../../common/translation-util.ts"; 4 | import {generateNodeFromHtml} from "../../components/base.ts"; 5 | 6 | type FindAccountsProperties = { 7 | $root: Element; 8 | }; 9 | 10 | async function findAccounts(url: string): Promise { 11 | if (!url) { 12 | return; 13 | } 14 | 15 | if (!url.startsWith("http")) { 16 | url = "https://" + url; 17 | } 18 | 19 | await LinkUtil.openBrowser(new URL("/accounts/find", url)); 20 | } 21 | 22 | export function initFindAccounts(properties: FindAccountsProperties): void { 23 | const $findAccounts = generateNodeFromHtml(html` 24 |
25 |
26 |
${t.__("Organization URL")}
27 | 28 |
29 |
30 | 33 |
34 |
35 | `); 36 | properties.$root.append($findAccounts); 37 | const $findAccountsButton = $findAccounts.querySelector( 38 | "#find-accounts-button", 39 | )!; 40 | const $serverUrlField: HTMLInputElement = $findAccounts.querySelector( 41 | "input.setting-input-value", 42 | )!; 43 | 44 | $findAccountsButton.addEventListener("click", async () => { 45 | await findAccounts($serverUrlField.value); 46 | }); 47 | 48 | $serverUrlField.addEventListener("click", () => { 49 | if ($serverUrlField.value === "zulipchat.com") { 50 | $serverUrlField.setSelectionRange(0, 0); 51 | } 52 | }); 53 | 54 | $serverUrlField.addEventListener("keypress", async (event) => { 55 | if (event.key === "Enter") { 56 | await findAccounts($serverUrlField.value); 57 | } 58 | }); 59 | 60 | $serverUrlField.addEventListener("input", () => { 61 | $serverUrlField.classList.toggle( 62 | "invalid-input-value", 63 | $serverUrlField.value === "", 64 | ); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /app/renderer/js/components/functional-tab.ts: -------------------------------------------------------------------------------- 1 | import {type Html, html} from "../../../common/html.ts"; 2 | import type {TabPage} from "../../../common/types.ts"; 3 | 4 | import {generateNodeFromHtml} from "./base.ts"; 5 | import Tab, {type TabProperties} from "./tab.ts"; 6 | 7 | export type FunctionalTabProperties = { 8 | $view: Element; 9 | page: TabPage; 10 | } & TabProperties; 11 | 12 | export default class FunctionalTab extends Tab { 13 | $view: Element; 14 | $el: Element; 15 | $closeButton?: Element; 16 | 17 | constructor({$view, ...properties}: FunctionalTabProperties) { 18 | super(properties); 19 | 20 | this.$view = $view; 21 | this.$el = generateNodeFromHtml(this.templateHtml()); 22 | if (properties.page !== "Settings") { 23 | this.properties.$root.append(this.$el); 24 | this.$closeButton = this.$el.querySelector(".server-tab-badge")!; 25 | this.registerListeners(); 26 | } 27 | } 28 | 29 | override async activate(): Promise { 30 | await super.activate(); 31 | this.$view.classList.add("active"); 32 | } 33 | 34 | override async deactivate(): Promise { 35 | await super.deactivate(); 36 | this.$view.classList.remove("active"); 37 | } 38 | 39 | override async destroy(): Promise { 40 | await super.destroy(); 41 | this.$view.remove(); 42 | } 43 | 44 | templateHtml(): Html { 45 | return html` 46 |
47 |
48 | close 49 |
50 |
51 | ${this.properties.materialIcon} 52 |
53 |
54 | `; 55 | } 56 | 57 | override registerListeners(): void { 58 | super.registerListeners(); 59 | 60 | this.$el.addEventListener("mouseover", () => { 61 | this.$closeButton?.classList.add("active"); 62 | }); 63 | 64 | this.$el.addEventListener("mouseout", () => { 65 | this.$closeButton?.classList.remove("active"); 66 | }); 67 | 68 | this.$closeButton?.addEventListener("click", (event) => { 69 | this.properties.onDestroy?.(); 70 | event.stopPropagation(); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/renderer/js/clipboard-decrypter.ts: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from "./typed-ipc-renderer.ts"; 2 | 3 | // This helper is exposed via electron_bridge for use in the social 4 | // login flow. 5 | // 6 | // It consists of a key and a promised token. The in-app page sends 7 | // the key to the server, and opens the user’s browser to a page where 8 | // they can log in and get a token encrypted to that key. When the 9 | // user copies the encrypted token from their browser to the 10 | // clipboard, we decrypt it and resolve the promise. The in-app page 11 | // then uses the decrypted token to log the user in within the app. 12 | // 13 | // The encryption is authenticated (AES-GCM) to guarantee that we 14 | // don’t leak anything from the user’s clipboard other than the token 15 | // intended for us. 16 | 17 | export type ClipboardDecrypter = { 18 | version: number; 19 | key: Uint8Array; 20 | pasted: Promise; 21 | }; 22 | 23 | export class ClipboardDecrypterImplementation implements ClipboardDecrypter { 24 | version: number; 25 | key: Uint8Array; 26 | pasted: Promise; 27 | 28 | constructor(_: number) { 29 | // At this time, the only version is 1. 30 | this.version = 1; 31 | const {key, sig} = ipcRenderer.sendSync("new-clipboard-key"); 32 | this.key = key; 33 | this.pasted = new Promise((resolve) => { 34 | let interval: NodeJS.Timeout | null = null; 35 | const startPolling = () => { 36 | interval ??= setInterval(poll, 1000); 37 | void poll(); 38 | }; 39 | 40 | const stopPolling = () => { 41 | if (interval !== null) { 42 | clearInterval(interval); 43 | interval = null; 44 | } 45 | }; 46 | 47 | const poll = async () => { 48 | const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig); 49 | if (plaintext === undefined) return; 50 | 51 | window.removeEventListener("focus", startPolling); 52 | window.removeEventListener("blur", stopPolling); 53 | stopPolling(); 54 | resolve(plaintext); 55 | }; 56 | 57 | window.addEventListener("focus", startPolling); 58 | window.addEventListener("blur", stopPolling); 59 | if (document.hasFocus()) { 60 | startPolling(); 61 | } 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/connected-org-section.ts: -------------------------------------------------------------------------------- 1 | import {html} from "../../../../common/html.ts"; 2 | import * as t from "../../../../common/translation-util.ts"; 3 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 4 | import * as DomainUtil from "../../utils/domain-util.ts"; 5 | 6 | import {reloadApp} from "./base-section.ts"; 7 | import {initFindAccounts} from "./find-accounts.ts"; 8 | import {initServerInfoForm} from "./server-info-form.ts"; 9 | 10 | type ConnectedOrgSectionProperties = { 11 | $root: Element; 12 | }; 13 | 14 | export function initConnectedOrgSection({ 15 | $root, 16 | }: ConnectedOrgSectionProperties): void { 17 | $root.textContent = ""; 18 | 19 | const servers = DomainUtil.getDomains(); 20 | $root.innerHTML = html` 21 |
22 |
${t.__("Connected organizations")}
23 |
24 | ${t.__("All the connected organizations will appear here.")} 25 |
26 |
27 |
28 | 31 |
32 |
${t.__("Find accounts by email")}
33 |
34 |
35 | `.html; 36 | 37 | const $serverInfoContainer = $root.querySelector("#server-info-container")!; 38 | const $existingServers = $root.querySelector("#existing-servers")!; 39 | const $newOrgButton: HTMLButtonElement = 40 | $root.querySelector("#new-org-button")!; 41 | const $findAccountsContainer = $root.querySelector( 42 | "#find-accounts-container", 43 | )!; 44 | 45 | const noServerText = t.__( 46 | "All the connected organizations will appear here.", 47 | ); 48 | // Show noServerText if no servers are there otherwise hide it 49 | $existingServers.textContent = servers.length === 0 ? noServerText : ""; 50 | 51 | for (const [i, server] of servers.entries()) { 52 | initServerInfoForm({ 53 | $root: $serverInfoContainer, 54 | server, 55 | index: i, 56 | onChange: reloadApp, 57 | }); 58 | } 59 | 60 | $newOrgButton.addEventListener("click", () => { 61 | ipcRenderer.send("forward-message", "open-org-tab"); 62 | }); 63 | 64 | initFindAccounts({ 65 | $root: $findAccountsContainer, 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /app/renderer/js/typed-ipc-renderer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type IpcRendererEvent, 3 | ipcRenderer as untypedIpcRenderer, // eslint-disable-line no-restricted-imports 4 | } from "electron/renderer"; 5 | 6 | import type { 7 | MainCall, 8 | MainMessage, 9 | RendererMessage, 10 | } from "../../common/typed-ipc.js"; 11 | 12 | type RendererListener = 13 | RendererMessage[Channel] extends (...arguments_: infer Arguments) => void 14 | ? (event: IpcRendererEvent, ...arguments_: Arguments) => void 15 | : never; 16 | 17 | export const ipcRenderer: { 18 | on( 19 | channel: Channel, 20 | listener: RendererListener, 21 | ): void; 22 | once( 23 | channel: Channel, 24 | listener: RendererListener, 25 | ): void; 26 | off( 27 | channel: Channel, 28 | listener: RendererListener, 29 | ): void; 30 | removeListener( 31 | channel: Channel, 32 | listener: RendererListener, 33 | ): void; 34 | removeAllListeners(channel: keyof RendererMessage): void; 35 | send( 36 | channel: "forward-message", 37 | rendererChannel: Channel, 38 | ...arguments_: Parameters 39 | ): void; 40 | send( 41 | channel: "forward-to", 42 | webContentsId: number, 43 | rendererChannel: Channel, 44 | ...arguments_: Parameters 45 | ): void; 46 | send( 47 | channel: Channel, 48 | ...arguments_: Parameters 49 | ): void; 50 | invoke( 51 | channel: Channel, 52 | ...arguments_: Parameters 53 | ): Promise>; 54 | sendSync( 55 | channel: Channel, 56 | ...arguments_: Parameters 57 | ): ReturnType; 58 | postMessage( 59 | channel: Channel, 60 | message: Parameters extends [infer Message] 61 | ? Message 62 | : never, 63 | transfer?: MessagePort[], 64 | ): void; 65 | sendToHost( 66 | channel: Channel, 67 | ...arguments_: Parameters 68 | ): void; 69 | } = untypedIpcRenderer; 70 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/base-section.ts: -------------------------------------------------------------------------------- 1 | import {type Html, html} from "../../../../common/html.ts"; 2 | import * as t from "../../../../common/translation-util.ts"; 3 | import {generateNodeFromHtml} from "../../components/base.ts"; 4 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 5 | 6 | type BaseSectionProperties = { 7 | $element: HTMLElement; 8 | disabled?: boolean; 9 | value: boolean; 10 | clickHandler: () => void; 11 | }; 12 | 13 | export function generateSettingOption(properties: BaseSectionProperties): void { 14 | const {$element, disabled, value, clickHandler} = properties; 15 | 16 | $element.textContent = ""; 17 | 18 | const $optionControl = generateNodeFromHtml( 19 | generateOptionHtml(value, disabled), 20 | ); 21 | $element.append($optionControl); 22 | 23 | if (!disabled) { 24 | $optionControl.addEventListener("click", clickHandler); 25 | } 26 | } 27 | 28 | export function generateOptionHtml( 29 | settingOption: boolean, 30 | disabled?: boolean, 31 | ): Html { 32 | const labelHtml = disabled 33 | ? html`` 37 | : html``; 38 | if (settingOption) { 39 | return html` 40 |
41 |
42 | 43 | ${labelHtml} 44 |
45 |
46 | `; 47 | } 48 | 49 | return html` 50 |
51 |
52 | 53 | ${labelHtml} 54 |
55 |
56 | `; 57 | } 58 | 59 | /* A method that in future can be used to create dropdown menus using 76 | ${optionsHtml} 77 | 78 | `; 79 | } 80 | 81 | export function reloadApp(): void { 82 | ipcRenderer.send("forward-message", "reload-viewer"); 83 | } 84 | -------------------------------------------------------------------------------- /public/translations/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "যুলিপ সম্পর্কে", 3 | "Actual Size": "প্রকৃত সাইজ", 4 | "Add Organization": "সংস্থা যুক্ত করুন", 5 | "Add a Zulip organization": "একটি যুলিপ প্রতিষ্ঠান যুক্ত করুন", 6 | "Add custom CSS": "কাস্টম সিএসএস যুক্ত করুন", 7 | "Advanced": "অগ্রসর", 8 | "Always start minimized": "সব সময় মিনিমাইজড ভাবে শুরু করুন", 9 | "App Updates": "অ্যাপ আপডেট", 10 | "Appearance": "প্রকাশ", 11 | "Application Shortcuts": "অ্যাপ্লিকেশান শর্টকাট", 12 | "Are you sure you want to disconnect this organization?": "আপনি কি নিশ্চিত যে আপনি এই সংস্থার সংযোগ বিচ্ছিন্ন করতে চান ?", 13 | "Auto hide Menu bar": "অটো মেনুবার হাইড করুন", 14 | "Auto hide menu bar (Press Alt key to display)": "অটো মেনুবার হাইড করুন (দেখার জন্য অল্টার কি চাপুন)", 15 | "Back": "পেছন", 16 | "Bounce dock on new private message": "ব্যাক্তিগত মেসেজে ডক বাউন্স করুন", 17 | "Cancel": "বাতিল", 18 | "Change": "পরিবর্তন", 19 | "Change the language from System Preferences → Keyboard → Text → Spelling.": "ভাষা পরিবর্তন করতে সিস্টেম প্রেফারেন্স → কীবোর্ড → টেক্সট → স্পেলিং এ যান।", 20 | "Check for Updates": "আপডেট চেক করুন", 21 | "Close": "বন্ধ করুন", 22 | "Connect": "সংযুক্ত করুন", 23 | "Connect to another organization": "অন্য একটি সংস্থার সাথে সংযুক্ত করুন", 24 | "Connected organizations": "সংযুক্ত সংস্থা সমূহ", 25 | "Copy": "কপি", 26 | "Copy Zulip URL": "যুলিপ ইউআরএল কপি করুন", 27 | "Create a new organization": "নতুন সংস্থা তৈরি করুন", 28 | "Cut": "কাট", 29 | "Delete": "ডিলিট", 30 | "Desktop Notifications": "ডেস্কটপ নোটিফিকেশান", 31 | "Desktop Settings": "ডেস্কটপ সেটিংস", 32 | "Disconnect": "সংযোগ বিছিন্ন করুন", 33 | "Download App Logs": "অ্যাপ লগ ডাউনলোড করুন", 34 | "Edit": "এডিট", 35 | "Edit Shortcuts": "শর্টকাটগুলো এডিট করুন", 36 | "Enable auto updates": "অটো আপডেট চালু করুন", 37 | "Factory Reset": "ফ্যাক্টরি রিসেট", 38 | "File": "ফাইল", 39 | "Find accounts": "অ্যাকাউন্ট খুজুন", 40 | "Find accounts by email": "ইমেইল ব্যাবহার করে অ্যাকাউন্ট খুজুন", 41 | "Forward": "ফরওয়ার্ড", 42 | "General": "সাধারন", 43 | "Help": "সাহায্য", 44 | "Help Center": "সাহায্য কেন্দ্র", 45 | "History": "ইতিহাস", 46 | "Log Out": "লগ আউট", 47 | "Log Out of Organization": "সংস্থা থেকে লগ আউট করুন", 48 | "Minimize": "ছোট করুন", 49 | "No Suggestion Found": "কোন সাজেশন পাওয়া যায়নি", 50 | "OR": "অথবা", 51 | "On macOS, the OS spellchecker is used.": "ম্যাক ওএস এ , ওএস এর স্পেলচেকার ব্যাবহার করা হয় ।", 52 | "Organizations": "সংস্থাসমূহ", 53 | "Save": "সেভ", 54 | "Settings": "সেটিংস", 55 | "Shortcuts": "শর্টকাট সমূহ", 56 | "Tip": "টিপ", 57 | "Undo": "অ্যান্ডু", 58 | "Upload": "আপলোড", 59 | "Zoom Out": "জুম আউট" 60 | } 61 | -------------------------------------------------------------------------------- /public/translations/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "A new update {{{version}}} has been downloaded.": "تم تنزيل تحديث جديد {{{version}}}.", 3 | "A new version {{{version}}} is available. Please update using your package manager.": "يتوفر إصدار جديد {{{version}}}. يرجى التحديث باستخدام مدير الحزم الخاص بك.", 4 | "Actual Size": "الحجم الفعلي", 5 | "Add Organization": "إضافة منظمة", 6 | "Add custom CSS": "إضافة CSS معدلة", 7 | "Advanced": "متقدم", 8 | "All the connected organizations will appear here.": "جميع المنظمات المتصلة ستظهر هنا", 9 | "Always start minimized": "دائماً إبدأ بالقليل", 10 | "App Updates": "تحديثات التطبيق", 11 | "Appearance": "المظهر", 12 | "Application Shortcuts": "إختصارات التطبيق", 13 | "Are you sure you want to disconnect this organization?": "هل أنت متأكد من فصل هذة المنظمة؟", 14 | "Auto hide Menu bar": "أخف القائمة تلقائياً", 15 | "Auto hide menu bar (Press Alt key to display)": "أخف القائمة تلقائياً (إضغط Alt لعرض القائمة)", 16 | "Back": "رجوع", 17 | "Bounce dock on new private message": "أخرج المنصة في حال رسالة خاصة جديدة", 18 | "Cancel": "إلغاء", 19 | "Change": "تغيير", 20 | "Check for Updates": "التحقق من التحديثات", 21 | "Close": "إغلاق", 22 | "Connect": "اتصال", 23 | "Connect to another organization": "التوصيل مع منظمة أخرى", 24 | "Connected organizations": "المنظمات المتصلة", 25 | "Copy": "نسخ", 26 | "Copy Zulip URL": "نسخ رابط زوليب", 27 | "Create a new organization": "إنشاء منظمة جديدة", 28 | "Cut": "قص", 29 | "Default download location": "موقع التحميل الافتراضي", 30 | "Delete": "حذف", 31 | "Desktop Notifications": "إشعارات سطح المكتب", 32 | "Desktop Settings": "إعدادات سطح المكتب", 33 | "Disconnect": "قطع الاتصال", 34 | "Download App Logs": "تنزيل سجلات التطبيق", 35 | "Edit": "تعديل", 36 | "Edit Shortcuts": "تعديل الاختصارات", 37 | "Emoji & Symbols": "الإيموجي و الرموز", 38 | "Enable auto updates": "تفعيل التحديثات التلقائية", 39 | "Enable error reporting (requires restart)": "تفعيل تقارير الأخطاء (يتطلب إعادة التشغيل)", 40 | "Enter Full Screen": "اعرض الشاشة كاملة", 41 | "Factory Reset": "إعادة ضبط المصنع", 42 | "File": "ملف", 43 | "Hide Zulip": "أخفي زوليب", 44 | "Network and Proxy Settings": "الشبكة و إعدادات البروكسي", 45 | "OK": "حسنًا", 46 | "Reset App Settings": "أعد ضبط إعدادات التطبيق", 47 | "Reset the application, thus deleting all the connected organizations and accounts.": "إعادة ضبط التطبيق, و بالتالي مسح جميع المنظمات المتصلة و الحسابات", 48 | "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} يقوم بتشغيل نسخة قديمة من خادم زوليب {{{version}}}. قد لا يعمل بشكل كامل مع هذا التطبيق" 49 | } 50 | -------------------------------------------------------------------------------- /app/common/logger-util.ts: -------------------------------------------------------------------------------- 1 | import {Console} from "node:console"; // eslint-disable-line n/prefer-global/console 2 | import fs from "node:fs"; 3 | import os from "node:os"; 4 | import process from "node:process"; 5 | 6 | import {app} from "zulip:remote"; 7 | 8 | import {initSetUp} from "./default-util.ts"; 9 | 10 | type LoggerOptions = { 11 | file?: string; 12 | }; 13 | 14 | initSetUp(); 15 | 16 | const logDirectory = `${app.getPath("userData")}/Logs`; 17 | 18 | type Level = "log" | "debug" | "info" | "warn" | "error"; 19 | 20 | export default class Logger { 21 | nodeConsole: Console; 22 | 23 | constructor(options: LoggerOptions = {}) { 24 | let {file = "console.log"} = options; 25 | 26 | file = `${logDirectory}/${file}`; 27 | 28 | // Trim log according to type of process 29 | if (process.type === "renderer") { 30 | requestIdleCallback(async () => this.trimLog(file)); 31 | } else { 32 | process.nextTick(async () => this.trimLog(file)); 33 | } 34 | 35 | const fileStream = fs.createWriteStream(file, {flags: "a"}); 36 | const nodeConsole = new Console(fileStream); 37 | 38 | this.nodeConsole = nodeConsole; 39 | } 40 | 41 | _log(type: Level, ...arguments_: unknown[]): void { 42 | arguments_.unshift(this.getTimestamp() + " |\t"); 43 | arguments_.unshift(type.toUpperCase() + " |"); 44 | this.nodeConsole[type](...arguments_); 45 | console[type](...arguments_); 46 | } 47 | 48 | log(...arguments_: unknown[]): void { 49 | this._log("log", ...arguments_); 50 | } 51 | 52 | debug(...arguments_: unknown[]): void { 53 | this._log("debug", ...arguments_); 54 | } 55 | 56 | info(...arguments_: unknown[]): void { 57 | this._log("info", ...arguments_); 58 | } 59 | 60 | warn(...arguments_: unknown[]): void { 61 | this._log("warn", ...arguments_); 62 | } 63 | 64 | error(...arguments_: unknown[]): void { 65 | this._log("error", ...arguments_); 66 | } 67 | 68 | getTimestamp(): string { 69 | const date = new Date(); 70 | const timestamp = 71 | `${date.getMonth()}/${date.getDate()} ` + 72 | `${date.getMinutes()}:${date.getSeconds()}`; 73 | return timestamp; 74 | } 75 | 76 | async trimLog(file: string): Promise { 77 | const data = await fs.promises.readFile(file, "utf8"); 78 | 79 | const maxLogFileLines = 500; 80 | const logs = data.split(os.EOL); 81 | const logLength = logs.length - 1; 82 | 83 | // Keep bottom maxLogFileLines of each log instance 84 | if (logLength > maxLogFileLines) { 85 | const trimmedLogs = logs.slice(logLength - maxLogFileLines); 86 | const toWrite = trimmedLogs.join(os.EOL); 87 | await fs.promises.writeFile(file, toWrite); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to Zulip's desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request. 6 | 7 | ## Getting Started 8 | 9 | Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.us/essential-electron) great article. 10 | 11 | ## Community 12 | 13 | - The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip web app project, and testing, can be read [here](https://zulip.readthedocs.io). 14 | 15 | - If you have any questions regarding zulip-desktop, open an [issue](https://github.com/zulip/zulip-desktop/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop). 16 | 17 | ## Issue 18 | 19 | Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-desktop/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-desktop/issues/new). 20 | 21 | The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot). 22 | 23 | Please pay attention to the following points while opening an issue. 24 | 25 | ### Does it happen on web browsers? (especially Chrome) 26 | 27 | Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application. 28 | If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip). 29 | 30 | ### Write detailed information 31 | 32 | Detailed information is very helpful to understand an issue. 33 | 34 | For example: 35 | 36 | - How to reproduce the issue, step-by-step. 37 | - The expected behavior (or what is wrong). 38 | - Screenshots for GUI issues. 39 | - The application version. 40 | - The operating system. 41 | - The Zulip-Desktop version. 42 | 43 | ## Pull Requests 44 | 45 | Pull Requests are always welcome. 46 | 47 | 1. When you edit the code, please run `node --run test` to check the formatting of your code before you `git commit`. 48 | 2. Ensure the PR description clearly describes the problem and solution. It should include: 49 | - The operating system on which you tested. 50 | - The Zulip-Desktop version on which you tested. 51 | - The relevant issue number, if applicable. 52 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/nav.ts: -------------------------------------------------------------------------------- 1 | import {type Html, html} from "../../../../common/html.ts"; 2 | import * as t from "../../../../common/translation-util.ts"; 3 | import type {NavigationItem} from "../../../../common/types.ts"; 4 | import {generateNodeFromHtml} from "../../components/base.ts"; 5 | 6 | type PreferenceNavigationProperties = { 7 | $root: Element; 8 | onItemSelected: (navigationItem: NavigationItem) => void; 9 | }; 10 | 11 | export default class PreferenceNavigation { 12 | navigationItems: Array<{navigationItem: NavigationItem; label: string}>; 13 | $el: Element; 14 | constructor(private readonly properties: PreferenceNavigationProperties) { 15 | this.navigationItems = [ 16 | {navigationItem: "General", label: t.__("General")}, 17 | {navigationItem: "Network", label: t.__("Network")}, 18 | {navigationItem: "AddServer", label: t.__("Add Organization")}, 19 | {navigationItem: "Organizations", label: t.__("Organizations")}, 20 | {navigationItem: "Shortcuts", label: t.__("Shortcuts")}, 21 | ]; 22 | 23 | this.$el = generateNodeFromHtml(this.templateHtml()); 24 | this.properties.$root.append(this.$el); 25 | this.registerListeners(); 26 | } 27 | 28 | templateHtml(): Html { 29 | const navigationItemsHtml = html``.join( 30 | this.navigationItems.map( 31 | ({navigationItem, label}) => 32 | html``, 33 | ), 34 | ); 35 | 36 | return html` 37 |
38 |
${t.__("Settings")}
39 | 40 |
41 | `; 42 | } 43 | 44 | registerListeners(): void { 45 | for (const {navigationItem} of this.navigationItems) { 46 | const $item = this.$el.querySelector( 47 | `#nav-${CSS.escape(navigationItem)}`, 48 | )!; 49 | $item.addEventListener("click", () => { 50 | this.properties.onItemSelected(navigationItem); 51 | }); 52 | } 53 | } 54 | 55 | select(navigationItemToSelect: NavigationItem): void { 56 | for (const {navigationItem} of this.navigationItems) { 57 | if (navigationItem === navigationItemToSelect) { 58 | this.activate(navigationItem); 59 | } else { 60 | this.deactivate(navigationItem); 61 | } 62 | } 63 | } 64 | 65 | activate(navigationItem: NavigationItem): void { 66 | const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!; 67 | $item.classList.add("active"); 68 | } 69 | 70 | deactivate(navigationItem: NavigationItem): void { 71 | const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!; 72 | $item.classList.remove("active"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/main/typed-ipc-main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type IpcMainEvent, 3 | type IpcMainInvokeEvent, 4 | type WebContents, 5 | ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports 6 | } from "electron/main"; 7 | 8 | import type { 9 | MainCall, 10 | MainMessage, 11 | RendererMessage, 12 | } from "../common/typed-ipc.js"; 13 | 14 | type MainListener = 15 | MainMessage[Channel] extends (...arguments_: infer Arguments) => infer Return 16 | ? ( 17 | event: IpcMainEvent & {returnValue: Return}, 18 | ...arguments_: Arguments 19 | ) => void 20 | : never; 21 | 22 | type MainHandler = MainCall[Channel] extends ( 23 | ...arguments_: infer Arguments 24 | ) => infer Return 25 | ? ( 26 | event: IpcMainInvokeEvent, 27 | ...arguments_: Arguments 28 | ) => Return | Promise 29 | : never; 30 | 31 | export const ipcMain: { 32 | on( 33 | channel: "forward-message", 34 | listener: ( 35 | event: IpcMainEvent, 36 | channel: Channel, 37 | ...arguments_: Parameters 38 | ) => void, 39 | ): void; 40 | on( 41 | channel: "forward-to", 42 | listener: ( 43 | event: IpcMainEvent, 44 | webContentsId: number, 45 | channel: Channel, 46 | ...arguments_: Parameters 47 | ) => void, 48 | ): void; 49 | on( 50 | channel: Channel, 51 | listener: MainListener, 52 | ): void; 53 | once( 54 | channel: Channel, 55 | listener: MainListener, 56 | ): void; 57 | removeListener( 58 | channel: Channel, 59 | listener: MainListener, 60 | ): void; 61 | removeAllListeners(channel?: keyof MainMessage): void; 62 | handle( 63 | channel: Channel, 64 | handler: MainHandler, 65 | ): void; 66 | handleOnce( 67 | channel: Channel, 68 | handler: MainHandler, 69 | ): void; 70 | removeHandler(channel: keyof MainCall): void; 71 | } = untypedIpcMain; 72 | 73 | export function send( 74 | contents: WebContents, 75 | channel: Channel, 76 | ...arguments_: Parameters 77 | ): void { 78 | contents.send(channel, ...arguments_); 79 | } 80 | 81 | export function sendToFrame( 82 | contents: WebContents, 83 | frameId: number | [number, number], 84 | channel: Channel, 85 | ...arguments_: Parameters 86 | ): void { 87 | contents.sendToFrame(frameId, channel, ...arguments_); 88 | } 89 | -------------------------------------------------------------------------------- /tools/push-to-pull-request: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | usage () { 5 | cat >&2 <&2 45 | exit 1 46 | fi 47 | 48 | # See https://developer.github.com/v3/pulls/#get-a-single-pull-request . 49 | # This is the old REST API; the new GraphQL API does look neat, but it 50 | # seems to require authentication even for simple lookups of public data, 51 | # and that'd be a pain for a simple script like this. 52 | pr_url=https://api.github.com/repos/"${repo_fq}"/pulls/"${pr_id}" 53 | pr_details="$(curl -s "$pr_url")" 54 | 55 | pr_jq () { 56 | echo "$pr_details" | jq "$@" 57 | } 58 | 59 | if [ "$(pr_jq -r .message)" = "Not Found" ]; then 60 | echo "Invalid PR URL: $pr_url" 61 | exit 1 62 | fi 63 | 64 | if [ "$(pr_jq .maintainer_can_modify)" != "true" ]; then 65 | # This happens when the PR has already been merged or closed, or 66 | # if the contributor has turned off the (default) setting to allow 67 | # maintainers of the target repo to push to their PR branch. 68 | # 69 | # The latter seems to be rare (in Greg's experience doing the 70 | # manual equivalent of this script for many different 71 | # contributors, none have ever chosen this setting), but give a 72 | # decent error message if it does happen. 73 | echo "error: PR already closed, or contributor has disallowed pushing to branch" >&2 74 | exit 1 75 | fi 76 | 77 | pr_head_repo_fq="$(pr_jq -r .head.repo.full_name)" 78 | pr_head_refname="$(pr_jq -r .head.ref)" 79 | 80 | set -x 81 | exec git push git@github.com:"$pr_head_repo_fq" +@:"$pr_head_refname" 82 | -------------------------------------------------------------------------------- /app/common/enterprise-util.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import process from "node:process"; 4 | 5 | import {z} from "zod"; 6 | import {dialog} from "zulip:remote"; 7 | 8 | import {enterpriseConfigSchemata} from "./config-schemata.ts"; 9 | import Logger from "./logger-util.ts"; 10 | 11 | type EnterpriseConfig = { 12 | [Key in keyof typeof enterpriseConfigSchemata]: z.output< 13 | (typeof enterpriseConfigSchemata)[Key] 14 | >; 15 | }; 16 | 17 | const logger = new Logger({ 18 | file: "enterprise-util.log", 19 | }); 20 | 21 | let enterpriseSettings: Partial; 22 | let configFile: boolean; 23 | 24 | reloadDatabase(); 25 | 26 | function reloadDatabase(): void { 27 | let enterpriseFile = "/etc/zulip-desktop-config/global_config.json"; 28 | if (process.platform === "win32") { 29 | enterpriseFile = String.raw`C:\Program Files\Zulip-Desktop-Config\global_config.json`; 30 | } 31 | 32 | enterpriseFile = path.resolve(enterpriseFile); 33 | if (fs.existsSync(enterpriseFile)) { 34 | configFile = true; 35 | try { 36 | const file = fs.readFileSync(enterpriseFile, "utf8"); 37 | const data: unknown = JSON.parse(file); 38 | enterpriseSettings = z 39 | .object(enterpriseConfigSchemata) 40 | .partial() 41 | .parse(data); 42 | } catch (error: unknown) { 43 | dialog.showErrorBox( 44 | "Error loading global_config", 45 | "We encountered an error while reading global_config.json, please make sure the file contains valid JSON.", 46 | ); 47 | logger.log("Error while JSON parsing global_config.json: "); 48 | logger.log(error); 49 | } 50 | } else { 51 | configFile = false; 52 | } 53 | } 54 | 55 | export function hasConfigFile(): boolean { 56 | return configFile; 57 | } 58 | 59 | export function getConfigItem( 60 | key: Key, 61 | defaultValue: EnterpriseConfig[Key], 62 | ): EnterpriseConfig[Key] { 63 | reloadDatabase(); 64 | if (!configFile) { 65 | return defaultValue; 66 | } 67 | 68 | const value = enterpriseSettings[key]; 69 | return value === undefined ? defaultValue : (value as EnterpriseConfig[Key]); 70 | } 71 | 72 | export function configItemExists(key: keyof EnterpriseConfig): boolean { 73 | reloadDatabase(); 74 | if (!configFile) { 75 | return false; 76 | } 77 | 78 | return enterpriseSettings[key] !== undefined; 79 | } 80 | 81 | export function isPresetOrg(url: string): boolean { 82 | if (!configFile || !configItemExists("presetOrganizations")) { 83 | return false; 84 | } 85 | 86 | const presetOrgs = enterpriseSettings.presetOrganizations; 87 | if (!Array.isArray(presetOrgs)) { 88 | throw new TypeError("Expected array for presetOrgs"); 89 | } 90 | 91 | for (const org of presetOrgs) { 92 | if (url.includes(org)) { 93 | return true; 94 | } 95 | } 96 | 97 | return false; 98 | } 99 | -------------------------------------------------------------------------------- /patches/i18n.patch: -------------------------------------------------------------------------------- 1 | diff --git a/i18n.js b/i18n.js 2 | index 950740589677df52c7e9b4c04249d83b4d1fe8df..c6221b0d73846f9f88f9f60228da0bc197058795 100644 3 | --- a/i18n.js 4 | +++ b/i18n.js 5 | @@ -16,7 +16,7 @@ const debug = require('debug')('i18n:debug') 6 | const warn = require('debug')('i18n:warn') 7 | const error = require('debug')('i18n:error') 8 | const Mustache = require('mustache') 9 | -const Messageformat = require('@messageformat/core') 10 | +const IntlMessageFormat = require('intl-messageformat').default 11 | const MakePlural = require('make-plural') 12 | const parseInterval = require('math-interval-parser').default 13 | 14 | @@ -27,7 +27,7 @@ const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') / 15 | * create constructor function 16 | */ 17 | const i18n = function I18n(_OPTS = false) { 18 | - const MessageformatInstanceForLocale = {} 19 | + const messageFormatCacheForLocale = new Map() 20 | const PluralsForLocale = {} 21 | let locales = {} 22 | const api = { 23 | @@ -300,11 +300,12 @@ const i18n = function I18n(_OPTS = false) { 24 | } 25 | 26 | i18n.__mf = function i18nMessageformat(phrase) { 27 | - let msg, mf, f 28 | + let msg, compiledFunctions, f 29 | let targetLocale = defaultLocale 30 | const argv = parseArgv(arguments) 31 | const namedValues = argv[0] 32 | - const args = argv[1] 33 | + if (argv[1].length > 0) 34 | + logWarn('i18n.__mf must be called with named values only') 35 | 36 | // called like __({phrase: "Hello", locale: "en"}) 37 | if (typeof phrase === 'object') { 38 | @@ -328,24 +329,23 @@ const i18n = function I18n(_OPTS = false) { 39 | 40 | // now head over to Messageformat 41 | // and try to cache instance 42 | - if (MessageformatInstanceForLocale[targetLocale]) { 43 | - mf = MessageformatInstanceForLocale[targetLocale] 44 | + if (messageFormatCacheForLocale.has(targetLocale)) { 45 | + compiledFunctions = messageFormatCacheForLocale.get(targetLocale) 46 | } else { 47 | - mf = new Messageformat(targetLocale) 48 | - 49 | - mf.compiledFunctions = {} 50 | - MessageformatInstanceForLocale[targetLocale] = mf 51 | + compiledFunctions = new Map() 52 | + messageFormatCacheForLocale.set(targetLocale, compiledFunctions) 53 | } 54 | 55 | // let's try to cache that function 56 | - if (mf.compiledFunctions[msg]) { 57 | - f = mf.compiledFunctions[msg] 58 | + if (compiledFunctions.has(msg)) { 59 | + f = compiledFunctions.get(msg) 60 | } else { 61 | - f = mf.compile(msg) 62 | - mf.compiledFunctions[msg] = f 63 | + const format = new IntlMessageFormat(msg, targetLocale) 64 | + f = format.format.bind(format) 65 | + compiledFunctions.set(msg, f) 66 | } 67 | 68 | - return postProcess(f(namedValues), namedValues, args) 69 | + return f(namedValues) 70 | } 71 | 72 | i18n.__l = function i18nTranslationList(phrase) { 73 | -------------------------------------------------------------------------------- /app/renderer/js/components/server-tab.ts: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | 3 | import {type Html, html} from "../../../common/html.ts"; 4 | import {ipcRenderer} from "../typed-ipc-renderer.ts"; 5 | 6 | import {generateNodeFromHtml} from "./base.ts"; 7 | import Tab, {type TabProperties} from "./tab.ts"; 8 | import type WebView from "./webview.ts"; 9 | 10 | export type ServerTabProperties = { 11 | webview: Promise; 12 | } & TabProperties; 13 | 14 | export default class ServerTab extends Tab { 15 | webview: Promise; 16 | $el: Element; 17 | $name: Element; 18 | $icon: HTMLImageElement; 19 | $badge: Element; 20 | 21 | constructor({webview, ...properties}: ServerTabProperties) { 22 | super(properties); 23 | 24 | this.webview = webview; 25 | this.$el = generateNodeFromHtml(this.templateHtml()); 26 | this.properties.$root.append(this.$el); 27 | this.registerListeners(); 28 | this.$name = this.$el.querySelector(".server-tooltip")!; 29 | this.$icon = this.$el.querySelector(".server-icons")!; 30 | this.$badge = this.$el.querySelector(".server-tab-badge")!; 31 | } 32 | 33 | override async activate(): Promise { 34 | await super.activate(); 35 | (await this.webview).load(); 36 | } 37 | 38 | override async deactivate(): Promise { 39 | await super.deactivate(); 40 | (await this.webview).hide(); 41 | } 42 | 43 | override async destroy(): Promise { 44 | await super.destroy(); 45 | (await this.webview).destroy(); 46 | } 47 | 48 | templateHtml(): Html { 49 | return html` 50 |
51 | 54 |
55 |
56 | 57 |
58 |
${this.generateShortcutText()}
59 |
60 | `; 61 | } 62 | 63 | setLabel(label: string): void { 64 | this.properties.label = label; 65 | this.$name.textContent = label; 66 | } 67 | 68 | setIcon(icon: string): void { 69 | this.properties.icon = icon; 70 | this.$icon.src = icon; 71 | } 72 | 73 | updateBadge(count: number): void { 74 | this.$badge.textContent = count > 999 ? "1K+" : count.toString(); 75 | this.$badge.classList.toggle("active", count > 0); 76 | } 77 | 78 | generateShortcutText(): string { 79 | // Only provide shortcuts for server [0..9] 80 | if (this.properties.index >= 9) { 81 | return ""; 82 | } 83 | 84 | const shownIndex = this.properties.index + 1; 85 | 86 | // Array index == Shown index - 1 87 | ipcRenderer.send("switch-server-tab", shownIndex - 1); 88 | 89 | return process.platform === "darwin" 90 | ? `⌘${shownIndex}` 91 | : `Ctrl+${shownIndex}`; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/server-info-form.ts: -------------------------------------------------------------------------------- 1 | import {dialog} from "@electron/remote"; 2 | 3 | import {html} from "../../../../common/html.ts"; 4 | import * as Messages from "../../../../common/messages.ts"; 5 | import * as t from "../../../../common/translation-util.ts"; 6 | import type {ServerConfig} from "../../../../common/types.ts"; 7 | import {generateNodeFromHtml} from "../../components/base.ts"; 8 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 9 | import * as DomainUtil from "../../utils/domain-util.ts"; 10 | 11 | type ServerInfoFormProperties = { 12 | $root: Element; 13 | server: ServerConfig; 14 | index: number; 15 | onChange: () => void; 16 | }; 17 | 18 | export function initServerInfoForm(properties: ServerInfoFormProperties): void { 19 | const $serverInfoForm = generateNodeFromHtml(html` 20 |
21 |
22 | 26 |
27 | ${properties.server.alias} 28 | open_in_new 29 |
30 |
31 |
32 |
33 | ${properties.server.url} 36 |
37 |
38 |
39 | ${t.__("Disconnect")} 40 |
41 |
42 |
43 |
44 | `); 45 | const $serverInfoAlias = $serverInfoForm.querySelector(".server-info-alias")!; 46 | const $serverIcon = $serverInfoForm.querySelector(".server-info-icon")!; 47 | const $deleteServerButton = $serverInfoForm.querySelector( 48 | ".server-delete-action", 49 | )!; 50 | const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!; 51 | properties.$root.append($serverInfoForm); 52 | 53 | $deleteServerButton.addEventListener("click", async () => { 54 | const {response} = await dialog.showMessageBox({ 55 | type: "warning", 56 | buttons: [t.__("Yes"), t.__("No")], 57 | defaultId: 0, 58 | message: t.__("Are you sure you want to disconnect this organization?"), 59 | }); 60 | if (response === 0) { 61 | if (DomainUtil.removeDomain(properties.index)) { 62 | ipcRenderer.send("reload-full-app"); 63 | } else { 64 | const {title, content} = Messages.orgRemovalError( 65 | DomainUtil.getDomain(properties.index).url, 66 | ); 67 | dialog.showErrorBox(title, content); 68 | } 69 | } 70 | }); 71 | 72 | $openServerButton.addEventListener("click", () => { 73 | ipcRenderer.send("forward-message", "switch-server-tab", properties.index); 74 | }); 75 | 76 | $serverInfoAlias.addEventListener("click", () => { 77 | ipcRenderer.send("forward-message", "switch-server-tab", properties.index); 78 | }); 79 | 80 | $serverIcon.addEventListener("click", () => { 81 | ipcRenderer.send("forward-message", "switch-server-tab", properties.index); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | # Development Setup 2 | 3 | This is a guide to running the Zulip desktop app from source, 4 | in order to contribute to developing it. 5 | 6 | ## Prerequisites 7 | 8 | To build and run the app from source, you'll need the following: 9 | 10 | - [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 11 | - Use our [Git Guide](https://zulip.readthedocs.io/en/latest/git/setup.html) to get started with Git and GitHub. 12 | - [Node.js](https://nodejs.org) (latest LTS version) 13 | - [pnpm](https://pnpm.io/installation) 14 | - [Python](https://www.python.org/downloads/) 15 | - A C++ compiler 16 | 17 | ### Ubuntu/Linux and other Debian-based distributions 18 | 19 | On a system running Debian, Ubuntu, or another Debian-based Linux 20 | distribution, you can install all dependencies through the package 21 | manager (see [here][node-debian] for more on the first command): 22 | 23 | ```sh 24 | $ curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash - 25 | $ sudo apt install git nodejs python build-essential 26 | $ corepack enable 27 | ``` 28 | 29 | [node-debian]: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions 30 | 31 | ### MacOS 32 | 33 | On a system running MacOS, you can refer to [official nodejs docs][node-mac] to 34 | install nodejs. To ensure Node.js has been installed, run `node -v` in terminal to know your node version. 35 | 36 | [node-mac]: https://nodejs.org/en/download/package-manager/#macos 37 | 38 | ### Windows 39 | 40 | - Download Node.js for Windows and install it. You can refer to the official docs [here][node-windows] to do so. To ensure Node.js has been installed, run `node -v` in Git Bash to know your node version. 41 | 42 | [node-windows]: https://nodejs.org/en/download/package-manager/#windows 43 | 44 | - Also, install Windows-Build-Tools to compile native node modules by using 45 | ```sh 46 | $ npm install --global windows-build-tools 47 | ``` 48 | 49 | ## Download, build, and run 50 | 51 | Clone the source locally: 52 | 53 | ```sh 54 | $ git clone https://github.com/zulip/zulip-desktop 55 | $ cd zulip-desktop 56 | ``` 57 | 58 | Install project dependencies: 59 | 60 | ```sh 61 | $ pnpm install 62 | ``` 63 | 64 | Start the app: 65 | 66 | ```sh 67 | $ node --run dev 68 | ``` 69 | 70 | Run tests: 71 | 72 | ```sh 73 | $ node --run test 74 | ``` 75 | 76 | ## How to contribute? 77 | 78 | Feel free to fork this repository, test it locally and then report any bugs 79 | you find in the [issue tracker](https://github.com/zulip/zulip-desktop/issues). 80 | 81 | You can read more about making contributions in our [Contributing Guide](./CONTRIBUTING.md). 82 | 83 | ## Making a release 84 | 85 | To package the app into an installer: 86 | 87 | ```sh 88 | node --run dist 89 | ``` 90 | 91 | This command will produce distributable packages or installers for the 92 | operating system you're running on: 93 | 94 | - on Windows, a .7z nsis file and a .exe WebSetup file 95 | - on macOS, a `.dmg` file 96 | - on Linux, a plain `.zip` file as well as a `.deb` file, `.snap` file and an 97 | `AppImage` file. 98 | 99 | To generate all three types of files, you will need all three operating 100 | systems. 101 | 102 | The output distributable packages appear in the `dist/` directory. 103 | -------------------------------------------------------------------------------- /app/common/config-util.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import * as Sentry from "@sentry/core"; 5 | import {JsonDB} from "node-json-db"; 6 | import {DataError} from "node-json-db/dist/lib/Errors.js"; 7 | import type {z} from "zod"; 8 | import {app, dialog} from "zulip:remote"; 9 | 10 | import {type ConfigSchemata, configSchemata} from "./config-schemata.ts"; 11 | import * as EnterpriseUtil from "./enterprise-util.ts"; 12 | import Logger from "./logger-util.ts"; 13 | 14 | export type Config = { 15 | [Key in keyof ConfigSchemata]: z.output; 16 | }; 17 | 18 | const logger = new Logger({ 19 | file: "config-util.log", 20 | }); 21 | 22 | let database: JsonDB; 23 | 24 | reloadDatabase(); 25 | 26 | export function getConfigItem( 27 | key: Key, 28 | defaultValue: Config[Key], 29 | ): z.output { 30 | try { 31 | database.reload(); 32 | } catch (error: unknown) { 33 | logger.error("Error while reloading settings.json: "); 34 | logger.error(error); 35 | } 36 | 37 | try { 38 | const typedSchemata: { 39 | [Key in keyof Config]: z.ZodType< 40 | z.output, 41 | z.input 42 | >; 43 | } = configSchemata; // https://github.com/colinhacks/zod/issues/5154 44 | return typedSchemata[key].parse(database.getObject(`/${key}`)); 45 | } catch (error: unknown) { 46 | if (!(error instanceof DataError)) throw error; 47 | setConfigItem(key, defaultValue); 48 | return defaultValue; 49 | } 50 | } 51 | 52 | // This function returns whether a key exists in the configuration file (settings.json) 53 | export function isConfigItemExists(key: string): boolean { 54 | try { 55 | database.reload(); 56 | } catch (error: unknown) { 57 | logger.error("Error while reloading settings.json: "); 58 | logger.error(error); 59 | } 60 | 61 | return database.exists(`/${key}`); 62 | } 63 | 64 | export function setConfigItem( 65 | key: Key, 66 | value: Config[Key], 67 | override?: boolean, 68 | ): void { 69 | if (EnterpriseUtil.configItemExists(key) && !override) { 70 | // If item is in global config and we're not trying to override 71 | return; 72 | } 73 | 74 | configSchemata[key].parse(value); 75 | database.push(`/${key}`, value, true); 76 | database.save(); 77 | } 78 | 79 | export function removeConfigItem(key: string): void { 80 | database.delete(`/${key}`); 81 | database.save(); 82 | } 83 | 84 | function reloadDatabase(): void { 85 | const settingsJsonPath = path.join( 86 | app.getPath("userData"), 87 | "/config/settings.json", 88 | ); 89 | try { 90 | const file = fs.readFileSync(settingsJsonPath, "utf8"); 91 | JSON.parse(file); 92 | } catch (error: unknown) { 93 | if (fs.existsSync(settingsJsonPath)) { 94 | fs.unlinkSync(settingsJsonPath); 95 | dialog.showErrorBox( 96 | "Error saving settings", 97 | "We encountered an error while saving the settings.", 98 | ); 99 | logger.error("Error while JSON parsing settings.json: "); 100 | logger.error(error); 101 | Sentry.captureException(error); 102 | } 103 | } 104 | 105 | database = new JsonDB(settingsJsonPath, true, true); 106 | } 107 | -------------------------------------------------------------------------------- /app/common/typed-ipc.ts: -------------------------------------------------------------------------------- 1 | import type {DndSettings} from "./dnd-util.ts"; 2 | import type {MenuProperties, ServerConfig} from "./types.ts"; 3 | 4 | export type MainMessage = { 5 | "clear-app-settings": () => void; 6 | "configure-spell-checker": () => void; 7 | "fetch-user-agent": () => string; 8 | "focus-app": () => void; 9 | "focus-this-webview": () => void; 10 | "new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array}; 11 | "permission-callback": (permissionCallbackId: number, grant: boolean) => void; 12 | "quit-app": () => void; 13 | "realm-icon-changed": (serverURL: string, iconURL: string) => void; 14 | "realm-name-changed": (serverURL: string, realmName: string) => void; 15 | "reload-full-app": () => void; 16 | "save-last-tab": (index: number) => void; 17 | "switch-server-tab": (index: number) => void; 18 | "toggle-app": () => void; 19 | "toggle-badge-option": (newValue: boolean) => void; 20 | "toggle-menubar": (showMenubar: boolean) => void; 21 | toggleAutoLauncher: (AutoLaunchValue: boolean) => void; 22 | "unread-count": (unreadCount: number) => void; 23 | "update-badge": (messageCount: number) => void; 24 | "update-menu": (properties: MenuProperties) => void; 25 | "update-taskbar-icon": (data: string, text: string) => void; 26 | }; 27 | 28 | export type MainCall = { 29 | "get-server-settings": (domain: string) => ServerConfig; 30 | "is-online": (url: string) => boolean; 31 | "poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined; 32 | "save-server-icon": (iconURL: string) => string | null; 33 | }; 34 | 35 | export type RendererMessage = { 36 | back: () => void; 37 | "copy-zulip-url": () => void; 38 | destroytray: () => void; 39 | "enter-fullscreen": () => void; 40 | focus: () => void; 41 | "focus-webview-with-id": (webviewId: number) => void; 42 | forward: () => void; 43 | "hard-reload": () => void; 44 | "leave-fullscreen": () => void; 45 | "log-out": () => void; 46 | logout: () => void; 47 | "new-server": () => void; 48 | "open-about": () => void; 49 | "open-help": () => void; 50 | "open-network-settings": () => void; 51 | "open-org-tab": () => void; 52 | "open-settings": () => void; 53 | "permission-request": ( 54 | options: {webContentsId: number | null; origin: string; permission: string}, 55 | rendererCallbackId: number, 56 | ) => void; 57 | "play-ding-sound": () => void; 58 | "reload-current-viewer": () => void; 59 | "reload-proxy": (showAlert: boolean) => void; 60 | "reload-viewer": () => void; 61 | "render-taskbar-icon": (messageCount: number) => void; 62 | "set-active": () => void; 63 | "set-idle": () => void; 64 | "show-keyboard-shortcuts": () => void; 65 | "show-notification-settings": () => void; 66 | "switch-server-tab": (index: number) => void; 67 | "tab-devtools": () => void; 68 | "toggle-autohide-menubar": ( 69 | autoHideMenubar: boolean, 70 | updateMenu: boolean, 71 | ) => void; 72 | "toggle-dnd": (state: boolean, newSettings: Partial) => void; 73 | "toggle-sidebar": (show: boolean) => void; 74 | "toggle-silent": (state: boolean) => void; 75 | "toggle-tray": (state: boolean) => void; 76 | toggletray: () => void; 77 | tray: (argument: number) => void; 78 | "update-realm-icon": (serverURL: string, iconURL: string) => void; 79 | "update-realm-name": (serverURL: string, realmName: string) => void; 80 | "webview-reload": () => void; 81 | zoomActualSize: () => void; 82 | zoomIn: () => void; 83 | zoomOut: () => void; 84 | }; 85 | -------------------------------------------------------------------------------- /app/main/request.ts: -------------------------------------------------------------------------------- 1 | import {type Session, app} from "electron/main"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | import {Readable} from "node:stream"; 5 | import {pipeline} from "node:stream/promises"; 6 | import type {ReadableStream} from "node:stream/web"; 7 | 8 | import * as Sentry from "@sentry/electron/main"; 9 | import {z} from "zod"; 10 | 11 | import Logger from "../common/logger-util.ts"; 12 | import * as Messages from "../common/messages.ts"; 13 | import type {ServerConfig} from "../common/types.ts"; 14 | 15 | /* Request: domain-util */ 16 | 17 | const logger = new Logger({ 18 | file: "domain-util.log", 19 | }); 20 | 21 | const generateFilePath = (url: string): string => { 22 | const directory = `${app.getPath("userData")}/server-icons`; 23 | const extension = path.extname(url).split("?")[0]; 24 | 25 | let hash = 5381; 26 | let {length} = url; 27 | 28 | while (length) { 29 | // eslint-disable-next-line no-bitwise, unicorn/prefer-code-point 30 | hash = (hash * 33) ^ url.charCodeAt(--length); 31 | } 32 | 33 | // Create 'server-icons' directory if not existed 34 | if (!fs.existsSync(directory)) { 35 | fs.mkdirSync(directory); 36 | } 37 | 38 | // eslint-disable-next-line no-bitwise 39 | return `${directory}/${hash >>> 0}${extension}`; 40 | }; 41 | 42 | export const _getServerSettings = async ( 43 | domain: string, 44 | session: Session, 45 | ): Promise => { 46 | const response = await session.fetch(domain + "/api/v1/server_settings"); 47 | if (!response.ok) { 48 | throw new Error(Messages.invalidZulipServerError(domain)); 49 | } 50 | 51 | const data: unknown = await response.json(); 52 | /* eslint-disable @typescript-eslint/naming-convention */ 53 | const { 54 | realm_name, 55 | realm_uri, 56 | realm_icon, 57 | zulip_version, 58 | zulip_feature_level, 59 | } = z 60 | .object({ 61 | realm_name: z.string(), 62 | realm_uri: z.url(), 63 | realm_icon: z.string(), 64 | zulip_version: z.string().default("unknown"), 65 | zulip_feature_level: z.number().default(0), 66 | }) 67 | .parse(data); 68 | /* eslint-enable @typescript-eslint/naming-convention */ 69 | 70 | return { 71 | // Some Zulip Servers use absolute URL for server icon whereas others use relative URL 72 | // Following check handles both the cases 73 | icon: realm_icon.startsWith("/") ? realm_uri + realm_icon : realm_icon, 74 | url: realm_uri, 75 | alias: realm_name, 76 | zulipVersion: zulip_version, 77 | zulipFeatureLevel: zulip_feature_level, 78 | }; 79 | }; 80 | 81 | export const _saveServerIcon = async ( 82 | url: string, 83 | session: Session, 84 | ): Promise => { 85 | try { 86 | const response = await session.fetch(url); 87 | if (!response.ok) { 88 | logger.log("Could not get server icon."); 89 | return null; 90 | } 91 | 92 | const filePath = generateFilePath(url); 93 | await pipeline( 94 | Readable.fromWeb(response.body as ReadableStream), 95 | fs.createWriteStream(filePath), 96 | ); 97 | return filePath; 98 | } catch (error: unknown) { 99 | logger.log("Could not get server icon."); 100 | logger.log(error); 101 | Sentry.captureException(error); 102 | return null; 103 | } 104 | }; 105 | 106 | /* Request: reconnect-util */ 107 | 108 | export const _isOnline = async ( 109 | url: string, 110 | session: Session, 111 | ): Promise => { 112 | try { 113 | const response = await session.fetch(`${url}/api/v1/server_settings`, { 114 | method: "HEAD", 115 | }); 116 | return response.ok; 117 | } catch (error: unknown) { 118 | logger.log(error); 119 | return false; 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/new-server-form.ts: -------------------------------------------------------------------------------- 1 | import {dialog} from "@electron/remote"; 2 | 3 | import {html} from "../../../../common/html.ts"; 4 | import * as LinkUtil from "../../../../common/link-util.ts"; 5 | import * as t from "../../../../common/translation-util.ts"; 6 | import {generateNodeFromHtml} from "../../components/base.ts"; 7 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 8 | import * as DomainUtil from "../../utils/domain-util.ts"; 9 | 10 | type NewServerFormProperties = { 11 | $root: Element; 12 | onChange: () => void; 13 | }; 14 | 15 | export function initNewServerForm({ 16 | $root, 17 | onChange, 18 | }: NewServerFormProperties): void { 19 | const $newServerForm = generateNodeFromHtml(html` 20 |
21 |
${t.__("Organization URL")}
22 |
23 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | ${t.__("OR")} 38 |
39 |
40 |
41 |
42 | 45 |
46 |
47 |
48 | ${t.__("Network and Proxy Settings")} 51 | open_in_new 52 |
53 |
54 |
55 | `); 56 | const $saveServerButton: HTMLButtonElement = 57 | $newServerForm.querySelector("#connect")!; 58 | $root.textContent = ""; 59 | $root.append($newServerForm); 60 | const $newServerUrl: HTMLInputElement = $newServerForm.querySelector( 61 | "input.setting-input-value", 62 | )!; 63 | 64 | async function submitFormHandler(): Promise { 65 | $saveServerButton.textContent = t.__("Connecting…"); 66 | let serverConfig; 67 | try { 68 | serverConfig = await DomainUtil.checkDomain($newServerUrl.value.trim()); 69 | } catch (error: unknown) { 70 | $saveServerButton.textContent = t.__("Connect"); 71 | await dialog.showMessageBox({ 72 | type: "error", 73 | message: 74 | error instanceof Error 75 | ? `${error.name}: ${error.message}` 76 | : t.__("Unknown error"), 77 | buttons: [t.__("OK")], 78 | }); 79 | return; 80 | } 81 | 82 | await DomainUtil.addDomain(serverConfig); 83 | onChange(); 84 | } 85 | 86 | $saveServerButton.addEventListener("click", async () => { 87 | await submitFormHandler(); 88 | }); 89 | $newServerUrl.addEventListener("keypress", async (event) => { 90 | if (event.key === "Enter") { 91 | await submitFormHandler(); 92 | } 93 | }); 94 | 95 | // Open create new org link in default browser 96 | const link = "https://zulip.com/new/"; 97 | const externalCreateNewOrgElement = $root.querySelector( 98 | "#open-create-org-link", 99 | )!; 100 | externalCreateNewOrgElement.addEventListener("click", async () => { 101 | await LinkUtil.openBrowser(new URL(link)); 102 | }); 103 | 104 | const networkSettingsId = $root.querySelector(".server-network-option")!; 105 | networkSettingsId.addEventListener("click", () => { 106 | ipcRenderer.send("forward-message", "open-network-settings"); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /app/renderer/js/electron-bridge.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | import { 4 | type ClipboardDecrypter, 5 | ClipboardDecrypterImplementation, 6 | } from "./clipboard-decrypter.ts"; 7 | import {type NotificationData, newNotification} from "./notification/index.ts"; 8 | import {ipcRenderer} from "./typed-ipc-renderer.ts"; 9 | 10 | type ListenerType = (...arguments_: unknown[]) => void; 11 | 12 | /* eslint-disable @typescript-eslint/naming-convention */ 13 | export type ElectronBridge = { 14 | send_event: (eventName: string, ...arguments_: unknown[]) => boolean; 15 | on_event: (eventName: string, listener: ListenerType) => void; 16 | new_notification: ( 17 | title: string, 18 | options: NotificationOptions, 19 | dispatch: (type: string, eventInit: EventInit) => boolean, 20 | ) => NotificationData; 21 | get_idle_on_system: () => boolean; 22 | get_last_active_on_system: () => number; 23 | get_send_notification_reply_message_supported: () => boolean; 24 | set_send_notification_reply_message_supported: (value: boolean) => void; 25 | decrypt_clipboard: (version: number) => ClipboardDecrypter; 26 | }; 27 | /* eslint-enable @typescript-eslint/naming-convention */ 28 | 29 | let notificationReplySupported = false; 30 | // Indicates if the user is idle or not 31 | let idle = false; 32 | // Indicates the time at which user was last active 33 | let lastActive = Date.now(); 34 | 35 | export const bridgeEvents = new EventTarget(); 36 | 37 | export class BridgeEvent extends Event { 38 | constructor( 39 | type: string, 40 | public readonly arguments_: unknown[] = [], 41 | ) { 42 | super(type); 43 | } 44 | } 45 | 46 | /* eslint-disable @typescript-eslint/naming-convention */ 47 | const electron_bridge: ElectronBridge = { 48 | send_event: (eventName: string, ...arguments_: unknown[]): boolean => 49 | bridgeEvents.dispatchEvent(new BridgeEvent(eventName, arguments_)), 50 | 51 | on_event(eventName: string, listener: ListenerType): void { 52 | bridgeEvents.addEventListener(eventName, (event) => { 53 | listener(...z.instanceof(BridgeEvent).parse(event).arguments_); 54 | }); 55 | }, 56 | 57 | new_notification: ( 58 | title: string, 59 | options: NotificationOptions, 60 | dispatch: (type: string, eventInit: EventInit) => boolean, 61 | ): NotificationData => newNotification(title, options, dispatch), 62 | 63 | get_idle_on_system: (): boolean => idle, 64 | 65 | get_last_active_on_system: (): number => lastActive, 66 | 67 | get_send_notification_reply_message_supported: (): boolean => 68 | notificationReplySupported, 69 | 70 | set_send_notification_reply_message_supported(value: boolean): void { 71 | notificationReplySupported = value; 72 | }, 73 | 74 | decrypt_clipboard: (version: number): ClipboardDecrypter => 75 | new ClipboardDecrypterImplementation(version), 76 | }; 77 | /* eslint-enable @typescript-eslint/naming-convention */ 78 | 79 | bridgeEvents.addEventListener("total_unread_count", (event) => { 80 | const [unreadCount] = z 81 | .tuple([z.number()]) 82 | .parse(z.instanceof(BridgeEvent).parse(event).arguments_); 83 | ipcRenderer.send("unread-count", unreadCount); 84 | }); 85 | 86 | bridgeEvents.addEventListener("realm_name", (event) => { 87 | const [realmName] = z 88 | .tuple([z.string()]) 89 | .parse(z.instanceof(BridgeEvent).parse(event).arguments_); 90 | const serverUrl = location.origin; 91 | ipcRenderer.send("realm-name-changed", serverUrl, realmName); 92 | }); 93 | 94 | bridgeEvents.addEventListener("realm_icon_url", (event) => { 95 | const [iconUrl] = z 96 | .tuple([z.string()]) 97 | .parse(z.instanceof(BridgeEvent).parse(event).arguments_); 98 | const serverUrl = location.origin; 99 | ipcRenderer.send( 100 | "realm-icon-changed", 101 | serverUrl, 102 | iconUrl.includes("http") ? iconUrl : `${serverUrl}${iconUrl}`, 103 | ); 104 | }); 105 | 106 | // Set user as active and update the time of last activity 107 | ipcRenderer.on("set-active", () => { 108 | idle = false; 109 | lastActive = Date.now(); 110 | }); 111 | 112 | // Set user as idle and time of last activity is left unchanged 113 | ipcRenderer.on("set-idle", () => { 114 | idle = true; 115 | }); 116 | 117 | // This follows node's idiomatic implementation of event 118 | // emitters to make event handling more simpler instead of using 119 | // functions zulip side will emit event using ElectronBridge.send_event 120 | // which is alias of .emit and on this side we can handle the data by adding 121 | // a listener for the event. 122 | export default electron_bridge; 123 | -------------------------------------------------------------------------------- /public/translations/si.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "සුලිප් පිළිබඳව", 3 | "Actual Size": "සැබෑ ප්‍රමාණය", 4 | "Add Organization": "සංවිධානය එක්කරන්න", 5 | "Add a Zulip organization": "සුලිප් සංවිධානයක් එක්කරන්න", 6 | "Add custom CSS": "අභිරුචි සීඑස්එස් යොදන්න", 7 | "Add to Dictionary": "ශබ්දකෝෂයට යොදන්න", 8 | "Advanced": "වැඩිදුර", 9 | "Always start minimized": "හකුළුවා දියත් කරන්න", 10 | "App Updates": "යෙදුමේ යාවත්කාල", 11 | "App language (requires restart)": "යෙදුමේ භාෂාව (යළි ඇරඹීම අවශ්‍යයි)", 12 | "Appearance": "පෙනුම", 13 | "Application Shortcuts": "යෙදුමේ කෙටිමං", 14 | "Are you sure you want to disconnect this organization?": "ඔබට මෙම සංවිධානය විසන්ධි කිරීමට අවශ්‍ය බව විශ්වාසද?", 15 | "Ask where to save files before downloading": "බාගැනීමට පෙර සුරැකිය යුතු ස්ථානය අසන්න", 16 | "Auto hide Menu bar": "වට්ටෝරු තීරුව ස්වයං සැඟවීම", 17 | "Auto hide menu bar (Press Alt key to display)": "වට්ටෝරු තීරුව ස්වයං සැඟවීම (දැකීමට Alt ඔබන්න)", 18 | "Back": "ආපසු", 19 | "Bounce dock on new private message": "නව පණිවිඩ ලැබෙන විට නිරූපකය සෙලවීම", 20 | "Cancel": "අවලංගු", 21 | "Change": "සංශෝධනය", 22 | "Check for Updates": "යාවත්කාල බලන්න", 23 | "Close": "වසන්න", 24 | "Connect": "සබඳින්න", 25 | "Connect to another organization": "වෙනත් සංවිධානයකට සබැඳින්න", 26 | "Connected organizations": "සම්බන්ධිත සංවිධාන", 27 | "Copy": "පිටපතක්", 28 | "Copy Image": "අනුරුවෙහි පිටපතක්", 29 | "Copy Image URL": "අනුරුවෙහි ඒ.ස.නි. පිටපතක්", 30 | "Copy Link": "සබැඳියේ පිටපතක්", 31 | "Copy Zulip URL": "සුලිප් ඒ.ස.නි. පිටපතක්", 32 | "Create a new organization": "නව සංවිධානයක් සාදන්න", 33 | "Cut": "කපන්න", 34 | "Default download location": "පෙරනිමි බාගැනීමේ ස්ථානය", 35 | "Delete": "මකන්න", 36 | "Desktop Notifications": "වැඩතල දැනුම්දීම්", 37 | "Desktop Settings": "වැඩතල සැකසුම්", 38 | "Disconnect": "විසන්ධි කරන්න", 39 | "Download App Logs": "යෙදුමේ සටහන් බාගන්න", 40 | "Edit": "සංස්කරණය", 41 | "Edit Shortcuts": "කෙටිමං සංස්කරණය", 42 | "Enable auto updates": "ස්වයං යාවත්කාල සබලකරන්න", 43 | "Enable error reporting (requires restart)": "දෝෂ වාර්තාව සබලකරන්න (යළි ඇරඹීම අවශ්‍යයි)", 44 | "Enable spellchecker (requires restart)": "අකුරු වින්‍යාසය පරීක්‍ෂාව (යළි අරඹන්න)", 45 | "File": "ගොනුව", 46 | "Find accounts": "ගිණුම් සොයාගන්න", 47 | "Find accounts by email": "වි-තැපෑලෙන් ගිණුම් සොයාගන්න", 48 | "Flash taskbar on new message": "නව පණිවිඩ සඳහා කාර්යතීරුව දිදුලන්න", 49 | "Forward": "හරවන්න", 50 | "Get beta updates": "බීටා යාවත්කාල ගන්න", 51 | "Help": "උදව්", 52 | "Help Center": "උදව් මධ්‍යස්ථානය", 53 | "Hide": "සඟවන්න", 54 | "Hide Others": "අන් අය සඟවන්න", 55 | "History": "ඉතිහාසය", 56 | "History Shortcuts": "ඉතිහාස කෙටිමං", 57 | "Keyboard Shortcuts": "යතුරුපුවරුවේ කෙටිමං", 58 | "Log Out": "නික්මෙන්න", 59 | "Log Out of Organization": "සංවිධානයෙන් නික්මෙන්න", 60 | "Manual proxy configuration": "අතින් ප්‍රතියුක්තය වින්‍යාසය", 61 | "Minimize": "හකුළන්න", 62 | "Mute all sounds from Zulip": "සියළුම සුලිප් ශබ්ද නිහඬකරන්න", 63 | "Network": "ජාලය", 64 | "No Suggestion Found": "යෝජනා හමු නොවිණි", 65 | "OK": "හරි", 66 | "OR": "හෝ", 67 | "On macOS, the OS spellchecker is used.": "මැක්ඕඑස් හි, මෙ. පද්. අකුරු පරීක්‍ෂාව භාවිතයි.", 68 | "Organization URL": "සංවිධානයේ ඒ.ස.නි.", 69 | "Organizations": "සංවිධාන", 70 | "Paste": "අලවන්න", 71 | "Proxy": "ප්‍රතියුක්තය", 72 | "Proxy rules": "ප්‍රතියුක්තයේ නීති", 73 | "Quit": "ඉවත් වන්න", 74 | "Quit Zulip": "සුලිප් වෙතින් ඉවත්වන්න", 75 | "Quit when the window is closed": "කවුළුව වැසූ විට ඉවත් වන්න", 76 | "Redo": "පසුසේ", 77 | "Release Notes": "නිකුතු සටහන්", 78 | "Reload": "යළි පූරණය", 79 | "Save": "සුරකින්න", 80 | "Select All": "සියල්ල තෝරන්න", 81 | "Services": "සේවා", 82 | "Settings": "සැකසුම්", 83 | "Shortcuts": "කෙටිමං", 84 | "Show desktop notifications": "වැඩතලයෙහි දැනුම්දීම් පෙන්වන්න", 85 | "Show sidebar": "පැති තීරුව පෙන්වන්න", 86 | "Spellchecker Languages": "අකුරු වින්‍යාසය පරීක්‍ෂාවට භාෂා", 87 | "Switch to Next Organization": "ඊළඟ සංවිධානයට මාරුවන්න", 88 | "Switch to Previous Organization": "කලින් සංවිධානයට මාරුවන්න", 89 | "Tools": "මෙවලම්", 90 | "Undo": "පෙරසේ", 91 | "Unhide": "නොසඟවන්න", 92 | "Upload": "උඩුගත කරන්න", 93 | "Use system proxy settings (requires restart)": "පද්ධතියේ ප්‍රතියුක්තය භාවිතය (යළි ඇරඹිය යුතුය)", 94 | "View": "බලන්න", 95 | "View Shortcuts": "කෙටිමං බලන්න", 96 | "Window": "කවුළුව", 97 | "Window Shortcuts": "කවුළුවේ කෙටිමං", 98 | "Yes": "ඔව්", 99 | "You can select a maximum of 3 languages for spellchecking.": "අකුරු වින්‍යාසය පරීක්‍ෂාව සඳහා උපරිම භාෂා 3 ක් තේරීමට හැකිය.", 100 | "Zoom In": "විශාලනය", 101 | "Zoom Out": "කුඩාලනය", 102 | "keyboard shortcuts": "යතුරුපුවරුවේ කෙටිමං" 103 | } 104 | -------------------------------------------------------------------------------- /public/translations/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "जूलिप के बारे में", 3 | "Actual Size": "वास्तविक आकार", 4 | "Add Organization": "संगठन जोड़ें", 5 | "Add a Zulip organization": "एक जूलिप संगठन जोड़ें", 6 | "Add custom CSS": "कस्टम CSS जोड़ें", 7 | "Advanced": "उन्नत", 8 | "Always start minimized": "हमेशा कम से कम शुरू करें", 9 | "App Updates": "ऐप अपडेट", 10 | "Appearance": "दिखावट", 11 | "Application Shortcuts": "आवेदन शॉर्टकट", 12 | "Are you sure you want to disconnect this organization?": "क्या आप वाकई इस संगठन को डिस्कनेक्ट करना चाहते हैं?", 13 | "Auto hide Menu bar": "ऑटो मेनू छुपाएँ", 14 | "Auto hide menu bar (Press Alt key to display)": "ऑटो छिपाने मेनू बार (प्रेस Alt कुंजी प्रदर्शित करने के लिए)", 15 | "Back": "वापस", 16 | "Bounce dock on new private message": "नए निजी संदेश पर बाउंस डॉक", 17 | "Cancel": "रद्द करना", 18 | "Change": "परिवर्तन", 19 | "Check for Updates": "अद्यतन के लिए जाँच", 20 | "Close": "बंद करे", 21 | "Connect": "जुडिये", 22 | "Connect to another organization": "किसी अन्य संगठन से कनेक्ट करें", 23 | "Connected organizations": "जुड़े हुए संगठन", 24 | "Copy": "प्रतिलिपि", 25 | "Copy Zulip URL": "Zulip URL को कॉपी करें", 26 | "Create a new organization": "एक नया संगठन बनाएं", 27 | "Cut": "कट गया", 28 | "Default download location": "डिफ़ॉल्ट डाउनलोड स्थान", 29 | "Delete": "हटाना", 30 | "Desktop Notifications": "डेस्कटॉप सूचनाएं", 31 | "Desktop Settings": "डेस्कटॉप सेटिंग्स", 32 | "Disconnect": "डिस्कनेक्ट", 33 | "Download App Logs": "ऐप लॉग डाउनलोड करें", 34 | "Edit": "संपादित करें", 35 | "Edit Shortcuts": "शॉर्टकट संपादित करें", 36 | "Enable auto updates": "ऑटो अपडेट सक्षम करें", 37 | "Enable error reporting (requires restart)": "त्रुटि रिपोर्टिंग सक्षम करें (पुनरारंभ की आवश्यकता है)", 38 | "Enable spellchecker (requires restart)": "वर्तनी जाँचक सक्षम करें (पुनः आरंभ करने की आवश्यकता है)", 39 | "Factory Reset": "नए यंत्र जैसी सेटिंग", 40 | "File": "फ़ाइल", 41 | "Find accounts": "खाते ढूंढे", 42 | "Find accounts by email": "ईमेल द्वारा खाते ढूंढें", 43 | "Flash taskbar on new message": "नए संदेश पर फ्लैश टास्कबार", 44 | "Forward": "आगे", 45 | "Functionality": "कार्यक्षमता", 46 | "General": "सामान्य", 47 | "Get beta updates": "बीटा अपडेट प्राप्त करें", 48 | "Hard Reload": "हार्ड रीलोड", 49 | "Help": "मदद", 50 | "Help Center": "सहायता केंद्र", 51 | "Hide": "छिपाना", 52 | "History": "इतिहास", 53 | "History Shortcuts": "इतिहास शॉर्टकट", 54 | "Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग", 55 | "Log Out": "लोग आउट", 56 | "Log Out of Organization": "संगठन से बाहर प्रवेश करें", 57 | "Manual proxy configuration": "मैनुअल प्रॉक्सी कॉन्फ़िगरेशन", 58 | "Minimize": "छोटा करना", 59 | "Mute all sounds from Zulip": "ज़ूलिप से सभी ध्वनियों को म्यूट करें", 60 | "Network": "नेटवर्क", 61 | "OK": "ठीक", 62 | "OR": "या", 63 | "Organization URL": "संगठन का URL", 64 | "Organizations": "संगठन", 65 | "Paste": "चिपकाएं", 66 | "Paste and Match Style": "पेस्ट और मैच स्टाइल", 67 | "Proxy": "प्रतिनिधि", 68 | "Proxy bypass rules": "प्रॉक्सी बायपास नियम", 69 | "Proxy rules": "प्रॉक्सी नियम", 70 | "Quit": "छोड़ना", 71 | "Quit Zulip": "जूलिप छोड़ दें", 72 | "Redo": "फिर से करना", 73 | "Release Notes": "रिलीज नोट्स", 74 | "Reload": "पुनः लोड करें", 75 | "Report an Issue": "मामले की रिपोर्ट करें", 76 | "Save": "बचाना / सहेजें", 77 | "Select All": "सभी का चयन करे", 78 | "Settings": "सेटिंग्स", 79 | "Shortcuts": "शॉर्टकट", 80 | "Show app icon in system tray": "सिस्टम ट्रे में ऐप आइकन दिखाएं", 81 | "Show desktop notifications": "डेस्कटॉप सूचनाएं दिखाएं", 82 | "Show sidebar": "साइडबार दिखाओ", 83 | "Start app at login": "लॉगिन पर ऐप शुरू करें", 84 | "Switch to Next Organization": "अगला संगठन पर स्विच करें", 85 | "Switch to Previous Organization": "पिछले संगठन पर स्विच करें", 86 | "These desktop app shortcuts extend the Zulip webapp's": "ये डेस्कटॉप ऐप शॉर्टकट Zulip webapp's का विस्तार करते हैं", 87 | "Tip": "टिप", 88 | "Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें", 89 | "Toggle DevTools for Zulip App": "Zulip App के लिए DevTools टॉगल करें", 90 | "Toggle Do Not Disturb": "टॉगल डू नॉट डिस्टर्ब", 91 | "Toggle Full Screen": "पूर्णस्क्रीन चालू करें", 92 | "Toggle Sidebar": "टॉगल साइडबार", 93 | "Toggle Tray Icon": "टॉगल ट्रे आइकन", 94 | "Tools": "उपकरण", 95 | "Undo": "पूर्ववत करें", 96 | "Unhide": "प्रकट करें", 97 | "Upload": "अपलोड", 98 | "Use system proxy settings (requires restart)": "सिस्टम प्रॉक्सी सेटिंग्स का उपयोग करें (पुनः आरंभ करने की आवश्यकता है)", 99 | "View": "राय", 100 | "View Shortcuts": "शॉर्टकट देखें", 101 | "Window": "खिड़की", 102 | "Window Shortcuts": "विंडो शॉर्टकट", 103 | "Zoom In": "ज़ूम इन", 104 | "Zoom Out": "ज़ूम आउट", 105 | "keyboard shortcuts": "कुंजीपटल अल्प मार्ग" 106 | } 107 | -------------------------------------------------------------------------------- /public/translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "About": "관하여", 3 | "About Zulip": "Zulip에 대해", 4 | "Actual Size": "실제 크기", 5 | "Add Organization": "조직 추가", 6 | "Add a Zulip organization": "새로운 Zulip 조직 추가", 7 | "Add custom CSS": "맞춤 CSS 추가", 8 | "Add to Dictionary": "사전에 추가하기", 9 | "Advanced": "많은", 10 | "Always start minimized": "항상 최소화 된 상태로 시작하십시오", 11 | "App Updates": "앱 업데이트", 12 | "App language (requires restart)": "앱 언어 (재시작 필요함)", 13 | "Appearance": "외관", 14 | "Application Shortcuts": "애플리케이션 단축키", 15 | "Are you sure you want to disconnect this organization?": "이 조직의 연결을 해제 하시겠습니까?", 16 | "Ask where to save files before downloading": "다운로드 전에 어디에 파일을 저장할지 묻기", 17 | "Auto hide Menu bar": "메뉴 바 자동 숨기기", 18 | "Auto hide menu bar (Press Alt key to display)": "메뉴 바 자동 숨기기 (표시하려면 Alt 키를 누릅니다)", 19 | "Back": "뒤로가기", 20 | "Bounce dock on new private message": "새로운 비공개 메시지에 바운스 독", 21 | "Cancel": "취소", 22 | "Change": "변경", 23 | "Change the language from System Preferences → Keyboard → Text → Spelling.": "시스템 환경설정 → 키보드 → 텍스트 → 맞춤법에서 언어를 바꾸세요.", 24 | "Check for Updates": "업데이트 확인", 25 | "Close": "닫기", 26 | "Connect": "연결", 27 | "Connect to another organization": "다른 조직에 연결", 28 | "Connected organizations": "연결된 조직", 29 | "Copy": "복사", 30 | "Copy Image": "이미지 복사", 31 | "Copy Image URL": "이미지 URL 복사", 32 | "Copy Link": "링크 복사", 33 | "Copy Zulip URL": "Zulip URL 복사", 34 | "Create a new organization": "새 조직 만들기", 35 | "Cut": "잘라내기", 36 | "Default download location": "기본 다운로드 위치", 37 | "Delete": "삭제", 38 | "Desktop Notifications": "데스크톱 알림", 39 | "Desktop Settings": "데스크톱 설정", 40 | "Disconnect": "연결 끊기", 41 | "Download App Logs": "앱 로그 다운로드", 42 | "Edit": "편집하다", 43 | "Edit Shortcuts": "바로 가기 편집", 44 | "Enable auto updates": "자동 업데이트 사용", 45 | "Enable error reporting (requires restart)": "오류보고 사용 (재시작 필요)", 46 | "Enable spellchecker (requires restart)": "맞춤법 검사기 사용 (재시작 필요)", 47 | "Factory Reset": "공장 초기화", 48 | "Factory Reset Data": "공장 초기화 정보", 49 | "File": "파일", 50 | "Find accounts": "계정 찾기", 51 | "Find accounts by email": "이메일을 통한 계정 찾기", 52 | "Flash taskbar on new message": "새 메시지의 Flash 작업 표시 줄", 53 | "Forward": "앞으로", 54 | "Functionality": "기능", 55 | "General": "일반", 56 | "Get beta updates": "베타 업데이트 받기", 57 | "Hard Reload": "하드 다시로드", 58 | "Help": "도움", 59 | "Help Center": "지원 센터", 60 | "Hide": "숨기기", 61 | "Hide Others": "나머지 숨기기", 62 | "History": "히스토리", 63 | "History Shortcuts": "히스토리 단축키", 64 | "Keyboard Shortcuts": "키보드 단축키", 65 | "Log Out": "로그 아웃", 66 | "Log Out of Organization": "조직에서 로그 아웃", 67 | "Look Up": "찾아보기", 68 | "Manual proxy configuration": "수동 프록시 구성", 69 | "Minimize": "최소화", 70 | "Mute all sounds from Zulip": "Zulip에서 모든 소리를 음소거합니다", 71 | "Network": "네트워크", 72 | "No": "아니오", 73 | "No Suggestion Found": "추천을 찾지 못했습니다", 74 | "Notification settings": "알림 설정", 75 | "OK": "OK", 76 | "OR": "또는", 77 | "On macOS, the OS spellchecker is used.": "macOS에서는 운영체제의 맞춤법 검사기가 사용됩니다.", 78 | "Organization URL": "조직 URL", 79 | "Organizations": "조직", 80 | "Paste": "붙여넣기", 81 | "Paste and Match Style": "스타일을 일치시켜 붙여넣기", 82 | "Proxy": "프록시", 83 | "Proxy bypass rules": "프록시 우회 규칙", 84 | "Proxy rules": "프록시 규칙", 85 | "Quit": "종료", 86 | "Quit Zulip": "Zulip 을 종료합니다", 87 | "Quit when the window is closed": "윈도우가 닫히면 종료", 88 | "Redo": "다시실행", 89 | "Release Notes": "릴리즈 노트", 90 | "Reload": "새로고침", 91 | "Report an Issue": "문제 신고", 92 | "Save": "저장", 93 | "Select All": "모두 선택", 94 | "Services": "서비스들", 95 | "Settings": "설정", 96 | "Shortcuts": "바로 가기", 97 | "Show app icon in system tray": "시스템 트레이에 앱 아이콘 표시", 98 | "Show desktop notifications": "바탕 화면 알림 표시", 99 | "Show sidebar": "사이드 바 표시", 100 | "Spellchecker Languages": "맞춤법 검사기 언어", 101 | "Start app at login": "로그인시 앱 시작", 102 | "Switch to Next Organization": "다음 조직으로 전환", 103 | "Switch to Previous Organization": "이전 조직으로 전환", 104 | "These desktop app shortcuts extend the Zulip webapp's": "데스크톱 앱 바로 가기들은 Zulip 웹 앱을 확장합니다", 105 | "Tip": "팁", 106 | "Toggle DevTools for Active Tab": "DevTools for Active Tab 토글", 107 | "Toggle DevTools for Zulip App": "Zulip App 용 DevTools 토글", 108 | "Toggle Do Not Disturb": "방해 금지 전환", 109 | "Toggle Full Screen": "전체 화면 토글", 110 | "Toggle Sidebar": "사이드 바 전환", 111 | "Toggle Tray Icon": "트레이 아이콘 토글", 112 | "Tools": "도구들", 113 | "Undo": "되돌리기", 114 | "Unhide": "나타내기", 115 | "Upload": "올리기", 116 | "Use system proxy settings (requires restart)": "시스템 프록시 설정 사용 (다시 시작해야 함)", 117 | "View": "보기", 118 | "View Shortcuts": "바로가기 보기", 119 | "Window": "창", 120 | "Window Shortcuts": "창 바로 가기", 121 | "Yes": "네", 122 | "You can select a maximum of 3 languages for spellchecking.": "최대 3개 언어에 대한 맞춤법 검사기를 선택할수 있습니다.", 123 | "Zoom In": "확대", 124 | "Zoom Out": "축소", 125 | "keyboard shortcuts": "키보드 단축키" 126 | } 127 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/preference.ts: -------------------------------------------------------------------------------- 1 | import type {IpcRendererEvent} from "electron/renderer"; 2 | import process from "node:process"; 3 | 4 | import type {DndSettings} from "../../../../common/dnd-util.ts"; 5 | import {bundleUrl} from "../../../../common/paths.ts"; 6 | import type {NavigationItem} from "../../../../common/types.ts"; 7 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 8 | 9 | import {initConnectedOrgSection} from "./connected-org-section.ts"; 10 | import {initGeneralSection} from "./general-section.ts"; 11 | import Nav from "./nav.ts"; 12 | import {initNetworkSection} from "./network-section.ts"; 13 | import {initServersSection} from "./servers-section.ts"; 14 | import {initShortcutsSection} from "./shortcuts-section.ts"; 15 | 16 | export class PreferenceView { 17 | static async create(): Promise { 18 | return new PreferenceView( 19 | await ( 20 | await fetch(new URL("app/renderer/preference.html", bundleUrl)) 21 | ).text(), 22 | ); 23 | } 24 | 25 | readonly $view: HTMLElement; 26 | private readonly $shadow: ShadowRoot; 27 | private readonly $settingsContainer: Element; 28 | private readonly nav: Nav; 29 | private navigationItem: NavigationItem = "General"; 30 | 31 | private constructor(templateHtml: string) { 32 | this.$view = document.createElement("div"); 33 | this.$shadow = this.$view.attachShadow({mode: "open"}); 34 | this.$shadow.innerHTML = templateHtml; 35 | 36 | const $sidebarContainer = this.$shadow.querySelector("#sidebar")!; 37 | this.$settingsContainer = this.$shadow.querySelector( 38 | "#settings-container", 39 | )!; 40 | 41 | this.nav = new Nav({ 42 | $root: $sidebarContainer, 43 | onItemSelected: this.handleNavigation, 44 | }); 45 | 46 | ipcRenderer.on("toggle-sidebar", this.handleToggleSidebar); 47 | ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar); 48 | ipcRenderer.on("toggle-dnd", this.handleToggleDnd); 49 | 50 | this.handleNavigation(this.navigationItem); 51 | } 52 | 53 | handleNavigation = (navigationItem: NavigationItem): void => { 54 | this.navigationItem = navigationItem; 55 | this.nav.select(navigationItem); 56 | switch (navigationItem) { 57 | case "AddServer": { 58 | initServersSection({ 59 | $root: this.$settingsContainer, 60 | }); 61 | break; 62 | } 63 | 64 | case "General": { 65 | initGeneralSection({ 66 | $root: this.$settingsContainer, 67 | }); 68 | break; 69 | } 70 | 71 | case "Organizations": { 72 | initConnectedOrgSection({ 73 | $root: this.$settingsContainer, 74 | }); 75 | break; 76 | } 77 | 78 | case "Network": { 79 | initNetworkSection({ 80 | $root: this.$settingsContainer, 81 | }); 82 | break; 83 | } 84 | 85 | case "Shortcuts": { 86 | initShortcutsSection({ 87 | $root: this.$settingsContainer, 88 | }); 89 | break; 90 | } 91 | } 92 | 93 | location.hash = `#${navigationItem}`; 94 | }; 95 | 96 | handleToggleTray(state: boolean) { 97 | this.handleToggle("tray-option", state); 98 | } 99 | 100 | destroy(): void { 101 | ipcRenderer.off("toggle-sidebar", this.handleToggleSidebar); 102 | ipcRenderer.off("toggle-autohide-menubar", this.handleToggleMenubar); 103 | ipcRenderer.off("toggle-dnd", this.handleToggleDnd); 104 | } 105 | 106 | // Handle toggling and reflect changes in preference page 107 | private handleToggle(elementName: string, state = false): void { 108 | const inputSelector = `#${elementName} .action .switch input`; 109 | const input: HTMLInputElement = this.$shadow.querySelector(inputSelector)!; 110 | if (input) { 111 | input.checked = state; 112 | } 113 | } 114 | 115 | private readonly handleToggleSidebar = ( 116 | _event: IpcRendererEvent, 117 | state: boolean, 118 | ) => { 119 | this.handleToggle("sidebar-option", state); 120 | }; 121 | 122 | private readonly handleToggleMenubar = ( 123 | _event: IpcRendererEvent, 124 | state: boolean, 125 | ) => { 126 | this.handleToggle("menubar-option", state); 127 | }; 128 | 129 | private readonly handleToggleDnd = ( 130 | _event: IpcRendererEvent, 131 | _state: boolean, 132 | newSettings: Partial, 133 | ) => { 134 | this.handleToggle("show-notification-option", newSettings.showNotification); 135 | this.handleToggle("silent-option", newSettings.silent); 136 | 137 | if (process.platform === "win32") { 138 | this.handleToggle( 139 | "flash-taskbar-option", 140 | newSettings.flashTaskbarOnMessage, 141 | ); 142 | } 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /app/renderer/js/components/context-menu.ts: -------------------------------------------------------------------------------- 1 | import {type Event, clipboard} from "electron/common"; 2 | import type {WebContents} from "electron/main"; 3 | import type { 4 | ContextMenuParams, 5 | MenuItemConstructorOptions, 6 | } from "electron/renderer"; 7 | import process from "node:process"; 8 | 9 | import {BrowserWindow, Menu} from "@electron/remote"; 10 | 11 | import * as t from "../../../common/translation-util.ts"; 12 | 13 | export const contextMenu = ( 14 | webContents: WebContents, 15 | event: Event, 16 | properties: ContextMenuParams, 17 | ) => { 18 | const isText = properties.selectionText !== ""; 19 | const isLink = properties.linkURL !== ""; 20 | const linkUrl = isLink ? new URL(properties.linkURL) : undefined; 21 | 22 | const makeSuggestion = (suggestion: string) => ({ 23 | label: suggestion, 24 | visible: true, 25 | async click() { 26 | await webContents.insertText(suggestion); 27 | }, 28 | }); 29 | 30 | let menuTemplate: MenuItemConstructorOptions[] = [ 31 | { 32 | label: t.__("Add to Dictionary"), 33 | visible: 34 | properties.isEditable && isText && properties.misspelledWord.length > 0, 35 | click(_item) { 36 | webContents.session.addWordToSpellCheckerDictionary( 37 | properties.misspelledWord, 38 | ); 39 | }, 40 | }, 41 | { 42 | type: "separator", 43 | visible: 44 | properties.isEditable && isText && properties.misspelledWord.length > 0, 45 | }, 46 | { 47 | label: `${t.__("Look Up")} "${properties.selectionText}"`, 48 | visible: process.platform === "darwin" && isText, 49 | click(_item) { 50 | webContents.showDefinitionForSelection(); 51 | }, 52 | }, 53 | { 54 | type: "separator", 55 | visible: process.platform === "darwin" && isText, 56 | }, 57 | { 58 | label: t.__("Cut"), 59 | visible: isText, 60 | enabled: properties.isEditable, 61 | accelerator: "CommandOrControl+X", 62 | click(_item) { 63 | webContents.cut(); 64 | }, 65 | }, 66 | { 67 | label: t.__("Copy"), 68 | accelerator: "CommandOrControl+C", 69 | enabled: properties.editFlags.canCopy, 70 | click(_item) { 71 | webContents.copy(); 72 | }, 73 | }, 74 | { 75 | label: t.__("Paste"), // Bug: Paste replaces text 76 | accelerator: "CommandOrControl+V", 77 | enabled: properties.isEditable, 78 | click() { 79 | webContents.paste(); 80 | }, 81 | }, 82 | { 83 | type: "separator", 84 | }, 85 | { 86 | label: 87 | linkUrl?.protocol === "mailto:" 88 | ? t.__("Copy Email Address") 89 | : t.__("Copy Link"), 90 | visible: isLink, 91 | click(_item) { 92 | clipboard.write({ 93 | bookmark: properties.linkText, 94 | text: 95 | linkUrl?.protocol === "mailto:" 96 | ? linkUrl.pathname 97 | : properties.linkURL, 98 | }); 99 | }, 100 | }, 101 | { 102 | label: t.__("Copy Image"), 103 | visible: properties.mediaType === "image", 104 | click(_item) { 105 | webContents.copyImageAt(properties.x, properties.y); 106 | }, 107 | }, 108 | { 109 | label: t.__("Copy Image URL"), 110 | visible: properties.mediaType === "image", 111 | click(_item) { 112 | clipboard.write({ 113 | bookmark: properties.srcURL, 114 | text: properties.srcURL, 115 | }); 116 | }, 117 | }, 118 | ]; 119 | 120 | if (properties.misspelledWord) { 121 | if (properties.dictionarySuggestions.length > 0) { 122 | const suggestions: MenuItemConstructorOptions[] = 123 | properties.dictionarySuggestions.map((suggestion: string) => 124 | makeSuggestion(suggestion), 125 | ); 126 | menuTemplate = [...suggestions, ...menuTemplate]; 127 | } else { 128 | menuTemplate.unshift({ 129 | label: t.__("No Suggestion Found"), 130 | enabled: false, 131 | }); 132 | } 133 | } 134 | // Hide the invisible separators on Linux and Windows 135 | // Electron has a bug which ignores visible: false parameter for separator menu items. So we remove them here. 136 | // https://github.com/electron/electron/issues/5869 137 | // https://github.com/electron/electron/issues/6906 138 | 139 | const filteredMenuTemplate = menuTemplate.filter( 140 | (menuItem) => menuItem.visible ?? true, 141 | ); 142 | const menu = Menu.buildFromTemplate(filteredMenuTemplate); 143 | menu.popup({ 144 | window: BrowserWindow.fromWebContents(webContents) ?? undefined, 145 | frame: properties.frame ?? undefined, 146 | x: properties.x, 147 | y: properties.y, 148 | sourceType: properties.menuSourceType, 149 | }); 150 | }; 151 | -------------------------------------------------------------------------------- /public/translations/mn.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "Тухай", 3 | "Actual Size": "Багтаамж", 4 | "Add Organization": "Бүлэг нэмэх", 5 | "Add a Zulip organization": "Чат бүлэг нэмэх", 6 | "Add custom CSS": "Нэмэлт CSS нэмэх", 7 | "Add to Dictionary": "Үгийн санд нэмэх", 8 | "Advanced": "Нарийвчилсан", 9 | "Always start minimized": "Minimized байдлаар эхлэнэ", 10 | "App Updates": "App шинэчлэлт", 11 | "App language (requires restart)": "Хэл (Унтрааж асаах шаарлагатай)", 12 | "Appearance": "Харагдах байдал", 13 | "Are you sure you want to disconnect this organization?": "Та энэ бүлгээс гарахдаа итгэлтэй байна уу?", 14 | "Ask where to save files before downloading": "Файл хаана татагдахыг асуух", 15 | "Auto hide Menu bar": "Цэс автоматаар нуух", 16 | "Auto hide menu bar (Press Alt key to display)": "Цэс автоматаар нуух ( Alt товч даран харна уу)", 17 | "Back": "Буцах", 18 | "Cancel": "Цуцлах", 19 | "Change": "Өөрчлөа", 20 | "Change the language from System Preferences → Keyboard → Text → Spelling.": "Хэл солих бол System preferences → Keyboard → Text → Spelling.", 21 | "Check for Updates": "Шинэчлэлт шалгах", 22 | "Close": "Хаах", 23 | "Connect": "Холбогдох", 24 | "Connect to another organization": "Өөр бүлэгт Холбогдох", 25 | "Connected organizations": "Холбогдсон бүлгүүд", 26 | "Copy": "Хуулах", 27 | "Copy Image": "Зураг хуулах", 28 | "Copy Image URL": "Зургийн холбоос хуулах", 29 | "Copy Link": "Холбоос хуулах", 30 | "Copy Zulip URL": "Оффис чатын холбоос хуулах", 31 | "Create a new organization": "Шинэ бүлэг үүсгэх", 32 | "Cut": "Бүр мөсөн хуулах", 33 | "Default download location": "Үндсэн татах байршил", 34 | "Delete": "Устгах", 35 | "Desktop Notifications": "Desktop Мэдэгдэл", 36 | "Desktop Settings": "Desktop тохиргоо", 37 | "Disconnect": "Холболт салгах", 38 | "Download App Logs": "Download App Лог", 39 | "Edit": "Засах", 40 | "Edit Shortcuts": "Холбоос засах", 41 | "Enable auto updates": "Авто шинэчлэлт идэвхижүүлэх", 42 | "Enable error reporting (requires restart)": "Алдаа мэдэгдэгч идэвхижүүлэх (Унтрааж асаах шаарлагатай)", 43 | "Enable spellchecker (requires restart)": "Дүрэм шалгагч идэвхижүүлэх (Унтрааж асаах шаарлагатай)", 44 | "Factory Reset": "Бүх датаг цэвэрлэж дахин эхлүүлэх", 45 | "Factory Reset Data": "Бүх датаг цэвэрлэж дахин эхлүүлэх", 46 | "File": "файл", 47 | "Find accounts": "Хаяг хайх", 48 | "Find accounts by email": "Имэйлээр нь хайх", 49 | "Flash taskbar on new message": "Мэссэж мэдэгдэх", 50 | "Forward": "Дамжуулах", 51 | "Functionality": "Үйлдэлүүд", 52 | "General": "Үндсэн", 53 | "Get beta updates": "Бэта шинэчлэлт авах", 54 | "Hard Reload": "Дахин ачааллуулах", 55 | "Help": "Тусламж", 56 | "Help Center": "Тусламжийн хэсэн", 57 | "Hide": "Нуух", 58 | "Hide Others": "Бусдаас нуух", 59 | "History": "Ашиглалтийн түүх", 60 | "History Shortcuts": "Холбоосын түүх", 61 | "Keyboard Shortcuts": "Keyboard холбоос", 62 | "Log Out": "Гарах", 63 | "Log Out of Organization": "Бүлгээс гарах", 64 | "Look Up": "Харах", 65 | "Mute all sounds from Zulip": "Бүх дууг хаах", 66 | "Network": "Сүлжээ", 67 | "No Suggestion Found": "Санал болголт олдсонгүй", 68 | "Notification settings": "Мэдэгдэлийн тохиргоо", 69 | "OK": "OK", 70 | "On macOS, the OS spellchecker is used.": ".", 71 | "Organization URL": "Бүлгийн холбоос", 72 | "Organizations": "Бүлгүүд", 73 | "Paste": "Хуулж тавих", 74 | "Paste and Match Style": "Хуулж тавих", 75 | "Proxy": "Proxy", 76 | "Proxy bypass rules": "Proxy bypass дүрмүүд", 77 | "Proxy rules": "Proxy дүрмүүд", 78 | "Quit": "Хаах", 79 | "Quit Zulip": "Чатыг хаах", 80 | "Quit when the window is closed": "Цонх хаагдахад гарах", 81 | "Redo": "Дахин хийх", 82 | "Reload": "Дахин ачааллах", 83 | "Report an Issue": "Алдааг мэдэгдэх", 84 | "Save": "Хадгалах", 85 | "Select All": "Бүгдийн идэвхижүүлэх", 86 | "Services": "Үйлчилгээ", 87 | "Settings": "Тохиргоо", 88 | "Shortcuts": "Холбоос", 89 | "Show app icon in system tray": "Жижиг icon харуулах", 90 | "Show desktop notifications": "Мэдэгдэл харуулах", 91 | "Show sidebar": "Хажуугын цэсийг харуулах", 92 | "Spellchecker Languages": "Дүрэм шалгах хэлүүд", 93 | "Start app at login": "Нэвтрэхэд ачааллуулах", 94 | "Switch to Next Organization": "Дараагийн бүлэг", 95 | "Switch to Previous Organization": "Өмнөх бүлэг", 96 | "These desktop app shortcuts extend the Zulip webapp's": "Browser-оор холбогдох холбоос", 97 | "Tip": "зөвлөгөө", 98 | "Undo": "Үйлдэлээ буцаах", 99 | "Unhide": "Нуухаа болих", 100 | "Upload": "Файл хуулах", 101 | "Use system proxy settings (requires restart)": "Proxy систем ашиглах (Унтрааж асаах шаарлагатай)", 102 | "View": "Харах", 103 | "View Shortcuts": "Холбоос харах", 104 | "You can select a maximum of 3 languages for spellchecking.": "Хамгийн ихдээ 3 хэл дүрэм шалгахад ашиглана.", 105 | "Zoom In": "Сунгах", 106 | "Zoom Out": "Жижигрүүлэх", 107 | "keyboard shortcuts": "keyboard холбоос" 108 | } 109 | -------------------------------------------------------------------------------- /app/main/autoupdater.ts: -------------------------------------------------------------------------------- 1 | import {shell} from "electron/common"; 2 | import {app, dialog, session} from "electron/main"; 3 | import process from "node:process"; 4 | 5 | import log from "electron-log/main"; 6 | import { 7 | type UpdateDownloadedEvent, 8 | type UpdateInfo, 9 | autoUpdater, 10 | } from "electron-updater"; 11 | 12 | import * as ConfigUtil from "../common/config-util.ts"; 13 | import * as t from "../common/translation-util.ts"; 14 | 15 | import {linuxUpdateNotification} from "./linuxupdater.ts"; // Required only in case of linux 16 | 17 | let quitting = false; 18 | 19 | export function shouldQuitForUpdate(): boolean { 20 | return quitting; 21 | } 22 | 23 | export async function appUpdater(updateFromMenu = false): Promise { 24 | // Don't initiate auto-updates in development 25 | if (!app.isPackaged) { 26 | return; 27 | } 28 | 29 | if (process.platform === "linux" && !process.env.APPIMAGE) { 30 | const ses = session.fromPartition("persist:webviewsession"); 31 | await linuxUpdateNotification(ses); 32 | return; 33 | } 34 | 35 | let updateAvailable = false; 36 | 37 | // Log what's happening 38 | const updateLogger = log.create({logId: "updates"}); 39 | updateLogger.transports.file.fileName = "updates.log"; 40 | updateLogger.transports.file.level = "info"; 41 | autoUpdater.logger = updateLogger; 42 | 43 | // Handle auto updates for beta/pre releases 44 | const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false); 45 | 46 | autoUpdater.allowPrerelease = isBetaUpdate; 47 | 48 | const eventsListenerRemove = [ 49 | "update-available", 50 | "update-not-available", 51 | ] as const; 52 | autoUpdater.on("update-available", async (info: UpdateInfo) => { 53 | if (updateFromMenu) { 54 | updateAvailable = true; 55 | 56 | // This is to prevent removal of 'update-downloaded' and 'error' event listener. 57 | for (const event of eventsListenerRemove) { 58 | autoUpdater.removeAllListeners(event); 59 | } 60 | 61 | await dialog.showMessageBox({ 62 | message: t.__( 63 | "A new version {{{version}}} of Zulip Desktop is available.", 64 | {version: info.version}, 65 | ), 66 | detail: t.__( 67 | "The update will be downloaded in the background. You will be notified when it is ready to be installed.", 68 | ), 69 | }); 70 | } 71 | }); 72 | 73 | autoUpdater.on("update-not-available", async () => { 74 | if (updateFromMenu) { 75 | // Remove all autoUpdator listeners so that next time autoUpdator is manually called these 76 | // listeners don't trigger multiple times. 77 | autoUpdater.removeAllListeners(); 78 | 79 | await dialog.showMessageBox({ 80 | message: t.__("No updates available."), 81 | detail: t.__( 82 | "You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}", 83 | {version: app.getVersion()}, 84 | ), 85 | }); 86 | } 87 | }); 88 | 89 | autoUpdater.on("error", async (error: Error) => { 90 | if (updateFromMenu) { 91 | // Remove all autoUpdator listeners so that next time autoUpdator is manually called these 92 | // listeners don't trigger multiple times. 93 | autoUpdater.removeAllListeners(); 94 | 95 | const messageText = updateAvailable 96 | ? t.__("Unable to download the update.") 97 | : t.__("Unable to check for updates."); 98 | const link = "https://zulip.com/apps/"; 99 | const {response} = await dialog.showMessageBox({ 100 | type: "error", 101 | buttons: [t.__("Manual Download"), t.__("Cancel")], 102 | message: messageText, 103 | detail: t.__( 104 | "Error: {{{error}}}\n\nThe latest version of Zulip Desktop is available at:\n{{{link}}}\nCurrent version: {{{version}}}", 105 | {error: error.message, link, version: app.getVersion()}, 106 | ), 107 | }); 108 | if (response === 0) { 109 | await shell.openExternal(link); 110 | } 111 | } 112 | }); 113 | 114 | // Ask the user if update is available 115 | autoUpdater.on("update-downloaded", async (event: UpdateDownloadedEvent) => { 116 | // Ask user to update the app 117 | const {response} = await dialog.showMessageBox({ 118 | type: "question", 119 | buttons: [t.__("Install and Relaunch"), t.__("Install Later")], 120 | defaultId: 0, 121 | message: t.__("A new update {{{version}}} has been downloaded.", { 122 | version: event.version, 123 | }), 124 | detail: t.__( 125 | "It will be installed the next time you restart the application.", 126 | ), 127 | }); 128 | if (response === 0) { 129 | quitting = true; 130 | autoUpdater.quitAndInstall(); 131 | } 132 | }); 133 | // Init for updates 134 | await autoUpdater.checkForUpdates(); 135 | } 136 | -------------------------------------------------------------------------------- /public/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "A new update {{{version}}} has been downloaded.": "新しいアップデート {{{version}}} がダウンロードされました。", 3 | "About": "Zulipについて", 4 | "About Zulip": "Zulip について", 5 | "Actual Size": "サイズを元に戻す", 6 | "Add Organization": "組織を追加", 7 | "Add a Zulip organization": "新しいZulip組織を追加", 8 | "Add custom CSS": "カスタムCSSを追加", 9 | "Advanced": "その他", 10 | "Always start minimized": "常に最小化して起動", 11 | "App Updates": "アプリのアップデート", 12 | "App language (requires restart)": "アプリの言語設定 (再起動が必要です)", 13 | "Appearance": "外観", 14 | "Application Shortcuts": "アプリケーションのショートカット", 15 | "Are you sure you want to disconnect this organization?": "本当にこの組織から脱退しますか?", 16 | "Ask where to save files before downloading": "ダウンロード時にファイルの保存先を指定する", 17 | "Auto hide Menu bar": "メニューバーを自動的に隠す", 18 | "Auto hide menu bar (Press Alt key to display)": "メニューバーを自動的に隠す (Altキーを押すと表示)", 19 | "Back": "戻る", 20 | "Bounce dock on new private message": "新しいプライベートメッセージがあると Dock アイコンが跳ねる", 21 | "Cancel": "キャンセル", 22 | "Change": "変更", 23 | "Check for Updates": "アップデートを確認", 24 | "Close": "閉じる", 25 | "Connect": "接続", 26 | "Connect to another organization": "別の組織に接続", 27 | "Connected organizations": "接続済みの組織", 28 | "Copy": "コピー", 29 | "Copy Image": "画像をコピー", 30 | "Copy Image URL": "画像の URL をコピー", 31 | "Copy Link": "リンクをコピー", 32 | "Copy Zulip URL": "Zulip URL をコピー", 33 | "Create a new organization": "新しい組織を作成", 34 | "Cut": "切り取り", 35 | "Default download location": "デフォルトのダウンロードフォルダー", 36 | "Delete": "削除", 37 | "Desktop Notifications": "デスクトップ通知", 38 | "Desktop Settings": "デスクトップ設定", 39 | "Disconnect": "切断", 40 | "Do Not Disturb": "サイレントモード", 41 | "Download App Logs": "アプリログをダウンロード", 42 | "Edit": "編集", 43 | "Edit Shortcuts": "ショートカットを編集", 44 | "Emoji & Symbols": "絵文字と記号", 45 | "Enable auto updates": "自動更新を有効にする", 46 | "Enable error reporting (requires restart)": "エラー報告を有効にする (再起動が必要です)", 47 | "Enable spellchecker (requires restart)": "スペルチェックを有効にする (再起動が必要です)", 48 | "Factory Reset": "ファクトリーリセット", 49 | "File": "ファイル", 50 | "Find accounts": "アカウントを探す", 51 | "Find accounts by email": "メールでアカウントを探す", 52 | "Flash taskbar on new message": "新しいメッセージがあるとタスクバーを点滅させる", 53 | "Forward": "進む", 54 | "Functionality": "機能", 55 | "General": "全般", 56 | "Get beta updates": "ベータ版のアップデートを入手", 57 | "Go Back": "戻る", 58 | "Hard Reload": "ハードリロード", 59 | "Help": "ヘルプ", 60 | "Help Center": "ヘルプセンター", 61 | "Hide": "非表示", 62 | "History": "履歴", 63 | "History Shortcuts": "履歴ショートカット", 64 | "Keyboard Shortcuts": "キーボードショートカット", 65 | "Loading": "ロード中", 66 | "Log Out": "ログアウト", 67 | "Log Out of Organization": "組織からログアウトする", 68 | "Manual proxy configuration": "手動プロキシ設定", 69 | "Minimize": "最小化", 70 | "Mute all sounds from Zulip": "Zulip からのすべてのサウンドをミュート", 71 | "Network": "ネットワーク", 72 | "Network and Proxy Settings": "ネットワークとプロキシ", 73 | "No": "いいえ", 74 | "Notification settings": "通知設定", 75 | "OK": "OK", 76 | "OR": "または", 77 | "On macOS, the OS spellchecker is used.": "macOSでは、OSのスペルチェックが使用されます。", 78 | "Organization URL": "組織のURL", 79 | "Organizations": "組織", 80 | "Paste": "貼り付け", 81 | "Paste and Match Style": "スタイルを合わせて貼り付け", 82 | "Proxy": "プロキシ", 83 | "Proxy bypass rules": "プロキシバイパスルール", 84 | "Proxy rules": "プロキシルール", 85 | "Proxy settings saved.": "プロキシ設定を保存しました。", 86 | "Quit": "終了", 87 | "Quit Zulip": "Zulip を終了", 88 | "Quit when the window is closed": "ウインドウを閉じるときに自動的に退出", 89 | "Redo": "やり直す", 90 | "Release Notes": "リリースノート", 91 | "Reload": "リロード", 92 | "Report an Issue": "問題を報告する", 93 | "Reset App Settings": "アプリの設定をリセット", 94 | "Reset the application, thus deleting all the connected organizations and accounts.": "アプリケーションをリセットし、接続されたすべての組織とアカウントを削除します。", 95 | "Save": "保存", 96 | "Select All": "すべて選択", 97 | "Services": "サービス", 98 | "Settings": "設定", 99 | "Shortcuts": "ショートカット", 100 | "Show app icon in system tray": "システムトレイにアプリアイコンを表示する", 101 | "Show desktop notifications": "デスクトップ通知を表示する", 102 | "Show sidebar": "サイドバーを表示", 103 | "Spellchecker Languages": "スペルチェックの言語", 104 | "Start app at login": "ログイン時にアプリを起動する", 105 | "Switch to Next Organization": "次の組織に切り替える", 106 | "Switch to Previous Organization": "前の組織に切り替える", 107 | "These desktop app shortcuts extend the Zulip webapp's": "これらのデスクトップアプリのショートカットは Zulip Web アプリケーションのショートカットを拡張します", 108 | "Tip": "ヒント", 109 | "Toggle DevTools for Active Tab": "アクティブなタブの DevTools を切り替え", 110 | "Toggle DevTools for Zulip App": "Zulip App の DevTools を切り替え", 111 | "Toggle Do Not Disturb": "サイレントモードの切り替え", 112 | "Toggle Full Screen": "フルスクリーンの切り替え", 113 | "Toggle Sidebar": "サイドバーの切り替え", 114 | "Toggle Tray Icon": "トレイアイコンの切り替え", 115 | "Tools": "ツール", 116 | "Undo": "元に戻す", 117 | "Unhide": "表示", 118 | "Upload": "アップロード", 119 | "Use system proxy settings (requires restart)": "システムのプロキシ設定を使用する (再起動が必要です)", 120 | "View": "表示", 121 | "View Shortcuts": "ショートカットを表示", 122 | "Window": "ウインドウ", 123 | "Window Shortcuts": "ウィンドウショートカット", 124 | "Yes": "はい", 125 | "You can select a maximum of 3 languages for spellchecking.": "最大で3つの言語のスペルチェックを選択できます。", 126 | "Zoom In": "拡大", 127 | "Zoom Out": "縮小", 128 | "keyboard shortcuts": "キーボードショートカット" 129 | } 130 | -------------------------------------------------------------------------------- /xo.config.ts: -------------------------------------------------------------------------------- 1 | import type {FlatXoConfig} from "xo"; 2 | 3 | const restrictedMainImports = [ 4 | { 5 | name: "@sentry/electron/main", 6 | message: "Cannot use main-only APIs here.", 7 | }, 8 | { 9 | name: "electron/main", 10 | message: "Cannot use main-only APIs here.", 11 | allowTypeImports: true, // https://github.com/zulip/zulip-desktop/issues/915 12 | }, 13 | { 14 | name: "electron-log/main", 15 | message: "Cannot use main-only APIs here.", 16 | }, 17 | ]; 18 | 19 | const restrictedRendererImports = [ 20 | { 21 | name: "@sentry/electron/renderer", 22 | message: "Cannot use renderer-only APIs here.", 23 | }, 24 | { 25 | name: "electron/renderer", 26 | message: "Cannot use renderer-only APIs here.", 27 | }, 28 | { 29 | name: "electron-log/renderer", 30 | message: "Cannot use renderer-only APIs here.", 31 | }, 32 | ]; 33 | 34 | const xoConfig: FlatXoConfig = [ 35 | { 36 | prettier: true, 37 | space: true, 38 | settings: { 39 | "import-x/resolver": "typescript", 40 | n: { 41 | resolvePaths: [import.meta.dirname], 42 | typescriptExtensionMap: [], 43 | }, 44 | }, 45 | rules: { 46 | "@typescript-eslint/consistent-type-imports": [ 47 | "error", 48 | {disallowTypeAnnotations: false}, 49 | ], 50 | "@typescript-eslint/no-dynamic-delete": "off", 51 | "@typescript-eslint/no-restricted-types": "off", 52 | "@typescript-eslint/no-unused-vars": [ 53 | "error", 54 | {argsIgnorePattern: "^_", caughtErrors: "all"}, 55 | ], 56 | "@typescript-eslint/switch-exhaustiveness-check": [ 57 | "error", 58 | { 59 | considerDefaultExhaustiveForUnions: true, 60 | requireDefaultForNonUnion: true, 61 | }, 62 | ], 63 | "arrow-body-style": "error", 64 | "import-x/no-extraneous-dependencies": [ 65 | "error", 66 | {includeTypes: true, packageDir: import.meta.dirname}, 67 | ], 68 | "import-x/no-restricted-paths": [ 69 | "error", 70 | { 71 | zones: [ 72 | { 73 | target: "./app/common", 74 | from: "./app", 75 | except: ["./common"], 76 | }, 77 | { 78 | target: "./app/main", 79 | from: "./app", 80 | except: ["./common", "./main"], 81 | }, 82 | { 83 | target: "./app/renderer", 84 | from: "./app", 85 | except: ["./common", "./renderer", "./resources"], 86 | }, 87 | ], 88 | }, 89 | ], 90 | "import-x/order": [ 91 | "error", 92 | {alphabetize: {order: "asc"}, "newlines-between": "always"}, 93 | ], 94 | "import-x/unambiguous": "error", 95 | "no-restricted-imports": [ 96 | "error", 97 | { 98 | paths: [ 99 | { 100 | name: "@sentry/electron", 101 | message: 102 | "Use @sentry/electron/main, @sentry/electron/renderer, or @sentry/core.", 103 | }, 104 | { 105 | name: "electron", 106 | message: 107 | "Use electron/main, electron/renderer, or electron/common.", 108 | }, 109 | { 110 | name: "electron/main", 111 | importNames: ["ipcMain"], 112 | message: "Use typed-ipc-main.", 113 | }, 114 | { 115 | name: "electron/renderer", 116 | importNames: ["ipcRenderer"], 117 | message: "Use typed-ipc-renderer.", 118 | }, 119 | { 120 | name: "electron-log", 121 | message: "Use electron-log/main or electron-log/renderer.", 122 | }, 123 | ], 124 | }, 125 | ], 126 | "no-warning-comments": "off", 127 | "sort-imports": ["error", {ignoreDeclarationSort: true}], 128 | strict: "error", 129 | "unicorn/no-await-expression-member": "off", 130 | "unicorn/prefer-module": "off", 131 | "unicorn/prefer-top-level-await": "off", 132 | }, 133 | }, 134 | { 135 | files: ["**/*.d.ts"], 136 | rules: { 137 | "import-x/unambiguous": "off", 138 | }, 139 | }, 140 | { 141 | files: ["app/common/**"], 142 | rules: { 143 | "@typescript-eslint/no-restricted-imports": [ 144 | "error", 145 | {paths: [...restrictedMainImports, ...restrictedRendererImports]}, 146 | ], 147 | }, 148 | }, 149 | { 150 | files: ["app/main/**"], 151 | rules: { 152 | "@typescript-eslint/no-restricted-imports": [ 153 | "error", 154 | {paths: restrictedRendererImports}, 155 | ], 156 | }, 157 | }, 158 | { 159 | files: ["app/renderer/**"], 160 | rules: { 161 | "@typescript-eslint/no-restricted-imports": [ 162 | "error", 163 | {paths: restrictedMainImports}, 164 | ], 165 | }, 166 | }, 167 | ]; 168 | export default xoConfig; 169 | -------------------------------------------------------------------------------- /public/translations/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "A new update {{{version}}} has been downloaded.": "En ny uppdatering {{{version}}} har laddats ned.", 3 | "A new version {{{version}}} of Zulip Desktop is available.": "En ny version {{{version}}} av Zulip Desktop är tillgänglig.", 4 | "About": "Om", 5 | "About Zulip": "Om Zulip", 6 | "Actual Size": "Faktisk storlek", 7 | "Add Organization": "Lägg till organisation", 8 | "Add a Zulip organization": "Lägg till en Zulip-organisation", 9 | "Add custom CSS": "Lägg till egen CSS", 10 | "Advanced": "Avancerad", 11 | "All the connected organizations will appear here.": "Alla anslutna organisationer kommer att visas här.", 12 | "Always start minimized": "Starta alltid minimerad", 13 | "App Updates": "App Uppdateringar", 14 | "Appearance": "Utseende", 15 | "Application Shortcuts": "Programgenvägar", 16 | "Are you sure you want to disconnect this organization?": "Är du säker på att du vill koppla ner organisationen?", 17 | "Are you sure?": "Är du säker?", 18 | "Auto hide Menu bar": "Göm Menyraden automatiskt", 19 | "Auto hide menu bar (Press Alt key to display)": "Göm menyraden automatiskt (Tryck Alt-tangentetn för att visa)", 20 | "Back": "Tillbaka", 21 | "Bounce dock on new private message": "Animera dock för nytt privat meddelande", 22 | "CSS file": "CSS fil", 23 | "Cancel": "Avbryt", 24 | "Change": "Ändra", 25 | "Check for Updates": "Leta efter uppdateringar", 26 | "Close": "Stäng", 27 | "Connect": "Anslut", 28 | "Connect to another organization": "Anslut till en annan organisation", 29 | "Connected organizations": "Anlutna organisationer", 30 | "Copy": "Kopiera", 31 | "Copy Zulip URL": "Kopiera Zulip-URL", 32 | "Create a new organization": "Skapa en ny organisation", 33 | "Cut": "Klipp ut", 34 | "Default download location": "Förvald plats för nedladdningar", 35 | "Delete": "Radera", 36 | "Desktop Notifications": "Datornotiser", 37 | "Desktop Settings": "Desktop-inställningar", 38 | "Disconnect": "Koppla från", 39 | "Download App Logs": "Ladda ner App-loggar", 40 | "Edit": "Ändra", 41 | "Edit Shortcuts": "Redigera Genvägar", 42 | "Enable auto updates": "Aktivera automatiska uppdateringar", 43 | "Enable error reporting (requires restart)": "Aktivera felrapportering (kräver omstart)", 44 | "Enable spellchecker (requires restart)": "Aktivera stavningskontroll (kräver omstart)", 45 | "Factory Reset": "Fabriksåterställning", 46 | "File": "Fil", 47 | "Find accounts": "Sök konton", 48 | "Find accounts by email": "Sök konton med mejladress", 49 | "Flash taskbar on new message": "Animera aktivitetsfältet vid nytt meddelande", 50 | "Forward": "Framåt", 51 | "Functionality": "Fuktionallitet", 52 | "General": "Allmän", 53 | "Get beta updates": "Hämta beta-uppdaeringar", 54 | "Hard Reload": "Hård omstart", 55 | "Help": "Hjälp", 56 | "Help Center": "Hjälpcenter", 57 | "History": "Historia", 58 | "History Shortcuts": "Historiegenvägar", 59 | "Keyboard Shortcuts": "Tangentbortskommandon", 60 | "Log Out": "Logga Ut", 61 | "Log Out of Organization": "Logga Ut alla Organisationer", 62 | "Manual proxy configuration": "Manuell proxy-konfigurering", 63 | "Minimize": "Minimera", 64 | "Mute all sounds from Zulip": "Tysta alla ljud från Zulip", 65 | "Network": "Nätverk", 66 | "OR": "ELLER", 67 | "Organization URL": "Organisations-URL", 68 | "Organizations": "Organisationer", 69 | "Paste": "Klistra in", 70 | "Paste and Match Style": "Klistra in och matcha stil", 71 | "Proxy": "Proxy", 72 | "Proxy bypass rules": "Proxy by-pass-regler", 73 | "Proxy rules": "Proxy-regler", 74 | "Quit": "Avsluta", 75 | "Quit Zulip": "Avsluta Zulip", 76 | "Quit when the window is closed": "Avsluta när föstret stängs", 77 | "Redo": "Gör om", 78 | "Release Notes": "Release Notes", 79 | "Reload": "Ladda om", 80 | "Report an Issue": "Rapportera ett problem", 81 | "Reset App Settings": "Återställ App-inställningar", 82 | "Save": "Spara", 83 | "Select All": "Markera alla", 84 | "Settings": "Inställningar", 85 | "Shortcuts": "Genvägar", 86 | "Show app icon in system tray": "Visa app-ikonen i systemfältet", 87 | "Show desktop notifications": "Visa skrivbordsnotiser", 88 | "Show sidebar": "Visa sidopanel", 89 | "Start app at login": "Starta app vi inloggning", 90 | "Switch to Next Organization": "Växla till Nästa Organisation", 91 | "Switch to Previous Organization": "Växla till Tigare Organisation", 92 | "These desktop app shortcuts extend the Zulip webapp's": "Dessa genvägar skrivbords-appen utökar Zulips webbapps", 93 | "Tip": "Tips", 94 | "Toggle DevTools for Active Tab": "Bläddra mellan Utvecklarverktyg för Aktiv flik", 95 | "Toggle DevTools for Zulip App": "Toggla Utvecklarverktyg för Zulip-app", 96 | "Toggle Do Not Disturb": "Växla Stör ej-läge", 97 | "Toggle Full Screen": "Växla Helskärm", 98 | "Toggle Sidebar": "Växla sidopanel", 99 | "Toggle Tray Icon": "Växla Fältikon", 100 | "Tools": "Verktyg", 101 | "Undo": "Ångra", 102 | "Upload": "Ladda upp", 103 | "Use system proxy settings (requires restart)": "Använd systemets proxy-inställningar (omstart krävs)", 104 | "View": "Vy", 105 | "View Shortcuts": "Visa Genvägar", 106 | "Window": "Fönster", 107 | "Window Shortcuts": "Fönstergenvägar", 108 | "Zoom In": "Zooma in", 109 | "Zoom Out": "Zooma Ut", 110 | "keyboard shortcuts": "Tangentbordsgenvägar" 111 | } 112 | -------------------------------------------------------------------------------- /public/translations/ml.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "സുലിപ്പിനെക്കുറിച്ച്", 3 | "Actual Size": "യഥാർത്ഥ വലുപ്പം", 4 | "Add Organization": "ഓർഗനൈസേഷൻ ചേർക്കുക", 5 | "Add a Zulip organization": "ഒരു സുലിപ്പ് ഓർഗനൈസേഷൻ ചേർക്കുക", 6 | "Add custom CSS": "ഇഷ്‌ടാനുസൃത CSS ചേർക്കുക", 7 | "Advanced": "വിപുലമായത്", 8 | "Always start minimized": "എല്ലായ്പ്പോഴും ചെറുതാക്കാൻ ആരംഭിക്കുക", 9 | "App Updates": "അപ്ലിക്കേഷൻ അപ്‌ഡേറ്റുകൾ", 10 | "Appearance": "രൂപം", 11 | "Application Shortcuts": "അപ്ലിക്കേഷൻ കുറുക്കുവഴികൾ", 12 | "Are you sure you want to disconnect this organization?": "ഈ ഓർഗനൈസേഷൻ വിച്ഛേദിക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ?", 13 | "Auto hide Menu bar": "യാന്ത്രികമായി മറയ്‌ക്കുക മെനു ബാർ", 14 | "Auto hide menu bar (Press Alt key to display)": "യാന്ത്രികമായി മറയ്‌ക്കുക മെനു ബാർ (പ്രദർശിപ്പിക്കുന്നതിന് Alt കീ അമർത്തുക)", 15 | "Back": "തിരികെ", 16 | "Bounce dock on new private message": "പുതിയ സ്വകാര്യ സന്ദേശത്തിൽ ഡോക്ക് ബൗൺസ് ചെയ്യുക", 17 | "Cancel": "റദ്ദാക്കുക", 18 | "Change": "മാറ്റുക", 19 | "Check for Updates": "അപ്‌ഡേറ്റുകൾക്കായി പരിശോധിക്കുക", 20 | "Close": "അടയ്‌ക്കുക", 21 | "Connect": "ബന്ധിപ്പിക്കുക", 22 | "Connect to another organization": "മറ്റൊരു ഓർഗനൈസേഷനിലേക്ക് കണക്റ്റുചെയ്യുക", 23 | "Connected organizations": "ബന്ധിപ്പിച്ച ഓർഗനൈസേഷനുകൾ", 24 | "Copy": "പകർത്തുക", 25 | "Copy Zulip URL": "Zulip URL പകർത്തുക", 26 | "Create a new organization": "ഒരു പുതിയ ഓർഗനൈസേഷൻ സൃഷ്ടിക്കുക", 27 | "Cut": "മുറിക്കുക", 28 | "Default download location": "സ്ഥിരസ്ഥിതി ഡ download ൺ‌ലോഡ് സ്ഥാനം", 29 | "Delete": "ഇല്ലാതാക്കുക", 30 | "Desktop Notifications": "ഡെസ്ക്ടോപ്പ് അറിയിപ്പുകൾ", 31 | "Desktop Settings": "ഡെസ്ക്ടോപ്പ് ക്രമീകരണങ്ങൾ", 32 | "Disconnect": "വിച്ഛേദിക്കുക", 33 | "Download App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ ഡൗൺലോഡുചെയ്യുക", 34 | "Edit": "എഡിറ്റുചെയ്യുക", 35 | "Edit Shortcuts": "കുറുക്കുവഴികൾ എഡിറ്റുചെയ്യുക", 36 | "Enable auto updates": "യാന്ത്രിക അപ്‌ഡേറ്റുകൾ പ്രവർത്തനക്ഷമമാക്കുക", 37 | "Enable error reporting (requires restart)": "പിശക് റിപ്പോർട്ടിംഗ് പ്രാപ്തമാക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)", 38 | "Enable spellchecker (requires restart)": "അക്ഷരത്തെറ്റ് പരിശോധന പ്രാപ്തമാക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)", 39 | "Factory Reset": "ഫാക്ടറി പുന .സജ്ജമാക്കുക", 40 | "File": "ഫയൽ", 41 | "Find accounts": "അക്കൗണ്ടുകൾ കണ്ടെത്തുക", 42 | "Find accounts by email": "ഇമെയിൽ വഴി അക്കൗണ്ടുകൾ കണ്ടെത്തുക", 43 | "Flash taskbar on new message": "പുതിയ സന്ദേശത്തിൽ ഫ്ലാഷ് ടാസ്‌ക്ബാർ", 44 | "Forward": "ഫോർവേഡ് ചെയ്യുക", 45 | "Functionality": "പ്രവർത്തനം", 46 | "General": "ജനറൽ", 47 | "Get beta updates": "ബീറ്റ അപ്‌ഡേറ്റുകൾ നേടുക", 48 | "Hard Reload": "ഹാർഡ് റീലോഡ്", 49 | "Help": "സഹായിക്കൂ", 50 | "Help Center": "സഹായകേന്ദ്രം", 51 | "History": "ചരിത്രം", 52 | "History Shortcuts": "ചരിത്രം കുറുക്കുവഴികൾ", 53 | "Keyboard Shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ", 54 | "Log Out": "ലോഗ് .ട്ട് ചെയ്യുക", 55 | "Log Out of Organization": "ഓർഗനൈസേഷനിൽ നിന്ന് പുറത്തുകടക്കുക", 56 | "Manual proxy configuration": "സ്വമേധയാലുള്ള പ്രോക്സി കോൺഫിഗറേഷൻ", 57 | "Minimize": "ചെറുതാക്കുക", 58 | "Mute all sounds from Zulip": "സുലിപ്പിൽ നിന്നുള്ള എല്ലാ ശബ്ദങ്ങളും നിശബ്ദമാക്കുക", 59 | "Network": "നെറ്റ്‌വർക്ക്", 60 | "No": "ഇല്ല", 61 | "OR": "അഥവാ", 62 | "Organization URL": "ഓർ‌ഗനൈസേഷൻ‌ URL", 63 | "Organizations": "ഓർഗനൈസേഷനുകൾ", 64 | "Paste": "പേസ്റ്റ്", 65 | "Paste and Match Style": "ഒട്ടിച്ച് പൊരുത്തപ്പെടുന്ന ശൈലി", 66 | "Proxy": "പ്രോക്സി", 67 | "Proxy bypass rules": "പ്രോക്സി ബൈപാസ് നിയമങ്ങൾ", 68 | "Proxy rules": "പ്രോക്സി നിയമങ്ങൾ", 69 | "Quit": "ഉപേക്ഷിക്കുക", 70 | "Quit Zulip": "സുലിപ്പ് ഉപേക്ഷിക്കുക", 71 | "Redo": "വീണ്ടും ചെയ്യുക", 72 | "Release Notes": "പ്രകാശന കുറിപ്പുകൾ", 73 | "Reload": "വീണ്ടും ലോഡുചെയ്യുക", 74 | "Report an Issue": "ഒരു പ്രശ്നം റിപ്പോർട്ട് ചെയ്യുക", 75 | "Save": "രക്ഷിക്കും", 76 | "Select All": "എല്ലാം തിരഞ്ഞെടുക്കുക", 77 | "Settings": "ക്രമീകരണങ്ങൾ", 78 | "Shortcuts": "കുറുക്കുവഴികൾ", 79 | "Show app icon in system tray": "സിസ്റ്റം ട്രേയിൽ അപ്ലിക്കേഷൻ ഐക്കൺ കാണിക്കുക", 80 | "Show desktop notifications": "ഡെസ്ക്ടോപ്പ് അറിയിപ്പുകൾ കാണിക്കുക", 81 | "Show sidebar": "സൈഡ്‌ബാർ കാണിക്കുക", 82 | "Start app at login": "ലോഗിൻ ചെയ്യുമ്പോൾ അപ്ലിക്കേഷൻ ആരംഭിക്കുക", 83 | "Switch to Next Organization": "അടുത്ത ഓർഗനൈസേഷനിലേക്ക് മാറുക", 84 | "Switch to Previous Organization": "മുമ്പത്തെ ഓർഗനൈസേഷനിലേക്ക് മാറുക", 85 | "These desktop app shortcuts extend the Zulip webapp's": "ഈ ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ കുറുക്കുവഴികൾ സുലിപ് വെബ്‌അപ്പിനെ വിപുലീകരിക്കുന്നു", 86 | "Tip": "നുറുങ്ങ്", 87 | "Toggle DevTools for Active Tab": "സജീവ ടാബിനായി DevTools ടോഗിൾ ചെയ്യുക", 88 | "Toggle DevTools for Zulip App": "Zulip അപ്ലിക്കേഷനായി DevTools ടോഗിൾ ചെയ്യുക", 89 | "Toggle Do Not Disturb": "ശല്യപ്പെടുത്തരുത് ടോഗിൾ ചെയ്യുക", 90 | "Toggle Full Screen": "പൂർണ്ണ സ്‌ക്രീൻ ടോഗിൾ ചെയ്യുക", 91 | "Toggle Sidebar": "സൈഡ്‌ബാർ ടോഗിൾ ചെയ്യുക", 92 | "Toggle Tray Icon": "ട്രേ ഐക്കൺ ടോഗിൾ ചെയ്യുക", 93 | "Tools": "ഉപകരണങ്ങൾ", 94 | "Undo": "പഴയപടിയാക്കുക", 95 | "Upload": "അപ്‌ലോഡുചെയ്യുക", 96 | "Use system proxy settings (requires restart)": "സിസ്റ്റം പ്രോക്സി ക്രമീകരണങ്ങൾ ഉപയോഗിക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)", 97 | "View": "കാണുക", 98 | "View Shortcuts": "കുറുക്കുവഴികൾ കാണുക", 99 | "Window": "ജാലകം", 100 | "Window Shortcuts": "വിൻഡോ കുറുക്കുവഴികൾ", 101 | "Yes": "ശരി", 102 | "Zoom In": "വലുതാക്കുക", 103 | "Zoom Out": "സൂം .ട്ട് ചെയ്യുക", 104 | "keyboard shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ" 105 | } 106 | -------------------------------------------------------------------------------- /public/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "About Zulip": "Относно Zulip", 3 | "Actual Size": "Действителен размер", 4 | "Add Organization": "Добавяне на организация", 5 | "Add a Zulip organization": "Добавете организация Zulip", 6 | "Add custom CSS": "Добавете персонализиран CSS", 7 | "Advanced": "напреднал", 8 | "All the connected organizations will appear here.": "Всички свързани организации ще се появят тук.", 9 | "Always start minimized": "Винаги започвайте да минимизирате", 10 | "App Updates": "Актуализации на приложения", 11 | "Appearance": "Външен вид", 12 | "Application Shortcuts": "Клавишни комбинации за приложения", 13 | "Are you sure you want to disconnect this organization?": "Наистина ли искате да прекъснете връзката с тази организация?", 14 | "Are you sure?": "Сигурни ли сте?", 15 | "Auto hide Menu bar": "Автоматично скриване на лентата с менюта", 16 | "Auto hide menu bar (Press Alt key to display)": "Автоматично скриване на лентата с менюта (натиснете клавиша Alt за показване)", 17 | "Back": "обратно", 18 | "Bounce dock on new private message": "Прескочи док в новото лично съобщение", 19 | "Cancel": "Откажи", 20 | "Change": "промяна", 21 | "Check for Updates": "Провери за обновления", 22 | "Close": "Близо", 23 | "Connect": "Свържете", 24 | "Connect to another organization": "Свържете се с друга организация", 25 | "Connected organizations": "Свързани организации", 26 | "Copy": "копие", 27 | "Copy Zulip URL": "Копирайте URL адреса на Zulip", 28 | "Create a new organization": "Създайте нова организация", 29 | "Cut": "Разрез", 30 | "Default download location": "Място на изтегляне по подразбиране", 31 | "Delete": "Изтрий", 32 | "Desktop Notifications": "Известия за работния плот", 33 | "Desktop Settings": "Настройки на работния плот", 34 | "Disconnect": "Прекъсване на връзката", 35 | "Download App Logs": "Изтеглете регистрационни файлове на приложенията", 36 | "Edit": "редактиране", 37 | "Edit Shortcuts": "Редактиране на преки пътища", 38 | "Enable auto updates": "Активиране на автоматичните актуализации", 39 | "Enable error reporting (requires restart)": "Активиране на отчитането за грешки (изисква се рестартиране)", 40 | "Enable spellchecker (requires restart)": "Активиране на проверката на правописа (изисква се рестартиране)", 41 | "Factory Reset": "Фабрично нулиране", 42 | "File": "досие", 43 | "Find accounts": "Намерете профили", 44 | "Find accounts by email": "Намерете профили по имейл", 45 | "Flash taskbar on new message": "Flash лентата на задачите в новото съобщение", 46 | "Forward": "напред", 47 | "Functionality": "Функционалност", 48 | "General": "Общ", 49 | "Get beta updates": "Изтеглете бета актуализации", 50 | "Help": "Помогне", 51 | "Help Center": "Помощен център", 52 | "History": "история", 53 | "History Shortcuts": "Преки пътища в историята", 54 | "Keyboard Shortcuts": "Комбинация от клавиши", 55 | "Log Out": "Излез от профила си", 56 | "Log Out of Organization": "Излезте от организацията", 57 | "Manual proxy configuration": "Ръчна конфигурация на прокси", 58 | "Minimize": "Минимизиране", 59 | "Mute all sounds from Zulip": "Заглуши всички звуци от Zulip", 60 | "Network": "мрежа", 61 | "Notification settings": "Настройки на известията", 62 | "OK": "OK", 63 | "OR": "ИЛИ", 64 | "Organization URL": "URL адрес на организацията", 65 | "Organizations": "организации", 66 | "Paste": "паста", 67 | "Paste and Match Style": "Поставяне и стил на съвпадение", 68 | "Proxy": "пълномощник", 69 | "Proxy bypass rules": "Правила за заобикаляне на прокси", 70 | "Proxy rules": "Прокси правила", 71 | "Quit": "напускам", 72 | "Quit Zulip": "Прекрати Zulip", 73 | "Redo": "ремонтирам", 74 | "Release Notes": "Бележки към изданието", 75 | "Reload": "Презареди", 76 | "Report an Issue": "Подаване на сигнал за проблем", 77 | "Save": "Запази", 78 | "Select All": "Избери всички", 79 | "Settings": "Настройки", 80 | "Show app icon in system tray": "Показване на иконата на приложението в системната област", 81 | "Show desktop notifications": "Показване на известията на работния плот", 82 | "Show sidebar": "Показване на страничната лента", 83 | "Start app at login": "Стартирайте приложението при влизане", 84 | "Switch to Next Organization": "Превключване към следваща организация", 85 | "Switch to Previous Organization": "Превключване към предишна организация", 86 | "These desktop app shortcuts extend the Zulip webapp's": "Тези клавишни комбинации за настолни приложения разширяват webapp на Zulip", 87 | "Tip": "Бакшиш", 88 | "Toggle DevTools for Active Tab": "Превключете DevTools за Active Tab", 89 | "Toggle DevTools for Zulip App": "Превключете DevTools за Zulip App", 90 | "Toggle Do Not Disturb": "Превключване Не безпокойте", 91 | "Toggle Full Screen": "Превключване на цял екран", 92 | "Toggle Sidebar": "Превключване на страничната лента", 93 | "Toggle Tray Icon": "Превключете иконата на тава", 94 | "Tools": "Инструменти", 95 | "Undo": "премахвам", 96 | "Upload": "Качи", 97 | "Use system proxy settings (requires restart)": "Използване на системните прокси настройки (изисква рестартиране)", 98 | "View": "изглед", 99 | "View Shortcuts": "Преглед на преки пътища", 100 | "Window": "прозорец", 101 | "Window Shortcuts": "Клавишни комбинации", 102 | "Zoom In": "Увеличавам", 103 | "Zoom Out": "Отдалечавам", 104 | "keyboard shortcuts": "комбинация от клавиши" 105 | } 106 | -------------------------------------------------------------------------------- /app/main/handle-external-link.ts: -------------------------------------------------------------------------------- 1 | import {type Event, shell} from "electron/common"; 2 | import { 3 | type HandlerDetails, 4 | Notification, 5 | type SaveDialogOptions, 6 | type WebContents, 7 | app, 8 | } from "electron/main"; 9 | import fs from "node:fs"; 10 | import path from "node:path"; 11 | 12 | import * as ConfigUtil from "../common/config-util.ts"; 13 | import * as LinkUtil from "../common/link-util.ts"; 14 | import * as t from "../common/translation-util.ts"; 15 | 16 | import {send} from "./typed-ipc-main.ts"; 17 | 18 | function isUploadsUrl(server: string, url: URL): boolean { 19 | return url.origin === server && url.pathname.startsWith("/user_uploads/"); 20 | } 21 | 22 | function downloadFile({ 23 | contents, 24 | url, 25 | downloadPath, 26 | completed, 27 | failed, 28 | }: { 29 | contents: WebContents; 30 | url: string; 31 | downloadPath: string; 32 | completed(filePath: string, fileName: string): Promise; 33 | failed(state: string): void; 34 | }) { 35 | contents.downloadURL(url); 36 | contents.session.once("will-download", async (_event, item) => { 37 | if (ConfigUtil.getConfigItem("promptDownload", false)) { 38 | const showDialogOptions: SaveDialogOptions = { 39 | defaultPath: path.join(downloadPath, item.getFilename()), 40 | }; 41 | item.setSaveDialogOptions(showDialogOptions); 42 | } else { 43 | const getTimeStamp = (): number => { 44 | const date = new Date(); 45 | return date.getTime(); 46 | }; 47 | 48 | const formatFile = (filePath: string): string => { 49 | const fileExtension = path.extname(filePath); 50 | const baseName = path.basename(filePath, fileExtension); 51 | return `${baseName}-${getTimeStamp()}${fileExtension}`; 52 | }; 53 | 54 | const filePath = path.join(downloadPath, item.getFilename()); 55 | 56 | // Update the name and path of the file if it already exists 57 | const updatedFilePath = path.join(downloadPath, formatFile(filePath)); 58 | const setFilePath: string = fs.existsSync(filePath) 59 | ? updatedFilePath 60 | : filePath; 61 | item.setSavePath(setFilePath); 62 | } 63 | 64 | const updatedListener = (_event: Event, state: string): void => { 65 | switch (state) { 66 | case "interrupted": { 67 | // Can interrupted to due to network error, cancel download then 68 | console.log( 69 | "Download interrupted, cancelling and fallback to dialog download.", 70 | ); 71 | item.cancel(); 72 | break; 73 | } 74 | 75 | case "progressing": { 76 | if (item.isPaused()) { 77 | item.cancel(); 78 | } 79 | 80 | // This event can also be used to show progress in percentage in future. 81 | break; 82 | } 83 | 84 | default: { 85 | console.info("Unknown updated state of download item"); 86 | } 87 | } 88 | }; 89 | 90 | item.on("updated", updatedListener); 91 | item.once("done", async (_event, state) => { 92 | if (state === "completed") { 93 | await completed(item.getSavePath(), path.basename(item.getSavePath())); 94 | } else { 95 | console.log("Download failed state:", state); 96 | failed(state); 97 | } 98 | 99 | // To stop item for listening to updated events of this file 100 | item.removeListener("updated", updatedListener); 101 | }); 102 | }); 103 | } 104 | 105 | export default function handleExternalLink( 106 | contents: WebContents, 107 | details: HandlerDetails, 108 | mainContents: WebContents, 109 | ): void { 110 | let url: URL; 111 | try { 112 | url = new URL(details.url); 113 | } catch { 114 | return; 115 | } 116 | 117 | const downloadPath = ConfigUtil.getConfigItem( 118 | "downloadsPath", 119 | `${app.getPath("downloads")}`, 120 | ); 121 | 122 | if (isUploadsUrl(new URL(contents.getURL()).origin, url)) { 123 | downloadFile({ 124 | contents, 125 | url: url.href, 126 | downloadPath, 127 | async completed(filePath: string, fileName: string) { 128 | const downloadNotification = new Notification({ 129 | title: t.__("Download Complete"), 130 | body: t.__("Click to show {{{fileName}}} in folder", {fileName}), 131 | silent: true, // We'll play our own sound - ding.ogg 132 | }); 133 | downloadNotification.on("click", () => { 134 | // Reveal file in download folder 135 | shell.showItemInFolder(filePath); 136 | }); 137 | downloadNotification.show(); 138 | 139 | // Play sound to indicate download complete 140 | if (!ConfigUtil.getConfigItem("silent", false)) { 141 | send(mainContents, "play-ding-sound"); 142 | } 143 | }, 144 | failed(state: string) { 145 | // Automatic download failed, so show save dialog prompt and download 146 | // through webview 147 | // Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save 148 | // prompts right after each other) 149 | // Check that the download is not cancelled by user 150 | if (state !== "cancelled") { 151 | if (ConfigUtil.getConfigItem("promptDownload", false)) { 152 | new Notification({ 153 | title: t.__("Download Complete"), 154 | body: t.__("Download failed"), 155 | }).show(); 156 | } else { 157 | contents.downloadURL(url.href); 158 | } 159 | } 160 | }, 161 | }); 162 | } else { 163 | (async () => LinkUtil.openBrowser(url))(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /public/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "About": "Over", 3 | "About Zulip": "Over Zulip", 4 | "Actual Size": "Ware grootte", 5 | "Add Organization": "Voeg organisatie toe", 6 | "Add a Zulip organization": "Voeg een Zulip-organisatie toe", 7 | "Add custom CSS": "Voeg aangepaste CSS toe", 8 | "Advanced": "gevorderd", 9 | "All the connected organizations will appear here.": "Alle verbonden organisaties verschijnen hier.", 10 | "Always start minimized": "Begin altijd geminimaliseerd", 11 | "App Updates": "App-updates", 12 | "Appearance": "Verschijning", 13 | "Application Shortcuts": "Applicatiesnelkoppelingen", 14 | "Are you sure you want to disconnect this organization?": "Weet je zeker dat je deze organisatie wilt ontkoppelen?", 15 | "Auto hide Menu bar": "Menubalk automatisch verbergen", 16 | "Auto hide menu bar (Press Alt key to display)": "Menubalk automatisch verbergen (druk op de Alt-toets om weer te geven)", 17 | "Back": "Terug", 18 | "Bounce dock on new private message": "Bounce dock op nieuw privébericht", 19 | "Cancel": "Annuleren", 20 | "Change": "Verandering", 21 | "Check for Updates": "Controleer op updates", 22 | "Close": "Dichtbij", 23 | "Connect": "Aansluiten", 24 | "Connect to another organization": "Maak verbinding met een andere organisatie", 25 | "Connected organizations": "Verbonden organisaties", 26 | "Copy": "Kopiëren", 27 | "Copy Zulip URL": "Kopieer Zulip-URL", 28 | "Create a new organization": "Maak een nieuwe organisatie", 29 | "Cut": "Besnoeiing", 30 | "Default download location": "Standaard downloadlocatie", 31 | "Delete": "Verwijder", 32 | "Desktop Notifications": "Bureaublad notificaties", 33 | "Desktop Settings": "Desktop-instellingen", 34 | "Disconnect": "Loskoppelen", 35 | "Download App Logs": "Applogs downloaden", 36 | "Edit": "Bewerk", 37 | "Edit Shortcuts": "Bewerk snelkoppelingen", 38 | "Emoji & Symbols": "Emoji's & Symbolen", 39 | "Enable auto updates": "Schakel automatische updates in", 40 | "Enable error reporting (requires restart)": "Foutrapportage inschakelen (opnieuw opstarten vereist)", 41 | "Enable spellchecker (requires restart)": "Spellingcontrole inschakelen (opnieuw opstarten vereist)", 42 | "Enter Full Screen": "Volledig scherm gebruiken", 43 | "Factory Reset": "Fabrieksinstellingen", 44 | "File": "het dossier", 45 | "Find accounts": "Vind accounts", 46 | "Find accounts by email": "Vind accounts per e-mail", 47 | "Flash taskbar on new message": "Flash-taakbalk voor nieuw bericht", 48 | "Forward": "Vooruit", 49 | "Functionality": "functionaliteit", 50 | "General": "Algemeen", 51 | "Get beta updates": "Ontvang bèta-updates", 52 | "Hard Reload": "Harde herladen", 53 | "Help": "Helpen", 54 | "Help Center": "Helpcentrum", 55 | "Hide Zulip": "Zulip verbergen", 56 | "History": "Geschiedenis", 57 | "History Shortcuts": "Geschiedenis Sneltoetsen", 58 | "Keyboard Shortcuts": "Toetsenbord sneltoetsen", 59 | "Log Out": "Uitloggen", 60 | "Log Out of Organization": "Uitloggen van organisatie", 61 | "Manual proxy configuration": "Handmatige proxyconfiguratie", 62 | "Minimize": "verkleinen", 63 | "Mute all sounds from Zulip": "Demp alle geluiden van Zulip", 64 | "Network": "Netwerk", 65 | "Network and Proxy Settings": "Netwerk- en Proxyinstellingen", 66 | "No": "Nee", 67 | "OK": "Oké", 68 | "OR": "OF", 69 | "Organization URL": "Organisatie-URL", 70 | "Organizations": "organisaties", 71 | "Paste": "Pasta", 72 | "Paste and Match Style": "Plak en match stijl", 73 | "Proxy": "volmacht", 74 | "Proxy bypass rules": "Proxy-bypassregels", 75 | "Proxy rules": "Proxy-regels", 76 | "Quit": "ophouden", 77 | "Quit Zulip": "Sluit Zulip", 78 | "Redo": "Opnieuw doen", 79 | "Release Notes": "Releaseopmerkingen", 80 | "Reload": "vernieuwen", 81 | "Report an Issue": "Een probleem melden", 82 | "Reset App Settings": "Instellingen resetten", 83 | "Reset the application, thus deleting all the connected organizations and accounts.": "De applicatie resetten en daarmee alle verbonden organisaties en accounts verwijderen.", 84 | "Save": "Opslaan", 85 | "Select All": "Selecteer alles", 86 | "Settings": "instellingen", 87 | "Shortcuts": "shortcuts", 88 | "Show app icon in system tray": "App-pictogram weergeven in systeemvak", 89 | "Show desktop notifications": "Toon bureaubladmeldingen", 90 | "Show sidebar": "Toon zijbalk", 91 | "Start app at login": "Start de app bij inloggen", 92 | "Switch to Next Organization": "Schakel over naar volgende organisatie", 93 | "Switch to Previous Organization": "Schakel over naar vorige organisatie", 94 | "These desktop app shortcuts extend the Zulip webapp's": "Deze sneltoetsen voor bureaubladapp breiden de Zulip-webapp's uit", 95 | "Tip": "Tip", 96 | "Toggle DevTools for Active Tab": "DevTools voor actieve tabblad omschakelen", 97 | "Toggle DevTools for Zulip App": "DevTools voor Zulip-app omschakelen", 98 | "Toggle Do Not Disturb": "Schakel Niet storen in", 99 | "Toggle Full Screen": "Volledig scherm activeren", 100 | "Toggle Sidebar": "Zijbalk verschuiven", 101 | "Toggle Tray Icon": "Pictogram Lade wisselen", 102 | "Tools": "Hulpmiddelen", 103 | "Undo": "ongedaan maken", 104 | "Upload": "Uploaden", 105 | "Use system proxy settings (requires restart)": "Systeem proxy-instellingen gebruiken (opnieuw opstarten vereist)", 106 | "View": "Uitzicht", 107 | "View Shortcuts": "Bekijk snelkoppelingen", 108 | "Window": "Venster", 109 | "Window Shortcuts": "Venster snelkoppelingen", 110 | "Yes": "Ja", 111 | "Zoom In": "In zoomen", 112 | "Zoom Out": "Uitzoomen", 113 | "keyboard shortcuts": "Toetsenbord sneltoetsen", 114 | "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} gebruikt een oude versie {{{version}}} van Zulip Server. Het kan zijn dat deze applicatie niet goed zal werken met deze server." 115 | } 116 | -------------------------------------------------------------------------------- /app/renderer/js/pages/preference/network-section.ts: -------------------------------------------------------------------------------- 1 | import * as ConfigUtil from "../../../../common/config-util.ts"; 2 | import {html} from "../../../../common/html.ts"; 3 | import * as t from "../../../../common/translation-util.ts"; 4 | import {ipcRenderer} from "../../typed-ipc-renderer.ts"; 5 | 6 | import {generateSettingOption} from "./base-section.ts"; 7 | 8 | type NetworkSectionProperties = { 9 | $root: Element; 10 | }; 11 | 12 | export function initNetworkSection({$root}: NetworkSectionProperties): void { 13 | $root.innerHTML = html` 14 |
15 |
${t.__("Proxy")}
16 |
17 |
18 |
19 | ${t.__("Use system proxy settings (requires restart)")} 20 |
21 |
22 |
23 |
24 |
25 | ${t.__("Manual proxy configuration")} 26 |
27 |
28 |
29 |
30 |
31 | ${t.__("PAC script")} 32 | 36 |
37 |
38 | ${t.__("Proxy rules")} 39 | 43 |
44 |
45 | ${t.__("Proxy bypass rules")} 46 | 47 |
48 |
49 |
50 | ${t.__("Save")} 51 |
52 |
53 |
54 |
55 |
56 | `.html; 57 | 58 | const $proxyPac: HTMLInputElement = $root.querySelector( 59 | "#proxy-pac-option .setting-input-value", 60 | )!; 61 | const $proxyRules: HTMLInputElement = $root.querySelector( 62 | "#proxy-rules-option .setting-input-value", 63 | )!; 64 | const $proxyBypass: HTMLInputElement = $root.querySelector( 65 | "#proxy-bypass-option .setting-input-value", 66 | )!; 67 | const $proxySaveAction = $root.querySelector("#proxy-save-action")!; 68 | const $manualProxyBlock = $root.querySelector(".manual-proxy-block")!; 69 | 70 | toggleManualProxySettings(ConfigUtil.getConfigItem("useManualProxy", false)); 71 | updateProxyOption(); 72 | 73 | $proxyPac.value = ConfigUtil.getConfigItem("proxyPAC", ""); 74 | $proxyRules.value = ConfigUtil.getConfigItem("proxyRules", ""); 75 | $proxyBypass.value = ConfigUtil.getConfigItem("proxyBypass", ""); 76 | 77 | $proxySaveAction.addEventListener("click", () => { 78 | ConfigUtil.setConfigItem("proxyPAC", $proxyPac.value); 79 | ConfigUtil.setConfigItem("proxyRules", $proxyRules.value); 80 | ConfigUtil.setConfigItem("proxyBypass", $proxyBypass.value); 81 | 82 | ipcRenderer.send("forward-message", "reload-proxy", true); 83 | }); 84 | 85 | function toggleManualProxySettings(option: boolean): void { 86 | $manualProxyBlock.classList.toggle("hidden", !option); 87 | } 88 | 89 | function updateProxyOption(): void { 90 | generateSettingOption({ 91 | $element: $root.querySelector("#use-system-settings .setting-control")!, 92 | value: ConfigUtil.getConfigItem("useSystemProxy", false), 93 | clickHandler() { 94 | const newValue = !ConfigUtil.getConfigItem("useSystemProxy", false); 95 | const manualProxyValue = ConfigUtil.getConfigItem( 96 | "useManualProxy", 97 | false, 98 | ); 99 | if (manualProxyValue && newValue) { 100 | ConfigUtil.setConfigItem("useManualProxy", !manualProxyValue); 101 | toggleManualProxySettings(!manualProxyValue); 102 | } 103 | 104 | if (!newValue) { 105 | // Remove proxy system proxy settings 106 | ConfigUtil.setConfigItem("proxyRules", ""); 107 | ipcRenderer.send("forward-message", "reload-proxy", false); 108 | } 109 | 110 | ConfigUtil.setConfigItem("useSystemProxy", newValue); 111 | updateProxyOption(); 112 | }, 113 | }); 114 | generateSettingOption({ 115 | $element: $root.querySelector("#use-manual-settings .setting-control")!, 116 | value: ConfigUtil.getConfigItem("useManualProxy", false), 117 | clickHandler() { 118 | const newValue = !ConfigUtil.getConfigItem("useManualProxy", false); 119 | const systemProxyValue = ConfigUtil.getConfigItem( 120 | "useSystemProxy", 121 | false, 122 | ); 123 | toggleManualProxySettings(newValue); 124 | if (systemProxyValue && newValue) { 125 | ConfigUtil.setConfigItem("useSystemProxy", !systemProxyValue); 126 | } 127 | 128 | ConfigUtil.setConfigItem("proxyRules", ""); 129 | ConfigUtil.setConfigItem("useManualProxy", newValue); 130 | // Reload app only when turning manual proxy off, hence !newValue 131 | ipcRenderer.send("forward-message", "reload-proxy", !newValue); 132 | updateProxyOption(); 133 | }, 134 | }); 135 | } 136 | } 137 | --------------------------------------------------------------------------------