Compare commits
	
		
			93 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | af7272a439 | ||
|  | 9d08a13e64 | ||
|  | f98d6d7037 | ||
|  | da1cad9dff | ||
|  | 955a2eb6c7 | ||
|  | 1cf822a2b5 | ||
|  | b9baf140eb | ||
|  | 727c2335f6 | ||
|  | e8173919f8 | ||
|  | cf2f4fe9c9 | ||
|  | 47cdd5fa8b | ||
|  | 90e76fab6e | ||
|  | 193adb1901 | ||
|  | b520e12492 | ||
|  | ae642bc7ba | ||
|  | e90f3732c5 | ||
|  | 6b31a8a0c4 | ||
|  | f8758fa303 | ||
|  | d2de965106 | ||
|  | a32119b55d | ||
|  | 58049a91c4 | ||
|  | 9810d69c3b | ||
|  | d2f949d683 | ||
|  | a8c283a50b | ||
|  | dab29d4720 | ||
|  | 7fba8cfae9 | ||
|  | 32301656cc | ||
|  | 0e16283a37 | ||
|  | d86482a804 | ||
|  | 3af350e4dc | ||
|  | 39fc2053c5 | ||
|  | 044f1fd0f9 | ||
|  | 10fb0a82f9 | ||
|  | 123bd5b2c0 | ||
|  | ad771c3da8 | ||
|  | 4c58bc3aa3 | ||
|  | 9a8680d209 | ||
|  | 1569890f4d | ||
|  | 2ed400c23c | ||
|  | 70621431dc | ||
|  | 55b7e09796 | ||
|  | de2829a968 | ||
|  | 296de41779 | ||
|  | 8b9ebeee25 | ||
|  | 76e81ca337 | ||
|  | 2e7a9bb4ed | ||
|  | 77638f6287 | ||
|  | 6e8fe36876 | ||
|  | 2eea4a32a5 | ||
|  | 677dfe425c | ||
|  | 1da3ec545a | ||
|  | 3cb6ea4694 | ||
|  | 0cb7297017 | ||
|  | b8d7003446 | ||
|  | 6d27cf8c7d | ||
|  | 1ac2483cc4 | ||
|  | 4d3420dcd0 | ||
|  | 38450a9aed | ||
|  | 24de7ebb97 | ||
|  | 5a571d66d0 | ||
|  | 0ae998a51e | ||
|  | 447dd18b8b | ||
|  | 9a200dc40c | ||
|  | d42b752ac1 | ||
|  | 2f4103248d | ||
|  | 985d731d2b | ||
|  | 032f95150c | ||
|  | d1aa5778c3 | ||
|  | 13ce24b75e | ||
|  | c89ec2faf1 | ||
|  | 56ab0833b8 | ||
|  | c62b393c52 | ||
|  | 991de77cad | ||
|  | 94780c44c8 | ||
|  | 82542a6390 | ||
|  | 53ff8443dc | ||
|  | 3855ecab58 | ||
|  | a57cbb4aa8 | ||
|  | 56a4461c2a | ||
|  | cd023ec5ab | ||
|  | 1aa4ade3c0 | ||
|  | dcb46eef4f | ||
|  | e3e8ef6e3e | ||
|  | 6808b1971a | ||
|  | 1dd5269549 | ||
|  | d33adca1e8 | ||
|  | 8ea7f7864f | ||
|  | 493ae06e52 | ||
|  | 2b8f3536d3 | ||
|  | 544d23ec09 | ||
|  | 588d32fd22 | ||
|  | 1c471fe624 | ||
|  | 52486d687d | 
							
								
								
									
										2
									
								
								.github/workflows/node.js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -10,6 +10,6 @@ jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v4 | ||||
|       - run: npm ci | ||||
|       - run: npm test | ||||
|   | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,7 +8,8 @@ | ||||
| .transifexrc | ||||
|  | ||||
| # Compiled binary build directory | ||||
| dist/ | ||||
| /dist/ | ||||
| /dist-electron/ | ||||
|  | ||||
| #snap generated files | ||||
| snap/parts | ||||
| @@ -39,6 +40,3 @@ config.gypi | ||||
| # tests/package.json | ||||
|  | ||||
| .python-version | ||||
|  | ||||
| # Ignore all the typescript compiled files | ||||
| app/**/*.js | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| /app/**/*.js | ||||
| /app/translations/*.json | ||||
| /dist | ||||
| /dist-electron | ||||
| /public/translations/*.json | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "extends": ["stylelint-config-standard", "stylelint-config-prettier"], | ||||
|   "extends": ["stylelint-config-standard"], | ||||
|   "rules": { | ||||
|     "color-named": "never", | ||||
|     "color-no-hex": true, | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| [main] | ||||
| host = https://www.transifex.com | ||||
|  | ||||
| [zulip.desktopjson] | ||||
| file_filter = app/translations/<lang>.json | ||||
| [o:zulip:p:zulip:r:desktopjson] | ||||
| file_filter = public/translations/<lang>.json | ||||
| minimum_perc = 0 | ||||
| source_file = app/translations/en.json | ||||
| source_file = public/translations/en.json | ||||
| source_lang = en | ||||
| type = KEYVALUEJSON | ||||
|   | ||||
| @@ -10,7 +10,7 @@ Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If yo | ||||
|  | ||||
| ## Community | ||||
|  | ||||
| - The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io). | ||||
| - 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). | ||||
|  | ||||
| - 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). | ||||
|  | ||||
|   | ||||
| @@ -24,9 +24,9 @@ Please see the [installation guide](https://zulip.com/help/desktop-app-install-g | ||||
|  | ||||
| # Reporting issues | ||||
|  | ||||
| This desktop client shares most of its code with the Zulip webapp. | ||||
| This desktop client shares most of its code with the Zulip web app. | ||||
| Issues in an individual organization's Zulip window should be reported | ||||
| in the [Zulip server and webapp | ||||
| in the [Zulip server and web app | ||||
| project](https://github.com/zulip/zulip/issues/new). Other | ||||
| issues in the desktop app and its settings should be reported [in this | ||||
| project](https://github.com/zulip/zulip-desktop/issues/new). | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as z from "zod"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| export const dndSettingsSchemata = { | ||||
|   showNotification: z.boolean(), | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
|  | ||||
| import * as Sentry from "@sentry/electron"; | ||||
| import * as Sentry from "@sentry/core"; | ||||
| import {JsonDB} from "node-json-db"; | ||||
| import {DataError} from "node-json-db/dist/lib/Errors"; | ||||
| import type * as z from "zod"; | ||||
| import type {z} from "zod"; | ||||
| import {app, dialog} from "zulip:remote"; | ||||
|  | ||||
| import {configSchemata} from "./config-schemata"; | ||||
| import * as EnterpriseUtil from "./enterprise-util"; | ||||
| import Logger from "./logger-util"; | ||||
| import {app, dialog} from "./remote"; | ||||
| import {configSchemata} from "./config-schemata.js"; | ||||
| import * as EnterpriseUtil from "./enterprise-util.js"; | ||||
| import Logger from "./logger-util.js"; | ||||
|  | ||||
| export type Config = { | ||||
|   [Key in keyof typeof configSchemata]: z.output<typeof configSchemata[Key]>; | ||||
|   [Key in keyof typeof configSchemata]: z.output<(typeof configSchemata)[Key]>; | ||||
| }; | ||||
|  | ||||
| const logger = new Logger({ | ||||
| @@ -26,7 +26,7 @@ reloadDb(); | ||||
| export function getConfigItem<Key extends keyof Config>( | ||||
|   key: Key, | ||||
|   defaultValue: Config[Key], | ||||
| ): z.output<typeof configSchemata[Key]> { | ||||
| ): z.output<(typeof configSchemata)[Key]> { | ||||
|   try { | ||||
|     db.reload(); | ||||
|   } catch (error: unknown) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import fs from "node:fs"; | ||||
|  | ||||
| import {app} from "./remote"; | ||||
| import {app} from "zulip:remote"; | ||||
|  | ||||
| let setupCompleted = false; | ||||
|  | ||||
|   | ||||
| @@ -1,22 +1,22 @@ | ||||
| import process from "node:process"; | ||||
|  | ||||
| import type * as z from "zod"; | ||||
| import type {z} from "zod"; | ||||
|  | ||||
| import type {dndSettingsSchemata} from "./config-schemata"; | ||||
| import * as ConfigUtil from "./config-util"; | ||||
| import type {dndSettingsSchemata} from "./config-schemata.js"; | ||||
| import * as ConfigUtil from "./config-util.js"; | ||||
|  | ||||
| export type DndSettings = { | ||||
|   [Key in keyof typeof dndSettingsSchemata]: z.output< | ||||
|     typeof dndSettingsSchemata[Key] | ||||
|     (typeof dndSettingsSchemata)[Key] | ||||
|   >; | ||||
| }; | ||||
|  | ||||
| type SettingName = keyof DndSettings; | ||||
|  | ||||
| interface Toggle { | ||||
| type Toggle = { | ||||
|   dnd: boolean; | ||||
|   newSettings: Partial<DndSettings>; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function toggle(): Toggle { | ||||
|   const dnd = !ConfigUtil.getConfigItem("dnd", false); | ||||
|   | ||||
| @@ -2,14 +2,14 @@ import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import * as z from "zod"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import {enterpriseConfigSchemata} from "./config-schemata"; | ||||
| import Logger from "./logger-util"; | ||||
| import {enterpriseConfigSchemata} from "./config-schemata.js"; | ||||
| import Logger from "./logger-util.js"; | ||||
|  | ||||
| type EnterpriseConfig = { | ||||
|   [Key in keyof typeof enterpriseConfigSchemata]: z.output< | ||||
|     typeof enterpriseConfigSchemata[Key] | ||||
|     (typeof enterpriseConfigSchemata)[Key] | ||||
|   >; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import fs from "node:fs"; | ||||
| import os from "node:os"; | ||||
| import path from "node:path"; | ||||
|  | ||||
| import {html} from "./html"; | ||||
| import {html} from "./html.js"; | ||||
|  | ||||
| export async function openBrowser(url: URL): Promise<void> { | ||||
|   if (["http:", "https:", "mailto:"].includes(url.protocol)) { | ||||
| @@ -16,7 +16,7 @@ export async function openBrowser(url: URL): Promise<void> { | ||||
|     fs.writeFileSync( | ||||
|       file, | ||||
|       html` | ||||
|         <!DOCTYPE html> | ||||
|         <!doctype html> | ||||
|         <html> | ||||
|           <head> | ||||
|             <meta charset="UTF-8" /> | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| import {Console} from "node:console"; | ||||
| import {Console} from "node:console"; // eslint-disable-line n/prefer-global/console | ||||
| import fs from "node:fs"; | ||||
| import os from "node:os"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import {initSetUp} from "./default-util"; | ||||
| import {app} from "./remote"; | ||||
| import {app} from "zulip:remote"; | ||||
|  | ||||
| interface LoggerOptions { | ||||
| import {initSetUp} from "./default-util.js"; | ||||
|  | ||||
| type LoggerOptions = { | ||||
|   file?: string; | ||||
| } | ||||
| }; | ||||
|  | ||||
| initSetUp(); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| interface DialogBoxError { | ||||
| type DialogBoxError = { | ||||
|   title: string; | ||||
|   content: string; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function invalidZulipServerError(domain: string): string { | ||||
|   return `${domain} does not appear to be a valid Zulip server. Make sure that | ||||
|   | ||||
							
								
								
									
										15
									
								
								app/common/paths.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| import path from "node:path"; | ||||
| import process from "node:process"; | ||||
| import url from "node:url"; | ||||
|  | ||||
| export const bundlePath = __dirname; | ||||
|  | ||||
| export const publicPath = import.meta.env.DEV | ||||
|   ? path.join(bundlePath, "../public") | ||||
|   : bundlePath; | ||||
|  | ||||
| export const bundleUrl = import.meta.env.DEV | ||||
|   ? process.env.VITE_DEV_SERVER_URL | ||||
|   : url.pathToFileURL(__dirname).href + "/"; | ||||
|  | ||||
| export const publicUrl = bundleUrl; | ||||
| @@ -1,8 +0,0 @@ | ||||
| import process from "node:process"; | ||||
|  | ||||
| export const {app, dialog} = | ||||
|   process.type === "renderer" | ||||
|     ? // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires | ||||
|       (require("@electron/remote") as typeof import("@electron/remote")) | ||||
|     : // eslint-disable-next-line @typescript-eslint/no-require-imports | ||||
|       require("electron/main"); | ||||
| @@ -2,17 +2,15 @@ import path from "node:path"; | ||||
|  | ||||
| import i18n from "i18n"; | ||||
|  | ||||
| import * as ConfigUtil from "./config-util"; | ||||
| import * as ConfigUtil from "./config-util.js"; | ||||
| import {publicPath} from "./paths.js"; | ||||
|  | ||||
| i18n.configure({ | ||||
|   directory: path.join(__dirname, "../translations/"), | ||||
|   directory: path.join(publicPath, "translations/"), | ||||
|   updateFiles: false, | ||||
| }); | ||||
|  | ||||
| /* Fetches the current appLocale from settings.json */ | ||||
| const appLocale = ConfigUtil.getConfigItem("appLanguage", "en"); | ||||
| i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en"); | ||||
|  | ||||
| /* If no locale present in the json, en is set default */ | ||||
| export function __(phrase: string): string { | ||||
|   return i18n.__({phrase, locale: appLocale ? appLocale : "en"}); | ||||
| } | ||||
| export {__} from "i18n"; | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import type {DndSettings} from "./dnd-util"; | ||||
| import type {MenuProps, ServerConf} from "./types"; | ||||
| import type {DndSettings} from "./dnd-util.js"; | ||||
| import type {MenuProps, ServerConf} from "./types.js"; | ||||
|  | ||||
| export interface MainMessage { | ||||
| export type MainMessage = { | ||||
|   "clear-app-settings": () => void; | ||||
|   "configure-spell-checker": () => void; | ||||
|   "fetch-user-agent": () => string; | ||||
|   "focus-app": () => void; | ||||
|   "focus-this-webview": () => void; | ||||
|   "new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array}; | ||||
|   "permission-callback": (permissionCallbackId: number, grant: boolean) => void; | ||||
|   "quit-app": () => void; | ||||
|   "realm-icon-changed": (serverURL: string, iconURL: string) => void; | ||||
| @@ -22,15 +23,16 @@ export interface MainMessage { | ||||
|   "update-badge": (messageCount: number) => void; | ||||
|   "update-menu": (props: MenuProps) => void; | ||||
|   "update-taskbar-icon": (data: string, text: string) => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export interface MainCall { | ||||
| export type MainCall = { | ||||
|   "get-server-settings": (domain: string) => ServerConf; | ||||
|   "is-online": (url: string) => boolean; | ||||
|   "save-server-icon": (iconURL: string) => string; | ||||
| } | ||||
|   "poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined; | ||||
|   "save-server-icon": (iconURL: string) => string | null; | ||||
| }; | ||||
|  | ||||
| export interface RendererMessage { | ||||
| export type RendererMessage = { | ||||
|   back: () => void; | ||||
|   "copy-zulip-url": () => void; | ||||
|   destroytray: () => void; | ||||
| @@ -74,9 +76,9 @@ export interface RendererMessage { | ||||
|   toggletray: () => void; | ||||
|   tray: (arg: number) => void; | ||||
|   "update-realm-icon": (serverURL: string, iconURL: string) => void; | ||||
|   "update-realm-name": (serveRURL: string, realmName: string) => void; | ||||
|   "update-realm-name": (serverURL: string, realmName: string) => void; | ||||
|   "webview-reload": () => void; | ||||
|   zoomActualSize: () => void; | ||||
|   zoomIn: () => void; | ||||
|   zoomOut: () => void; | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| export interface MenuProps { | ||||
| export type MenuProps = { | ||||
|   tabs: TabData[]; | ||||
|   activeTabIndex?: number; | ||||
|   enableMenu?: boolean; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export type NavItem = | ||||
|   | "General" | ||||
| @@ -11,16 +11,18 @@ export type NavItem = | ||||
|   | "Organizations" | ||||
|   | "Shortcuts"; | ||||
|  | ||||
| export interface ServerConf { | ||||
| export type ServerConf = { | ||||
|   url: string; | ||||
|   alias: string; | ||||
|   icon: string; | ||||
| } | ||||
|   zulipVersion: string; | ||||
|   zulipFeatureLevel: number; | ||||
| }; | ||||
|  | ||||
| export type TabRole = "server" | "function"; | ||||
|  | ||||
| export interface TabData { | ||||
| export type TabData = { | ||||
|   role: TabRole; | ||||
|   name: string; | ||||
|   index: number; | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| import {shell} from "electron/common"; | ||||
| import {app, dialog, session} from "electron/main"; | ||||
| import process from "node:process"; | ||||
| import util from "node:util"; | ||||
|  | ||||
| import log from "electron-log"; | ||||
| import log from "electron-log/main"; | ||||
| import type {UpdateDownloadedEvent, UpdateInfo} from "electron-updater"; | ||||
| import {autoUpdater} from "electron-updater"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
|  | ||||
| import {linuxUpdateNotification} from "./linuxupdater"; // Required only in case of linux | ||||
| import {linuxUpdateNotification} from "./linuxupdater.js"; // Required only in case of linux | ||||
|  | ||||
| const sleep = util.promisify(setTimeout); | ||||
| let quitting = false; | ||||
|  | ||||
| export function shouldQuitForUpdate(): boolean { | ||||
|   return quitting; | ||||
| } | ||||
|  | ||||
| export async function appUpdater(updateFromMenu = false): Promise<void> { | ||||
|   // Don't initiate auto-updates in development | ||||
| @@ -27,17 +30,21 @@ export async function appUpdater(updateFromMenu = false): Promise<void> { | ||||
|  | ||||
|   let updateAvailable = false; | ||||
|  | ||||
|   // Log whats happening | ||||
|   log.transports.file.fileName = "updates.log"; | ||||
|   log.transports.file.level = "info"; | ||||
|   autoUpdater.logger = log; | ||||
|   // Log what's happening | ||||
|   const updateLogger = log.create({logId: "updates"}); | ||||
|   updateLogger.transports.file.fileName = "updates.log"; | ||||
|   updateLogger.transports.file.level = "info"; | ||||
|   autoUpdater.logger = updateLogger; | ||||
|  | ||||
|   // Handle auto updates for beta/pre releases | ||||
|   const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false); | ||||
|  | ||||
|   autoUpdater.allowPrerelease = isBetaUpdate; | ||||
|  | ||||
|   const eventsListenerRemove = ["update-available", "update-not-available"]; | ||||
|   const eventsListenerRemove = [ | ||||
|     "update-available", | ||||
|     "update-not-available", | ||||
|   ] as const; | ||||
|   autoUpdater.on("update-available", async (info: UpdateInfo) => { | ||||
|     if (updateFromMenu) { | ||||
|       updateAvailable = true; | ||||
| @@ -104,10 +111,8 @@ Current Version: ${app.getVersion()}`, | ||||
|       detail: "It will be installed the next time you restart the application", | ||||
|     }); | ||||
|     if (response === 0) { | ||||
|       await sleep(1000); | ||||
|       quitting = true; | ||||
|       autoUpdater.quitAndInstall(); | ||||
|       // Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app. | ||||
|       app.quit(); | ||||
|     } | ||||
|   }); | ||||
|   // Init for updates | ||||
|   | ||||
| @@ -3,9 +3,9 @@ import type {BrowserWindow} from "electron/main"; | ||||
| import {app} from "electron/main"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
|  | ||||
| import {send} from "./typed-ipc-main"; | ||||
| import {send} from "./typed-ipc-main.js"; | ||||
|  | ||||
| function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void { | ||||
|   if (process.platform === "win32") { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type {Event} from "electron/common"; | ||||
| import {shell} from "electron/common"; | ||||
| import type { | ||||
|   HandlerDetails, | ||||
| @@ -8,10 +9,10 @@ import {Notification, app} from "electron/main"; | ||||
| import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import * as LinkUtil from "../common/link-util"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
| import * as LinkUtil from "../common/link-util.js"; | ||||
|  | ||||
| import {send} from "./typed-ipc-main"; | ||||
| import {send} from "./typed-ipc-main.js"; | ||||
|  | ||||
| function isUploadsUrl(server: string, url: URL): boolean { | ||||
|   return url.origin === server && url.pathname.startsWith("/user_uploads/"); | ||||
| @@ -31,7 +32,7 @@ function downloadFile({ | ||||
|   failed(state: string): void; | ||||
| }) { | ||||
|   contents.downloadURL(url); | ||||
|   contents.session.once("will-download", async (_event: Event, item) => { | ||||
|   contents.session.once("will-download", async (_event, item) => { | ||||
|     if (ConfigUtil.getConfigItem("promptDownload", false)) { | ||||
|       const showDialogOptions: SaveDialogOptions = { | ||||
|         defaultPath: path.join(downloadPath, item.getFilename()), | ||||
| @@ -86,7 +87,7 @@ function downloadFile({ | ||||
|     }; | ||||
|  | ||||
|     item.on("updated", updatedListener); | ||||
|     item.once("done", async (_event: Event, state) => { | ||||
|     item.once("done", async (_event, state) => { | ||||
|       if (state === "completed") { | ||||
|         await completed(item.getSavePath(), path.basename(item.getSavePath())); | ||||
|       } else { | ||||
| @@ -105,7 +106,13 @@ export default function handleExternalLink( | ||||
|   details: HandlerDetails, | ||||
|   mainContents: WebContents, | ||||
| ): void { | ||||
|   const url = new URL(details.url); | ||||
|   let url: URL; | ||||
|   try { | ||||
|     url = new URL(details.url); | ||||
|   } catch { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const downloadPath = ConfigUtil.getConfigItem( | ||||
|     "downloadsPath", | ||||
|     `${app.getPath("downloads")}`, | ||||
|   | ||||
| @@ -1,23 +1,37 @@ | ||||
| import type {Event} from "electron/common"; | ||||
| import {clipboard} from "electron/common"; | ||||
| import type {IpcMainEvent, WebContents} from "electron/main"; | ||||
| import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main"; | ||||
| import { | ||||
|   BrowserWindow, | ||||
|   app, | ||||
|   dialog, | ||||
|   powerMonitor, | ||||
|   session, | ||||
|   webContents, | ||||
| } from "electron/main"; | ||||
| import {Buffer} from "node:buffer"; | ||||
| import crypto from "node:crypto"; | ||||
| import path from "node:path"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import * as remoteMain from "@electron/remote/main"; | ||||
| import windowStateKeeper from "electron-window-state"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import type {RendererMessage} from "../common/typed-ipc"; | ||||
| import type {MenuProps} from "../common/types"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
| import {bundlePath, bundleUrl, publicPath} from "../common/paths.js"; | ||||
| import type {RendererMessage} from "../common/typed-ipc.js"; | ||||
| import type {MenuProps} from "../common/types.js"; | ||||
|  | ||||
| import {appUpdater} from "./autoupdater"; | ||||
| import * as BadgeSettings from "./badge-settings"; | ||||
| import handleExternalLink from "./handle-external-link"; | ||||
| import * as AppMenu from "./menu"; | ||||
| import {_getServerSettings, _isOnline, _saveServerIcon} from "./request"; | ||||
| import {sentryInit} from "./sentry"; | ||||
| import {setAutoLaunch} from "./startup"; | ||||
| import {ipcMain, send} from "./typed-ipc-main"; | ||||
| import {appUpdater, shouldQuitForUpdate} from "./autoupdater.js"; | ||||
| import * as BadgeSettings from "./badge-settings.js"; | ||||
| import handleExternalLink from "./handle-external-link.js"; | ||||
| import * as AppMenu from "./menu.js"; | ||||
| import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.js"; | ||||
| import {sentryInit} from "./sentry.js"; | ||||
| import {setAutoLaunch} from "./startup.js"; | ||||
| import {ipcMain, send} from "./typed-ipc-main.js"; | ||||
|  | ||||
| import "gatemaker/electron-setup"; // eslint-disable-line import/no-unassigned-import | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
| const {GDK_BACKEND} = process.env; | ||||
| @@ -33,13 +47,13 @@ let badgeCount: number; | ||||
|  | ||||
| let isQuitting = false; | ||||
|  | ||||
| // Load this url in main window | ||||
| const mainUrl = "file://" + path.join(__dirname, "../renderer", "main.html"); | ||||
| // Load this file in main window | ||||
| const mainUrl = new URL("app/renderer/main.html", bundleUrl).href; | ||||
|  | ||||
| const permissionCallbacks = new Map<number, (grant: boolean) => void>(); | ||||
| let nextPermissionCallbackId = 0; | ||||
|  | ||||
| const appIcon = path.join(__dirname, "../resources", "Icon"); | ||||
| const appIcon = path.join(publicPath, "resources/Icon"); | ||||
|  | ||||
| const iconPath = (): string => | ||||
|   appIcon + (process.platform === "win32" ? ".ico" : ".png"); | ||||
| @@ -72,7 +86,8 @@ function createMainWindow(): BrowserWindow { | ||||
|     minWidth: 500, | ||||
|     minHeight: 400, | ||||
|     webPreferences: { | ||||
|       preload: require.resolve("../renderer/js/main"), | ||||
|       preload: path.join(bundlePath, "renderer.js"), | ||||
|       sandbox: false, | ||||
|       webviewTag: true, | ||||
|     }, | ||||
|     show: false, | ||||
| @@ -91,7 +106,7 @@ function createMainWindow(): BrowserWindow { | ||||
|       app.quit(); | ||||
|     } | ||||
|  | ||||
|     if (!isQuitting) { | ||||
|     if (!isQuitting && !shouldQuitForUpdate()) { | ||||
|       event.preventDefault(); | ||||
|  | ||||
|       if (process.platform === "darwin") { | ||||
| @@ -163,7 +178,7 @@ function createMainWindow(): BrowserWindow { | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "permission-callback", | ||||
|     (event: Event, permissionCallbackId: number, grant: boolean) => { | ||||
|     (event, permissionCallbackId: number, grant: boolean) => { | ||||
|       permissionCallbacks.get(permissionCallbackId)?.(grant); | ||||
|       permissionCallbacks.delete(permissionCallbackId); | ||||
|     }, | ||||
| @@ -174,7 +189,7 @@ function createMainWindow(): BrowserWindow { | ||||
|     mainWindow.show(); | ||||
|   }); | ||||
|  | ||||
|   app.on("web-contents-created", (_event: Event, contents: WebContents) => { | ||||
|   app.on("web-contents-created", (_event, contents: WebContents) => { | ||||
|     contents.setWindowOpenHandler((details) => { | ||||
|       handleExternalLink(contents, details, page); | ||||
|       return {action: "deny"}; | ||||
| @@ -198,6 +213,42 @@ function createMainWindow(): BrowserWindow { | ||||
|   configureSpellChecker(); | ||||
|   ipcMain.on("configure-spell-checker", configureSpellChecker); | ||||
|  | ||||
|   const clipboardSigKey = crypto.randomBytes(32); | ||||
|  | ||||
|   ipcMain.on("new-clipboard-key", (event) => { | ||||
|     const key = crypto.randomBytes(32); | ||||
|     const hmac = crypto.createHmac("sha256", clipboardSigKey); | ||||
|     hmac.update(key); | ||||
|     event.returnValue = {key, sig: hmac.digest()}; | ||||
|   }); | ||||
|  | ||||
|   ipcMain.handle("poll-clipboard", (event, key, sig) => { | ||||
|     // Check that the key was generated here. | ||||
|     const hmac = crypto.createHmac("sha256", clipboardSigKey); | ||||
|     hmac.update(key); | ||||
|     if (!crypto.timingSafeEqual(sig, hmac.digest())) return; | ||||
|  | ||||
|     try { | ||||
|       // Check that the data on the clipboard was encrypted to the key. | ||||
|       const data = Buffer.from(clipboard.readText(), "hex"); | ||||
|       const iv = data.slice(0, 12); | ||||
|       const ciphertext = data.slice(12, -16); | ||||
|       const authTag = data.slice(-16); | ||||
|       const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, { | ||||
|         authTagLength: 16, | ||||
|       }); | ||||
|       decipher.setAuthTag(authTag); | ||||
|       return ( | ||||
|         decipher.update(ciphertext, undefined, "utf8") + decipher.final("utf8") | ||||
|       ); | ||||
|     } catch { | ||||
|       // If the parsing or decryption failed in any way, | ||||
|       // the correct token hasn’t been copied yet; try | ||||
|       // again next time. | ||||
|       return undefined; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   AppMenu.setMenu({ | ||||
|     tabs: [], | ||||
|   }); | ||||
| @@ -318,24 +369,21 @@ ${error}`, | ||||
|     BadgeSettings.updateBadge(badgeCount, mainWindow); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on("toggle-menubar", (_event: IpcMainEvent, showMenubar: boolean) => { | ||||
|   ipcMain.on("toggle-menubar", (_event, showMenubar: boolean) => { | ||||
|     mainWindow.autoHideMenuBar = showMenubar; | ||||
|     mainWindow.setMenuBarVisibility(!showMenubar); | ||||
|     send(page, "toggle-autohide-menubar", showMenubar, true); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on("update-badge", (_event: IpcMainEvent, messageCount: number) => { | ||||
|   ipcMain.on("update-badge", (_event, messageCount: number) => { | ||||
|     badgeCount = messageCount; | ||||
|     BadgeSettings.updateBadge(badgeCount, mainWindow); | ||||
|     send(page, "tray", messageCount); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "update-taskbar-icon", | ||||
|     (_event: IpcMainEvent, data: string, text: string) => { | ||||
|       BadgeSettings.updateTaskbarIcon(data, text, mainWindow); | ||||
|     }, | ||||
|   ); | ||||
|   ipcMain.on("update-taskbar-icon", (_event, data: string, text: string) => { | ||||
|     BadgeSettings.updateTaskbarIcon(data, text, mainWindow); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "forward-message", | ||||
| @@ -348,7 +396,22 @@ ${error}`, | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   ipcMain.on("update-menu", (_event: IpcMainEvent, props: MenuProps) => { | ||||
|   ipcMain.on( | ||||
|     "forward-to", | ||||
|     <Channel extends keyof RendererMessage>( | ||||
|       _event: IpcMainEvent, | ||||
|       webContentsId: number, | ||||
|       listener: Channel, | ||||
|       ...parameters: Parameters<RendererMessage[Channel]> | ||||
|     ) => { | ||||
|       const contents = webContents.fromId(webContentsId); | ||||
|       if (contents !== undefined) { | ||||
|         send(contents, listener, ...parameters); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   ipcMain.on("update-menu", (_event, props: MenuProps) => { | ||||
|     AppMenu.setMenu(props); | ||||
|     if (props.activeTabIndex !== undefined) { | ||||
|       const activeTab = props.tabs[props.activeTabIndex]; | ||||
| @@ -356,32 +419,29 @@ ${error}`, | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "toggleAutoLauncher", | ||||
|     async (_event: IpcMainEvent, AutoLaunchValue: boolean) => { | ||||
|       await setAutoLaunch(AutoLaunchValue); | ||||
|     }, | ||||
|   ); | ||||
|   ipcMain.on("toggleAutoLauncher", async (_event, AutoLaunchValue: boolean) => { | ||||
|     await setAutoLaunch(AutoLaunchValue); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "realm-name-changed", | ||||
|     (_event: IpcMainEvent, serverURL: string, realmName: string) => { | ||||
|     (_event, serverURL: string, realmName: string) => { | ||||
|       send(page, "update-realm-name", serverURL, realmName); | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   ipcMain.on( | ||||
|     "realm-icon-changed", | ||||
|     (_event: IpcMainEvent, serverURL: string, iconURL: string) => { | ||||
|     (_event, serverURL: string, iconURL: string) => { | ||||
|       send(page, "update-realm-icon", serverURL, iconURL); | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   ipcMain.on("save-last-tab", (_event: IpcMainEvent, index: number) => { | ||||
|   ipcMain.on("save-last-tab", (_event, index: number) => { | ||||
|     ConfigUtil.setConfigItem("lastActiveTab", index); | ||||
|   }); | ||||
|  | ||||
|   ipcMain.on("focus-this-webview", (event: IpcMainEvent) => { | ||||
|   ipcMain.on("focus-this-webview", (event) => { | ||||
|     send(page, "focus-webview-with-id", event.sender.id); | ||||
|     mainWindow.show(); | ||||
|   }); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import path from "node:path"; | ||||
| import {JsonDB} from "node-json-db"; | ||||
| import {DataError} from "node-json-db/dist/lib/Errors"; | ||||
|  | ||||
| import Logger from "../common/logger-util"; | ||||
| import Logger from "../common/logger-util.js"; | ||||
|  | ||||
| const logger = new Logger({ | ||||
|   file: "linux-update-util.log", | ||||
|   | ||||
| @@ -1,15 +1,13 @@ | ||||
| import type {Session} from "electron/main"; | ||||
| import {Notification, app, net} from "electron/main"; | ||||
| import {Notification, app} from "electron/main"; | ||||
|  | ||||
| import getStream from "get-stream"; | ||||
| import * as semver from "semver"; | ||||
| import * as z from "zod"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import Logger from "../common/logger-util"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
| import Logger from "../common/logger-util.js"; | ||||
|  | ||||
| import * as LinuxUpdateUtil from "./linux-update-util"; | ||||
| import {fetchResponse} from "./request"; | ||||
| import * as LinuxUpdateUtil from "./linux-update-util.js"; | ||||
|  | ||||
| const logger = new Logger({ | ||||
|   file: "linux-update-util.log", | ||||
| @@ -20,13 +18,13 @@ export async function linuxUpdateNotification(session: Session): Promise<void> { | ||||
|   url = ConfigUtil.getConfigItem("betaUpdate", false) ? url : url + "/latest"; | ||||
|  | ||||
|   try { | ||||
|     const response = await fetchResponse(net.request({url, session})); | ||||
|     if (response.statusCode !== 200) { | ||||
|       logger.log("Linux update response status: ", response.statusCode); | ||||
|     const response = await session.fetch(url); | ||||
|     if (!response.ok) { | ||||
|       logger.log("Linux update response status: ", response.status); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const data: unknown = JSON.parse(await getStream(response)); | ||||
|     const data: unknown = await response.json(); | ||||
|     /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|     const latestVersion = ConfigUtil.getConfigItem("betaUpdate", false) | ||||
|       ? z.array(z.object({tag_name: z.string()})).parse(data)[0].tag_name | ||||
|   | ||||
| @@ -5,14 +5,14 @@ import process from "node:process"; | ||||
|  | ||||
| import AdmZip from "adm-zip"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import * as DNDUtil from "../common/dnd-util"; | ||||
| import * as t from "../common/translation-util"; | ||||
| import type {RendererMessage} from "../common/typed-ipc"; | ||||
| import type {MenuProps, TabData} from "../common/types"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
| import * as DNDUtil from "../common/dnd-util.js"; | ||||
| import * as t from "../common/translation-util.js"; | ||||
| import type {RendererMessage} from "../common/typed-ipc.js"; | ||||
| import type {MenuProps, TabData} from "../common/types.js"; | ||||
|  | ||||
| import {appUpdater} from "./autoupdater"; | ||||
| import {send} from "./typed-ipc-main"; | ||||
| import {appUpdater} from "./autoupdater.js"; | ||||
| import {send} from "./typed-ipc-main.js"; | ||||
|  | ||||
| const appName = app.name; | ||||
|  | ||||
| @@ -66,7 +66,7 @@ function getToolsSubmenu(): MenuItemConstructorOptions[] { | ||||
|       click() { | ||||
|         const zip = new AdmZip(); | ||||
|         const date = new Date(); | ||||
|         const dateString = date.toLocaleDateString().replace(/\//g, "-"); | ||||
|         const dateString = date.toLocaleDateString().replaceAll("/", "-"); | ||||
|  | ||||
|         // Create a zip file of all the logs and config data | ||||
|         zip.addLocalFolder(`${app.getPath("appData")}/${appName}/Logs`); | ||||
|   | ||||
| @@ -1,37 +1,20 @@ | ||||
| import type {ClientRequest, IncomingMessage, Session} from "electron/main"; | ||||
| import {app, net} from "electron/main"; | ||||
| import type {Session} from "electron/main"; | ||||
| import {app} from "electron/main"; | ||||
| import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
| import stream from "node:stream"; | ||||
| import util from "node:util"; | ||||
| import {Readable} from "node:stream"; | ||||
| import {pipeline} from "node:stream/promises"; | ||||
| import type {ReadableStream} from "node:stream/web"; | ||||
|  | ||||
| import * as Sentry from "@sentry/electron"; | ||||
| import getStream from "get-stream"; | ||||
| import * as z from "zod"; | ||||
| import * as Sentry from "@sentry/electron/main"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import Logger from "../common/logger-util"; | ||||
| import * as Messages from "../common/messages"; | ||||
| import type {ServerConf} from "../common/types"; | ||||
|  | ||||
| export async function fetchResponse( | ||||
|   request: ClientRequest, | ||||
| ): Promise<IncomingMessage> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     request.on("response", resolve); | ||||
|     request.on("abort", () => { | ||||
|       reject(new Error("Request aborted")); | ||||
|     }); | ||||
|     request.on("error", reject); | ||||
|     request.end(); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| const pipeline = util.promisify(stream.pipeline); | ||||
| import Logger from "../common/logger-util.js"; | ||||
| import * as Messages from "../common/messages.js"; | ||||
| import type {ServerConf} from "../common/types.js"; | ||||
|  | ||||
| /* Request: domain-util */ | ||||
|  | ||||
| const defaultIconUrl = "../renderer/img/icon.png"; | ||||
|  | ||||
| const logger = new Logger({ | ||||
|   file: "domain-util.log", | ||||
| }); | ||||
| @@ -61,23 +44,26 @@ export const _getServerSettings = async ( | ||||
|   domain: string, | ||||
|   session: Session, | ||||
| ): Promise<ServerConf> => { | ||||
|   const response = await fetchResponse( | ||||
|     net.request({ | ||||
|       url: domain + "/api/v1/server_settings", | ||||
|       session, | ||||
|     }), | ||||
|   ); | ||||
|   if (response.statusCode !== 200) { | ||||
|   const response = await session.fetch(domain + "/api/v1/server_settings"); | ||||
|   if (!response.ok) { | ||||
|     throw new Error(Messages.invalidZulipServerError(domain)); | ||||
|   } | ||||
|  | ||||
|   const data: unknown = JSON.parse(await getStream(response)); | ||||
|   const data: unknown = await response.json(); | ||||
|   /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|   const {realm_name, realm_uri, realm_icon} = z | ||||
|   const { | ||||
|     realm_name, | ||||
|     realm_uri, | ||||
|     realm_icon, | ||||
|     zulip_version, | ||||
|     zulip_feature_level, | ||||
|   } = z | ||||
|     .object({ | ||||
|       realm_name: z.string(), | ||||
|       realm_uri: z.string(), | ||||
|       realm_uri: z.string().url(), | ||||
|       realm_icon: z.string(), | ||||
|       zulip_version: z.string().default("unknown"), | ||||
|       zulip_feature_level: z.number().default(0), | ||||
|     }) | ||||
|     .parse(data); | ||||
|   /* eslint-enable @typescript-eslint/naming-convention */ | ||||
| @@ -88,28 +74,33 @@ export const _getServerSettings = async ( | ||||
|     icon: realm_icon.startsWith("/") ? realm_uri + realm_icon : realm_icon, | ||||
|     url: realm_uri, | ||||
|     alias: realm_name, | ||||
|     zulipVersion: zulip_version, | ||||
|     zulipFeatureLevel: zulip_feature_level, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const _saveServerIcon = async ( | ||||
|   url: string, | ||||
|   session: Session, | ||||
| ): Promise<string> => { | ||||
| ): Promise<string | null> => { | ||||
|   try { | ||||
|     const response = await fetchResponse(net.request({url, session})); | ||||
|     if (response.statusCode !== 200) { | ||||
|     const response = await session.fetch(url); | ||||
|     if (!response.ok) { | ||||
|       logger.log("Could not get server icon."); | ||||
|       return defaultIconUrl; | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const filePath = generateFilePath(url); | ||||
|     await pipeline(response, fs.createWriteStream(filePath)); | ||||
|     await pipeline( | ||||
|       Readable.fromWeb(response.body as ReadableStream<Uint8Array>), | ||||
|       fs.createWriteStream(filePath), | ||||
|     ); | ||||
|     return filePath; | ||||
|   } catch (error: unknown) { | ||||
|     logger.log("Could not get server icon."); | ||||
|     logger.log(error); | ||||
|     Sentry.captureException(error); | ||||
|     return defaultIconUrl; | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @@ -120,16 +111,10 @@ export const _isOnline = async ( | ||||
|   session: Session, | ||||
| ): Promise<boolean> => { | ||||
|   try { | ||||
|     const response = await fetchResponse( | ||||
|       net.request({ | ||||
|         method: "HEAD", | ||||
|         url: `${url}/api/v1/server_settings`, | ||||
|         session, | ||||
|       }), | ||||
|     ); | ||||
|     const isValidResponse = | ||||
|       response.statusCode >= 200 && response.statusCode < 400; | ||||
|     return isValidResponse; | ||||
|     const response = await session.fetch(`${url}/api/v1/server_settings`, { | ||||
|       method: "HEAD", | ||||
|     }); | ||||
|     return response.ok; | ||||
|   } catch (error: unknown) { | ||||
|     logger.log(error); | ||||
|     return false; | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import {app} from "electron/main"; | ||||
|  | ||||
| import * as Sentry from "@sentry/electron"; | ||||
| import * as Sentry from "@sentry/electron/main"; | ||||
|  | ||||
| import {getConfigItem} from "../common/config-util"; | ||||
| import {getConfigItem} from "../common/config-util.js"; | ||||
|  | ||||
| export const sentryInit = (): void => { | ||||
|   Sentry.init({ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import process from "node:process"; | ||||
|  | ||||
| import AutoLaunch from "auto-launch"; | ||||
|  | ||||
| import * as ConfigUtil from "../common/config-util"; | ||||
| import * as ConfigUtil from "../common/config-util.js"; | ||||
|  | ||||
| export const setAutoLaunch = async ( | ||||
|   AutoLaunchValue: boolean, | ||||
|   | ||||
| @@ -33,6 +33,15 @@ export const ipcMain: { | ||||
|       ...args: Parameters<RendererMessage[Channel]> | ||||
|     ) => void, | ||||
|   ): void; | ||||
|   on( | ||||
|     channel: "forward-to", | ||||
|     listener: <Channel extends keyof RendererMessage>( | ||||
|       event: IpcMainEvent, | ||||
|       webContentsId: number, | ||||
|       channel: Channel, | ||||
|       ...args: Parameters<RendererMessage[Channel]> | ||||
|     ) => void, | ||||
|   ): void; | ||||
|   on<Channel extends keyof MainMessage>( | ||||
|     channel: Channel, | ||||
|     listener: MainListener<Channel>, | ||||
|   | ||||
							
								
								
									
										26
									
								
								app/renderer/about.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| <!doctype html> | ||||
| <meta charset="UTF-8" /> | ||||
| <link rel="stylesheet" href="css/about.css" /> | ||||
|  | ||||
| <!-- Initially hidden to prevent FOUC --> | ||||
| <div class="about" hidden> | ||||
|   <img class="logo" src="../resources/zulip.png" /> | ||||
|   <p class="detail" id="version"></p> | ||||
|   <div class="maintenance-info"> | ||||
|     <p class="detail maintainer"> | ||||
|       Maintained by | ||||
|       <a href="https://zulip.com" target="_blank" rel="noopener noreferrer" | ||||
|         >Zulip</a | ||||
|       > | ||||
|     </p> | ||||
|     <p class="detail license"> | ||||
|       Available under the | ||||
|       <a | ||||
|         href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE" | ||||
|         target="_blank" | ||||
|         rel="noopener noreferrer" | ||||
|         >Apache 2.0 License</a | ||||
|       > | ||||
|     </p> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -2,7 +2,9 @@ | ||||
|   font-family: "Material Icons"; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local("Material Icons"), local("MaterialIcons-Regular"), | ||||
|   src: | ||||
|     local("Material Icons"), | ||||
|     local("MaterialIcons-Regular"), | ||||
|     url("../fonts/MaterialIcons-Regular.ttf") format("truetype"); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -44,6 +44,7 @@ body { | ||||
|  | ||||
| #view-controls-container { | ||||
|   height: calc(100% - 208px); | ||||
|   scrollbar-gutter: stable both-edges; | ||||
|   overflow-y: hidden; | ||||
| } | ||||
|  | ||||
| @@ -52,16 +53,15 @@ body { | ||||
| } | ||||
|  | ||||
| #view-controls-container::-webkit-scrollbar-track { | ||||
|   box-shadow: inset 0 0 6px rgb(0 0 0 / 30%); | ||||
|   background-color: rgb(0 0 0 / 30%); | ||||
| } | ||||
|  | ||||
| #view-controls-container::-webkit-scrollbar-thumb { | ||||
|   background-color: rgb(169 169 169 / 100%); | ||||
|   outline: 1px solid rgb(169 169 169 / 100%); | ||||
| } | ||||
|  | ||||
| #view-controls-container:hover { | ||||
|   overflow-y: overlay; | ||||
|   overflow-y: scroll; | ||||
| } | ||||
|  | ||||
| /******************* | ||||
| @@ -290,7 +290,9 @@ body { | ||||
|   content: ""; | ||||
|   position: absolute; | ||||
|   z-index: 1; | ||||
|   background: rgb(255 255 255 / 100%) url("../img/ic_loading.gif") no-repeat; | ||||
|  | ||||
|   /* Spinner is released under loading.io free License: https://loading.io/license/#free-license */ | ||||
|   background: rgb(255 255 255 / 100%) url("../img/ic_loading.svg") no-repeat; | ||||
|   background-size: 60px 60px; | ||||
|   background-position: center; | ||||
|   width: 100%; | ||||
| @@ -303,7 +305,7 @@ body { | ||||
|   visibility: hidden; | ||||
| } | ||||
|  | ||||
| webview, | ||||
| .webview-pane, | ||||
| .functional-view { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
| @@ -312,7 +314,16 @@ webview, | ||||
|   visibility: hidden; | ||||
| } | ||||
|  | ||||
| webview.active, | ||||
| .webview-pane { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|  | ||||
| .webview-pane > webview { | ||||
|   flex: 1; | ||||
| } | ||||
|  | ||||
| .webview-pane.active, | ||||
| .functional-view.active { | ||||
|   z-index: 1; | ||||
|   visibility: visible; | ||||
| @@ -322,6 +333,30 @@ webview.focus { | ||||
|   outline: 0 solid transparent; | ||||
| } | ||||
|  | ||||
| .webview-unsupported { | ||||
|   background: rgb(254 243 199); | ||||
|   border: 1px solid rgb(253 230 138); | ||||
|   color: rgb(69 26 3); | ||||
|   font-family: system-ui; | ||||
|   font-size: 14px; | ||||
|   display: flex; | ||||
| } | ||||
|  | ||||
| .webview-unsupported[hidden] { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .webview-unsupported-message { | ||||
|   padding: 0.3em; | ||||
|   flex: 1; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .webview-unsupported-dismiss { | ||||
|   padding: 0.3em; | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| /* Tooltip styling */ | ||||
|  | ||||
| #loading-tooltip, | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| @import url("@yaireo/tagify/dist/tagify.css"); | ||||
|  | ||||
| :host { | ||||
|   contain: strict; | ||||
|   display: flow-root; | ||||
| @@ -10,6 +12,11 @@ | ||||
|   letter-spacing: -0.08px; | ||||
|   line-height: 18px; | ||||
|   color: rgb(139 142 143 / 100%); | ||||
|  | ||||
|   /* Copied from https://github.com/yairEO/tagify/blob/v4.17.7/src/tagify.scss#L4-L8 */ | ||||
|   --tagify-dd-color-primary: rgb(53 149 246); | ||||
|   --tagify-dd-bg-color: rgb(255 255 255); | ||||
|   --tagify-dd-item-pad: 0.3em 0.5em; | ||||
| } | ||||
|  | ||||
| kbd { | ||||
| @@ -300,7 +307,9 @@ img.server-info-icon { | ||||
| } | ||||
|  | ||||
| .settings-card:hover { | ||||
|   box-shadow: 0 2px 5px 0 rgb(0 0 0 / 16%), 0 2px 0 0 rgb(0 0 0 / 12%); | ||||
|   box-shadow: | ||||
|     0 2px 5px 0 rgb(0 0 0 / 16%), | ||||
|     0 2px 0 0 rgb(0 0 0 / 12%); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
| @@ -475,10 +484,7 @@ input.toggle-round + label::after { | ||||
| input.toggle-round + label::before { | ||||
|   background-color: rgb(241 241 241 / 100%); | ||||
|   border-radius: 25px; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   left: 0; | ||||
|   bottom: 0; | ||||
|   inset: 0; | ||||
| } | ||||
|  | ||||
| input.toggle-round + label::after { | ||||
| @@ -490,10 +496,7 @@ input.toggle-round + label::after { | ||||
|  | ||||
| input.toggle-round:checked + label::before { | ||||
|   background-color: rgb(78 191 172 / 100%); | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   left: 0; | ||||
|   bottom: 0; | ||||
|   inset: 0; | ||||
| } | ||||
|  | ||||
| input.toggle-round:checked + label::after { | ||||
| @@ -651,7 +654,7 @@ i.open-network-button { | ||||
| } | ||||
|  | ||||
| /* responsive grid */ | ||||
| @media (min-width: 500px) and (max-width: 720px) { | ||||
| @media (width >= 500px) and (width <= 720px) { | ||||
|   #new-server-container { | ||||
|     padding-left: 0; | ||||
|     width: 60vw; | ||||
| @@ -663,7 +666,7 @@ i.open-network-button { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (max-width: 500px) { | ||||
| @media (width <= 500px) { | ||||
|   #new-server-container { | ||||
|     padding-left: 0; | ||||
|     width: 54%; | ||||
| @@ -674,7 +677,7 @@ i.open-network-button { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (max-width: 650px) { | ||||
| @media (width <= 650px) { | ||||
|   .selected-css-path, | ||||
|   .download-folder-path { | ||||
|     margin-right: 15px; | ||||
| @@ -689,7 +692,7 @@ i.open-network-button { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (max-width: 720px) { | ||||
| @media (width <= 720px) { | ||||
|   .modal-container { | ||||
|     width: 60vw; | ||||
|     padding: 40px; | ||||
| @@ -712,7 +715,7 @@ i.open-network-button { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (max-width: 600px) { | ||||
| @media (width <= 600px) { | ||||
|   .divider { | ||||
|     margin-left: 4%; | ||||
|   } | ||||
| @@ -724,7 +727,7 @@ i.open-network-button { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (max-width: 900px) { | ||||
| @media (width <= 900px) { | ||||
|   .settings-card { | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
| @@ -760,3 +763,9 @@ i.open-network-button { | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
| } | ||||
|  | ||||
| .settings-tagify-dropdown { | ||||
|   position: relative; | ||||
|   z-index: 9999; | ||||
|   height: 0; | ||||
| } | ||||
|   | ||||
| Before Width: | Height: | Size: 22 KiB | 
							
								
								
									
										8
									
								
								app/renderer/img/ic_loading.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;" width="150px" height="150px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"> | ||||
| <circle cx="50" cy="50" fill="none" stroke="#759ed4" stroke-width="10" r="42" stroke-dasharray="197.92033717615698 67.97344572538566" style="animation-play-state: running; animation-delay: 0s;"> | ||||
|   <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1" style="animation-play-state: running; animation-delay: 0s;"></animateTransform> | ||||
| </circle> | ||||
| <!-- Created with loading.io (https://loading.io/spinner/rolling/-bar-circle-curve-round-rotate) --> | ||||
| <!-- "The Rolling spinner is released under loading.io free License." (https://loading.io/license/#free-license) --> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1018 B | 
| @@ -1,6 +1,4 @@ | ||||
| import {clipboard} from "electron/common"; | ||||
| import {Buffer} from "node:buffer"; | ||||
| import crypto from "node:crypto"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer.js"; | ||||
|  | ||||
| // This helper is exposed via electron_bridge for use in the social | ||||
| // login flow. | ||||
| @@ -16,11 +14,11 @@ import crypto from "node:crypto"; | ||||
| // don’t leak anything from the user’s clipboard other than the token | ||||
| // intended for us. | ||||
|  | ||||
| export interface ClipboardDecrypter { | ||||
| export type ClipboardDecrypter = { | ||||
|   version: number; | ||||
|   key: Uint8Array; | ||||
|   pasted: Promise<string>; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export class ClipboardDecrypterImpl implements ClipboardDecrypter { | ||||
|   version: number; | ||||
| @@ -30,7 +28,8 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter { | ||||
|   constructor(_: number) { | ||||
|     // At this time, the only version is 1. | ||||
|     this.version = 1; | ||||
|     this.key = crypto.randomBytes(32); | ||||
|     const {key, sig} = ipcRenderer.sendSync("new-clipboard-key"); | ||||
|     this.key = key; | ||||
|     this.pasted = new Promise((resolve) => { | ||||
|       let interval: NodeJS.Timeout | null = null; | ||||
|       const startPolling = () => { | ||||
| @@ -38,7 +37,7 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter { | ||||
|           interval = setInterval(poll, 1000); | ||||
|         } | ||||
|  | ||||
|         poll(); | ||||
|         void poll(); | ||||
|       }; | ||||
|  | ||||
|       const stopPolling = () => { | ||||
| @@ -48,30 +47,9 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter { | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       const poll = () => { | ||||
|         let plaintext; | ||||
|  | ||||
|         try { | ||||
|           const data = Buffer.from(clipboard.readText(), "hex"); | ||||
|           const iv = data.slice(0, 12); | ||||
|           const ciphertext = data.slice(12, -16); | ||||
|           const authTag = data.slice(-16); | ||||
|           const decipher = crypto.createDecipheriv( | ||||
|             "aes-256-gcm", | ||||
|             this.key, | ||||
|             iv, | ||||
|             {authTagLength: 16}, | ||||
|           ); | ||||
|           decipher.setAuthTag(authTag); | ||||
|           plaintext = | ||||
|             decipher.update(ciphertext, undefined, "utf8") + | ||||
|             decipher.final("utf8"); | ||||
|         } catch { | ||||
|           // If the parsing or decryption failed in any way, | ||||
|           // the correct token hasn’t been copied yet; try | ||||
|           // again next time. | ||||
|           return; | ||||
|         } | ||||
|       const poll = async () => { | ||||
|         const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig); | ||||
|         if (plaintext === undefined) return; | ||||
|  | ||||
|         window.removeEventListener("focus", startPolling); | ||||
|         window.removeEventListener("blur", stopPolling); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import type {Html} from "../../../common/html"; | ||||
| import type {Html} from "../../../common/html.js"; | ||||
|  | ||||
| export function generateNodeFromHtml(html: Html): Element { | ||||
|   const wrapper = document.createElement("div"); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type {Event} from "electron/common"; | ||||
| import {clipboard} from "electron/common"; | ||||
| import type {WebContents} from "electron/main"; | ||||
| import type { | ||||
| @@ -8,7 +9,7 @@ import process from "node:process"; | ||||
|  | ||||
| import {Menu} from "@electron/remote"; | ||||
|  | ||||
| import * as t from "../../../common/translation-util"; | ||||
| import * as t from "../../../common/translation-util.js"; | ||||
|  | ||||
| export const contextMenu = ( | ||||
|   webContents: WebContents, | ||||
| @@ -137,7 +138,7 @@ export const contextMenu = ( | ||||
|     } | ||||
|   } | ||||
|   // Hide the invisible separators on Linux and Windows | ||||
|   // Electron has a bug which ignores visible: false parameter for separator menuitems. So we remove them here. | ||||
|   // Electron has a bug which ignores visible: false parameter for separator menu items. So we remove them here. | ||||
|   // https://github.com/electron/electron/issues/5869 | ||||
|   // https://github.com/electron/electron/issues/6906 | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import type {Html} from "../../../common/html"; | ||||
| import {html} from "../../../common/html"; | ||||
| import type {Html} from "../../../common/html.js"; | ||||
| import {html} from "../../../common/html.js"; | ||||
|  | ||||
| import {generateNodeFromHtml} from "./base"; | ||||
| import type {TabProps} from "./tab"; | ||||
| import Tab from "./tab"; | ||||
| import {generateNodeFromHtml} from "./base.js"; | ||||
| import type {TabProps} from "./tab.js"; | ||||
| import Tab from "./tab.js"; | ||||
|  | ||||
| export interface FunctionalTabProps extends TabProps { | ||||
| export type FunctionalTabProps = { | ||||
|   $view: Element; | ||||
| } | ||||
| } & TabProps; | ||||
|  | ||||
| export default class FunctionalTab extends Tab { | ||||
|   $view: Element; | ||||
| @@ -65,7 +65,7 @@ export default class FunctionalTab extends Tab { | ||||
|       this.$closeButton?.classList.remove("active"); | ||||
|     }); | ||||
|  | ||||
|     this.$closeButton?.addEventListener("click", (event: Event) => { | ||||
|     this.$closeButton?.addEventListener("click", (event) => { | ||||
|       this.props.onDestroy?.(); | ||||
|       event.stopPropagation(); | ||||
|     }); | ||||
|   | ||||
| @@ -1,21 +1,23 @@ | ||||
| import process from "node:process"; | ||||
|  | ||||
| import type {Html} from "../../../common/html"; | ||||
| import {html} from "../../../common/html"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import type {Html} from "../../../common/html.js"; | ||||
| import {html} from "../../../common/html.js"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| import {generateNodeFromHtml} from "./base"; | ||||
| import type {TabProps} from "./tab"; | ||||
| import Tab from "./tab"; | ||||
| import type WebView from "./webview"; | ||||
| import {generateNodeFromHtml} from "./base.js"; | ||||
| import type {TabProps} from "./tab.js"; | ||||
| import Tab from "./tab.js"; | ||||
| import type WebView from "./webview.js"; | ||||
|  | ||||
| export interface ServerTabProps extends TabProps { | ||||
| export type ServerTabProps = { | ||||
|   webview: Promise<WebView>; | ||||
| } | ||||
| } & TabProps; | ||||
|  | ||||
| export default class ServerTab extends Tab { | ||||
|   webview: Promise<WebView>; | ||||
|   $el: Element; | ||||
|   $name: Element; | ||||
|   $icon: HTMLImageElement; | ||||
|   $badge: Element; | ||||
|  | ||||
|   constructor({webview, ...props}: ServerTabProps) { | ||||
| @@ -25,6 +27,8 @@ export default class ServerTab extends Tab { | ||||
|     this.$el = generateNodeFromHtml(this.templateHtml()); | ||||
|     this.props.$root.append(this.$el); | ||||
|     this.registerListeners(); | ||||
|     this.$name = this.$el.querySelector(".server-tooltip")!; | ||||
|     this.$icon = this.$el.querySelector(".server-icons")!; | ||||
|     this.$badge = this.$el.querySelector(".server-tab-badge")!; | ||||
|   } | ||||
|  | ||||
| @@ -40,7 +44,7 @@ export default class ServerTab extends Tab { | ||||
|  | ||||
|   override async destroy(): Promise<void> { | ||||
|     await super.destroy(); | ||||
|     (await this.webview).$el.remove(); | ||||
|     (await this.webview).destroy(); | ||||
|   } | ||||
|  | ||||
|   templateHtml(): Html { | ||||
| @@ -58,6 +62,16 @@ export default class ServerTab extends Tab { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   setName(name: string): void { | ||||
|     this.props.name = name; | ||||
|     this.$name.textContent = name; | ||||
|   } | ||||
|  | ||||
|   setIcon(icon: string): void { | ||||
|     this.props.icon = icon; | ||||
|     this.$icon.src = icon; | ||||
|   } | ||||
|  | ||||
|   updateBadge(count: number): void { | ||||
|     this.$badge.textContent = count > 999 ? "1K+" : count.toString(); | ||||
|     this.$badge.classList.toggle("active", count > 0); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import type {TabRole} from "../../../common/types"; | ||||
| import type {TabRole} from "../../../common/types.js"; | ||||
|  | ||||
| export interface TabProps { | ||||
| export type TabProps = { | ||||
|   role: TabRole; | ||||
|   icon?: string; | ||||
|   name: string; | ||||
| @@ -12,15 +12,12 @@ export interface TabProps { | ||||
|   onHoverOut?: () => void; | ||||
|   materialIcon?: string; | ||||
|   onDestroy?: () => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export default abstract class Tab { | ||||
|   props: TabProps; | ||||
|   abstract $el: Element; | ||||
|  | ||||
|   constructor(props: TabProps) { | ||||
|     this.props = props; | ||||
|   } | ||||
|   constructor(readonly props: TabProps) {} | ||||
|  | ||||
|   registerListeners(): void { | ||||
|     this.$el.addEventListener("click", this.props.onClick); | ||||
|   | ||||
| @@ -1,25 +1,25 @@ | ||||
| import type {WebContents} from "electron/main"; | ||||
| import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import * as remote from "@electron/remote"; | ||||
| import {app, dialog} from "@electron/remote"; | ||||
|  | ||||
| import * as ConfigUtil from "../../../common/config-util"; | ||||
| import type {Html} from "../../../common/html"; | ||||
| import {html} from "../../../common/html"; | ||||
| import type {RendererMessage} from "../../../common/typed-ipc"; | ||||
| import type {TabRole} from "../../../common/types"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import * as SystemUtil from "../utils/system-util"; | ||||
| import * as ConfigUtil from "../../../common/config-util.js"; | ||||
| import type {Html} from "../../../common/html.js"; | ||||
| import {html} from "../../../common/html.js"; | ||||
| import type {RendererMessage} from "../../../common/typed-ipc.js"; | ||||
| import type {TabRole} from "../../../common/types.js"; | ||||
| import preloadCss from "../../css/preload.css?raw"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
| import * as SystemUtil from "../utils/system-util.js"; | ||||
|  | ||||
| import {generateNodeFromHtml} from "./base"; | ||||
| import {contextMenu} from "./context-menu"; | ||||
| import {generateNodeFromHtml} from "./base.js"; | ||||
| import {contextMenu} from "./context-menu.js"; | ||||
|  | ||||
| const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false); | ||||
|  | ||||
| interface WebViewProps { | ||||
| type WebViewProps = { | ||||
|   $root: Element; | ||||
|   rootWebContents: WebContents; | ||||
|   index: number; | ||||
| @@ -32,35 +32,46 @@ interface WebViewProps { | ||||
|   preload?: string; | ||||
|   onTitleChange: () => void; | ||||
|   hasPermission?: (origin: string, permission: string) => boolean; | ||||
| } | ||||
|   unsupportedMessage?: string; | ||||
| }; | ||||
|  | ||||
| export default class WebView { | ||||
|   static templateHtml(props: WebViewProps): Html { | ||||
|     return html` | ||||
|       <webview | ||||
|         data-tab-id="${props.tabIndex}" | ||||
|         src="${props.url}" | ||||
|         ${props.preload === undefined | ||||
|           ? html`` | ||||
|           : html`preload="${props.preload}"`} | ||||
|         partition="persist:webviewsession" | ||||
|         allowpopups | ||||
|       > | ||||
|       </webview> | ||||
|       <div class="webview-pane"> | ||||
|         <div | ||||
|           class="webview-unsupported" | ||||
|           ${props.unsupportedMessage === undefined ? html`hidden` : html``} | ||||
|         > | ||||
|           <span class="webview-unsupported-message" | ||||
|             >${props.unsupportedMessage ?? ""}</span | ||||
|           > | ||||
|           <span class="webview-unsupported-dismiss">×</span> | ||||
|         </div> | ||||
|         <webview | ||||
|           data-tab-id="${props.tabIndex}" | ||||
|           src="${props.url}" | ||||
|           ${props.preload === undefined | ||||
|             ? html`` | ||||
|             : html`preload="${props.preload}"`} | ||||
|           partition="persist:webviewsession" | ||||
|           allowpopups | ||||
|         > | ||||
|         </webview> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static async create(props: WebViewProps): Promise<WebView> { | ||||
|     const $element = generateNodeFromHtml( | ||||
|     const $pane = generateNodeFromHtml( | ||||
|       WebView.templateHtml(props), | ||||
|     ) as HTMLElement; | ||||
|     props.$root.append($element); | ||||
|     props.$root.append($pane); | ||||
|  | ||||
|     // Wait for did-navigate rather than did-attach to work around | ||||
|     // https://github.com/electron/electron/issues/31918 | ||||
|     const $webview: HTMLElement = $pane.querySelector(":scope > webview")!; | ||||
|     await new Promise<void>((resolve) => { | ||||
|       $element.addEventListener( | ||||
|         "did-navigate", | ||||
|       $webview.addEventListener( | ||||
|         "did-attach", | ||||
|         () => { | ||||
|           resolve(); | ||||
|         }, | ||||
| @@ -89,162 +100,60 @@ export default class WebView { | ||||
|       throw new TypeError("Failed to get WebContents ID"); | ||||
|     } | ||||
|  | ||||
|     return new WebView(props, $element, webContentsId); | ||||
|     return new WebView(props, $pane, $webview, webContentsId); | ||||
|   } | ||||
|  | ||||
|   props: WebViewProps; | ||||
|   zoomFactor: number; | ||||
|   badgeCount: number; | ||||
|   loading: boolean; | ||||
|   customCss: string | false | null; | ||||
|   $webviewsContainer: DOMTokenList; | ||||
|   $el: HTMLElement; | ||||
|   webContentsId: number; | ||||
|   badgeCount = 0; | ||||
|   loading = true; | ||||
|   private zoomFactor = 1; | ||||
|   private customCss: string | false | null; | ||||
|   private readonly $webviewsContainer: DOMTokenList; | ||||
|   private readonly $unsupported: HTMLElement; | ||||
|   private readonly $unsupportedMessage: HTMLElement; | ||||
|   private readonly $unsupportedDismiss: HTMLElement; | ||||
|   private unsupportedDismissed = false; | ||||
|  | ||||
|   private constructor( | ||||
|     props: WebViewProps, | ||||
|     $element: HTMLElement, | ||||
|     webContentsId: number, | ||||
|     readonly props: WebViewProps, | ||||
|     private readonly $pane: HTMLElement, | ||||
|     private readonly $webview: HTMLElement, | ||||
|     readonly webContentsId: number, | ||||
|   ) { | ||||
|     this.props = props; | ||||
|     this.zoomFactor = 1; | ||||
|     this.loading = true; | ||||
|     this.badgeCount = 0; | ||||
|     this.customCss = ConfigUtil.getConfigItem("customCSS", null); | ||||
|     this.$webviewsContainer = document.querySelector( | ||||
|       "#webviews-container", | ||||
|     )!.classList; | ||||
|     this.$el = $element; | ||||
|     this.webContentsId = webContentsId; | ||||
|     this.$unsupported = $pane.querySelector(".webview-unsupported")!; | ||||
|     this.$unsupportedMessage = $pane.querySelector( | ||||
|       ".webview-unsupported-message", | ||||
|     )!; | ||||
|     this.$unsupportedDismiss = $pane.querySelector( | ||||
|       ".webview-unsupported-dismiss", | ||||
|     )!; | ||||
|  | ||||
|     this.registerListeners(); | ||||
|   } | ||||
|  | ||||
|   destroy(): void { | ||||
|     this.$pane.remove(); | ||||
|   } | ||||
|  | ||||
|   getWebContents(): WebContents { | ||||
|     return remote.webContents.fromId(this.webContentsId); | ||||
|   } | ||||
|  | ||||
|   registerListeners(): void { | ||||
|     const webContents = this.getWebContents(); | ||||
|  | ||||
|     if (shouldSilentWebview) { | ||||
|       webContents.setAudioMuted(true); | ||||
|     } | ||||
|  | ||||
|     webContents.on("page-title-updated", (_event, title) => { | ||||
|       this.badgeCount = this.getBadgeCount(title); | ||||
|       this.props.onTitleChange(); | ||||
|     }); | ||||
|  | ||||
|     this.$el.addEventListener("did-navigate-in-page", () => { | ||||
|       this.canGoBackButton(); | ||||
|     }); | ||||
|  | ||||
|     this.$el.addEventListener("did-navigate", () => { | ||||
|       this.canGoBackButton(); | ||||
|     }); | ||||
|  | ||||
|     webContents.on("page-favicon-updated", (_event, favicons) => { | ||||
|       // This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like | ||||
|       // https://chat.zulip.org/static/images/favicon/favicon-pms.png | ||||
|       if ( | ||||
|         favicons[0].indexOf("favicon-pms") > 0 && | ||||
|         process.platform === "darwin" | ||||
|       ) { | ||||
|         // This api is only supported on macOS | ||||
|         app.dock.setBadge("●"); | ||||
|         // Bounce the dock | ||||
|         if (ConfigUtil.getConfigItem("dockBouncing", true)) { | ||||
|           app.dock.bounce(); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     webContents.addListener("context-menu", (event, menuParameters) => { | ||||
|       contextMenu(webContents, event, menuParameters); | ||||
|     }); | ||||
|  | ||||
|     this.$el.addEventListener("dom-ready", () => { | ||||
|       this.loading = false; | ||||
|       this.props.switchLoading(false, this.props.url); | ||||
|       this.show(); | ||||
|     }); | ||||
|  | ||||
|     webContents.on("did-fail-load", (_event, _errorCode, errorDescription) => { | ||||
|       const hasConnectivityError = | ||||
|         SystemUtil.connectivityError.includes(errorDescription); | ||||
|       if (hasConnectivityError) { | ||||
|         console.error("error", errorDescription); | ||||
|         if (!this.props.url.includes("network.html")) { | ||||
|           this.props.onNetworkError(this.props.index); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     this.$el.addEventListener("did-start-loading", () => { | ||||
|       this.props.switchLoading(true, this.props.url); | ||||
|     }); | ||||
|  | ||||
|     this.$el.addEventListener("did-stop-loading", () => { | ||||
|       this.props.switchLoading(false, this.props.url); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBadgeCount(title: string): number { | ||||
|     const messageCountInTitle = /^\((\d+)\)/.exec(title); | ||||
|     return messageCountInTitle ? Number(messageCountInTitle[1]) : 0; | ||||
|     return remote.webContents.fromId(this.webContentsId)!; | ||||
|   } | ||||
|  | ||||
|   showNotificationSettings(): void { | ||||
|     this.send("show-notification-settings"); | ||||
|   } | ||||
|  | ||||
|   show(): void { | ||||
|     // Do not show WebView if another tab was selected and this tab should be in background. | ||||
|     if (!this.props.isActive()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // To show or hide the loading indicator in the the active tab | ||||
|     this.$webviewsContainer.toggle("loaded", !this.loading); | ||||
|  | ||||
|     this.$el.classList.add("active"); | ||||
|     this.focus(); | ||||
|     this.props.onTitleChange(); | ||||
|     // Injecting preload css in webview to override some css rules | ||||
|     (async () => | ||||
|       this.getWebContents().insertCSS( | ||||
|         fs.readFileSync(path.join(__dirname, "/../../css/preload.css"), "utf8"), | ||||
|       ))(); | ||||
|  | ||||
|     // Get customCSS again from config util to avoid warning user again | ||||
|     const customCss = ConfigUtil.getConfigItem("customCSS", null); | ||||
|     this.customCss = customCss; | ||||
|     if (customCss) { | ||||
|       if (!fs.existsSync(customCss)) { | ||||
|         this.customCss = null; | ||||
|         ConfigUtil.setConfigItem("customCSS", null); | ||||
|  | ||||
|         const errorMessage = "The custom css previously set is deleted!"; | ||||
|         dialog.showErrorBox("custom css file deleted!", errorMessage); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       (async () => | ||||
|         this.getWebContents().insertCSS( | ||||
|           fs.readFileSync(path.resolve(__dirname, customCss), "utf8"), | ||||
|         ))(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   focus(): void { | ||||
|     this.$el.focus(); | ||||
|     this.$webview.focus(); | ||||
|     // Work around https://github.com/electron/electron/issues/31918 | ||||
|     this.$el.shadowRoot?.querySelector("iframe")?.focus(); | ||||
|     this.$webview.shadowRoot?.querySelector("iframe")?.focus(); | ||||
|   } | ||||
|  | ||||
|   hide(): void { | ||||
|     this.$el.classList.remove("active"); | ||||
|     this.$pane.classList.remove("active"); | ||||
|   } | ||||
|  | ||||
|   load(): void { | ||||
| @@ -307,10 +216,125 @@ export default class WebView { | ||||
|     this.getWebContents().reload(); | ||||
|   } | ||||
|  | ||||
|   setUnsupportedMessage(unsupportedMessage: string | undefined) { | ||||
|     this.$unsupported.hidden = | ||||
|       unsupportedMessage === undefined || this.unsupportedDismissed; | ||||
|     this.$unsupportedMessage.textContent = unsupportedMessage ?? ""; | ||||
|   } | ||||
|  | ||||
|   send<Channel extends keyof RendererMessage>( | ||||
|     channel: Channel, | ||||
|     ...args: Parameters<RendererMessage[Channel]> | ||||
|   ): void { | ||||
|     ipcRenderer.sendTo(this.webContentsId, channel, ...args); | ||||
|     ipcRenderer.send("forward-to", this.webContentsId, channel, ...args); | ||||
|   } | ||||
|  | ||||
|   private registerListeners(): void { | ||||
|     const webContents = this.getWebContents(); | ||||
|  | ||||
|     if (shouldSilentWebview) { | ||||
|       webContents.setAudioMuted(true); | ||||
|     } | ||||
|  | ||||
|     webContents.on("page-title-updated", (_event, title) => { | ||||
|       this.badgeCount = this.getBadgeCount(title); | ||||
|       this.props.onTitleChange(); | ||||
|     }); | ||||
|  | ||||
|     this.$webview.addEventListener("did-navigate-in-page", () => { | ||||
|       this.canGoBackButton(); | ||||
|     }); | ||||
|  | ||||
|     this.$webview.addEventListener("did-navigate", () => { | ||||
|       this.canGoBackButton(); | ||||
|     }); | ||||
|  | ||||
|     webContents.on("page-favicon-updated", (_event, favicons) => { | ||||
|       // This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like | ||||
|       // https://chat.zulip.org/static/images/favicon/favicon-pms.png | ||||
|       if ( | ||||
|         favicons[0].indexOf("favicon-pms") > 0 && | ||||
|         process.platform === "darwin" | ||||
|       ) { | ||||
|         // This api is only supported on macOS | ||||
|         app.dock.setBadge("●"); | ||||
|         // Bounce the dock | ||||
|         if (ConfigUtil.getConfigItem("dockBouncing", true)) { | ||||
|           app.dock.bounce(); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     webContents.addListener("context-menu", (event, menuParameters) => { | ||||
|       contextMenu(webContents, event, menuParameters); | ||||
|     }); | ||||
|  | ||||
|     this.$webview.addEventListener("dom-ready", () => { | ||||
|       this.loading = false; | ||||
|       this.props.switchLoading(false, this.props.url); | ||||
|       this.show(); | ||||
|     }); | ||||
|  | ||||
|     webContents.on("did-fail-load", (_event, _errorCode, errorDescription) => { | ||||
|       const hasConnectivityError = | ||||
|         SystemUtil.connectivityError.includes(errorDescription); | ||||
|       if (hasConnectivityError) { | ||||
|         console.error("error", errorDescription); | ||||
|         if (!this.props.url.includes("network.html")) { | ||||
|           this.props.onNetworkError(this.props.index); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     this.$webview.addEventListener("did-start-loading", () => { | ||||
|       this.props.switchLoading(true, this.props.url); | ||||
|     }); | ||||
|  | ||||
|     this.$webview.addEventListener("did-stop-loading", () => { | ||||
|       this.props.switchLoading(false, this.props.url); | ||||
|     }); | ||||
|  | ||||
|     this.$unsupportedDismiss.addEventListener("click", () => { | ||||
|       this.unsupportedDismissed = true; | ||||
|       this.$unsupported.hidden = true; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private getBadgeCount(title: string): number { | ||||
|     const messageCountInTitle = /^\((\d+)\)/.exec(title); | ||||
|     return messageCountInTitle ? Number(messageCountInTitle[1]) : 0; | ||||
|   } | ||||
|  | ||||
|   private show(): void { | ||||
|     // Do not show WebView if another tab was selected and this tab should be in background. | ||||
|     if (!this.props.isActive()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // To show or hide the loading indicator in the active tab | ||||
|     this.$webviewsContainer.toggle("loaded", !this.loading); | ||||
|  | ||||
|     this.$pane.classList.add("active"); | ||||
|     this.focus(); | ||||
|     this.props.onTitleChange(); | ||||
|     // Injecting preload css in webview to override some css rules | ||||
|     (async () => this.getWebContents().insertCSS(preloadCss))(); | ||||
|  | ||||
|     // Get customCSS again from config util to avoid warning user again | ||||
|     const customCss = ConfigUtil.getConfigItem("customCSS", null); | ||||
|     this.customCss = customCss; | ||||
|     if (customCss) { | ||||
|       if (!fs.existsSync(customCss)) { | ||||
|         this.customCss = null; | ||||
|         ConfigUtil.setConfigItem("customCSS", null); | ||||
|  | ||||
|         const errorMessage = "The custom css previously set is deleted!"; | ||||
|         dialog.showErrorBox("custom css file deleted!", errorMessage); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       (async () => | ||||
|         this.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| import {EventEmitter} from "node:events"; | ||||
|  | ||||
| import type {ClipboardDecrypter} from "./clipboard-decrypter"; | ||||
| import {ClipboardDecrypterImpl} from "./clipboard-decrypter"; | ||||
| import type {NotificationData} from "./notification"; | ||||
| import {newNotification} from "./notification"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer"; | ||||
| import type {ClipboardDecrypter} from "./clipboard-decrypter.js"; | ||||
| import {ClipboardDecrypterImpl} from "./clipboard-decrypter.js"; | ||||
| import type {NotificationData} from "./notification/index.js"; | ||||
| import {newNotification} from "./notification/index.js"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer.js"; | ||||
|  | ||||
| type ListenerType = (...args: any[]) => void; | ||||
|  | ||||
| export interface ElectronBridge { | ||||
| /* eslint-disable @typescript-eslint/naming-convention */ | ||||
| export type ElectronBridge = { | ||||
|   send_event: (eventName: string | symbol, ...args: unknown[]) => boolean; | ||||
|   on_event: (eventName: string, listener: ListenerType) => void; | ||||
|   new_notification: ( | ||||
| @@ -21,7 +22,8 @@ export interface ElectronBridge { | ||||
|   get_send_notification_reply_message_supported: () => boolean; | ||||
|   set_send_notification_reply_message_supported: (value: boolean) => void; | ||||
|   decrypt_clipboard: (version: number) => ClipboardDecrypter; | ||||
| } | ||||
| }; | ||||
| /* eslint-enable @typescript-eslint/naming-convention */ | ||||
|  | ||||
| let notificationReplySupported = false; | ||||
| // Indicates if the user is idle or not | ||||
| @@ -29,7 +31,7 @@ let idle = false; | ||||
| // Indicates the time at which user was last active | ||||
| let lastActive = Date.now(); | ||||
|  | ||||
| export const bridgeEvents = new EventEmitter(); | ||||
| export const bridgeEvents = new EventEmitter(); // eslint-disable-line unicorn/prefer-event-target | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/naming-convention */ | ||||
| const electron_bridge: ElectronBridge = { | ||||
| @@ -105,7 +107,7 @@ ipcRenderer.on("set-idle", () => { | ||||
|  | ||||
| // This follows node's idiomatic implementation of event | ||||
| // emitters to make event handling more simpler instead of using | ||||
| // functions zulip side will emit event using ElectronBrigde.send_event | ||||
| // functions zulip side will emit event using ElectronBridge.send_event | ||||
| // which is alias of .emit and on this side we can handle the data by adding | ||||
| // a listener for the event. | ||||
| export default electron_bridge; | ||||
|   | ||||
| @@ -1,113 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| type ElectronBridge = import("./electron-bridge").ElectronBridge; | ||||
|  | ||||
| interface CompatElectronBridge extends ElectronBridge { | ||||
|   readonly idle_on_system: boolean; | ||||
|   readonly last_active_on_system: number; | ||||
|   send_notification_reply_message_supported: boolean; | ||||
| } | ||||
|  | ||||
| (() => { | ||||
|   const zulipWindow = window as typeof window & { | ||||
|     electron_bridge: CompatElectronBridge; | ||||
|     raw_electron_bridge: ElectronBridge; | ||||
|   }; | ||||
|  | ||||
|   /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|   const electron_bridge: CompatElectronBridge = { | ||||
|     ...zulipWindow.raw_electron_bridge, | ||||
|  | ||||
|     get idle_on_system(): boolean { | ||||
|       return this.get_idle_on_system(); | ||||
|     }, | ||||
|  | ||||
|     get last_active_on_system(): number { | ||||
|       return this.get_last_active_on_system(); | ||||
|     }, | ||||
|  | ||||
|     get send_notification_reply_message_supported(): boolean { | ||||
|       return this.get_send_notification_reply_message_supported(); | ||||
|     }, | ||||
|  | ||||
|     set send_notification_reply_message_supported(value: boolean) { | ||||
|       this.set_send_notification_reply_message_supported(value); | ||||
|     }, | ||||
|   }; | ||||
|   /* eslint-enable @typescript-eslint/naming-convention */ | ||||
|  | ||||
|   zulipWindow.electron_bridge = electron_bridge; | ||||
|  | ||||
|   function attributeListener<T extends EventTarget>( | ||||
|     type: string, | ||||
|   ): PropertyDescriptor { | ||||
|     const handlers = new WeakMap<T, (event: Event) => unknown>(); | ||||
|  | ||||
|     function listener(this: T, event: Event): void { | ||||
|       if (handlers.get(this)!.call(this, event) === false) { | ||||
|         event.preventDefault(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       configurable: true, | ||||
|       enumerable: true, | ||||
|       get(this: T) { | ||||
|         return handlers.get(this); | ||||
|       }, | ||||
|       set(this: T, value: unknown) { | ||||
|         if (typeof value === "function") { | ||||
|           if (!handlers.has(this)) { | ||||
|             this.addEventListener(type, listener); | ||||
|           } | ||||
|  | ||||
|           handlers.set(this, value as (event: Event) => unknown); | ||||
|         } else if (handlers.has(this)) { | ||||
|           this.removeEventListener(type, listener); | ||||
|           handlers.delete(this); | ||||
|         } | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
|   const NativeNotification = Notification; | ||||
|  | ||||
|   class InjectedNotification extends EventTarget { | ||||
|     static get permission(): NotificationPermission { | ||||
|       return NativeNotification.permission; | ||||
|     } | ||||
|  | ||||
|     static async requestPermission( | ||||
|       callback?: NotificationPermissionCallback, | ||||
|     ): Promise<NotificationPermission> { | ||||
|       if (callback) { | ||||
|         callback(await Promise.resolve(NativeNotification.permission)); | ||||
|       } | ||||
|  | ||||
|       return NativeNotification.permission; | ||||
|     } | ||||
|  | ||||
|     constructor(title: string, options: NotificationOptions = {}) { | ||||
|       super(); | ||||
|       Object.assign( | ||||
|         this, | ||||
|         electron_bridge.new_notification( | ||||
|           title, | ||||
|           options, | ||||
|           (type: string, eventInit: EventInit) => | ||||
|             this.dispatchEvent(new Event(type, eventInit)), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Object.defineProperties(InjectedNotification.prototype, { | ||||
|     onclick: attributeListener("click"), | ||||
|     onclose: attributeListener("close"), | ||||
|     onerror: attributeListener("error"), | ||||
|     onshow: attributeListener("show"), | ||||
|   }); | ||||
|  | ||||
|   window.Notification = InjectedNotification as unknown as typeof Notification; | ||||
| })(); | ||||
| @@ -1,30 +1,33 @@ | ||||
| import {clipboard} from "electron/common"; | ||||
| import path from "node:path"; | ||||
| import process from "node:process"; | ||||
| import url from "node:url"; | ||||
|  | ||||
| import {Menu, app, dialog, session} from "@electron/remote"; | ||||
| import * as remote from "@electron/remote"; | ||||
| import * as Sentry from "@sentry/electron"; | ||||
| import * as Sentry from "@sentry/electron/renderer"; | ||||
|  | ||||
| import type {Config} from "../../common/config-util"; | ||||
| import * as ConfigUtil from "../../common/config-util"; | ||||
| import * as DNDUtil from "../../common/dnd-util"; | ||||
| import type {DndSettings} from "../../common/dnd-util"; | ||||
| import * as EnterpriseUtil from "../../common/enterprise-util"; | ||||
| import * as LinkUtil from "../../common/link-util"; | ||||
| import Logger from "../../common/logger-util"; | ||||
| import * as Messages from "../../common/messages"; | ||||
| import type {NavItem, ServerConf, TabData} from "../../common/types"; | ||||
| import type {Config} from "../../common/config-util.js"; | ||||
| import * as ConfigUtil from "../../common/config-util.js"; | ||||
| import * as DNDUtil from "../../common/dnd-util.js"; | ||||
| import type {DndSettings} from "../../common/dnd-util.js"; | ||||
| import * as EnterpriseUtil from "../../common/enterprise-util.js"; | ||||
| import * as LinkUtil from "../../common/link-util.js"; | ||||
| import Logger from "../../common/logger-util.js"; | ||||
| import * as Messages from "../../common/messages.js"; | ||||
| import {bundlePath, bundleUrl} from "../../common/paths.js"; | ||||
| import type {NavItem, ServerConf, TabData} from "../../common/types.js"; | ||||
| import defaultIcon from "../img/icon.png"; | ||||
|  | ||||
| import FunctionalTab from "./components/functional-tab"; | ||||
| import ServerTab from "./components/server-tab"; | ||||
| import WebView from "./components/webview"; | ||||
| import {AboutView} from "./pages/about"; | ||||
| import {PreferenceView} from "./pages/preference/preference"; | ||||
| import {initializeTray} from "./tray"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer"; | ||||
| import * as DomainUtil from "./utils/domain-util"; | ||||
| import ReconnectUtil from "./utils/reconnect-util"; | ||||
| import FunctionalTab from "./components/functional-tab.js"; | ||||
| import ServerTab from "./components/server-tab.js"; | ||||
| import WebView from "./components/webview.js"; | ||||
| import {AboutView} from "./pages/about.js"; | ||||
| import {PreferenceView} from "./pages/preference/preference.js"; | ||||
| import {initializeTray} from "./tray.js"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer.js"; | ||||
| import * as DomainUtil from "./utils/domain-util.js"; | ||||
| import ReconnectUtil from "./utils/reconnect-util.js"; | ||||
|  | ||||
| Sentry.init({}); | ||||
|  | ||||
| @@ -44,12 +47,13 @@ const logger = new Logger({ | ||||
|   file: "errors.log", | ||||
| }); | ||||
|  | ||||
| const rendererDirectory = path.resolve(__dirname, ".."); | ||||
| type ServerOrFunctionalTab = ServerTab | FunctionalTab; | ||||
|  | ||||
| const rootWebContents = remote.getCurrentWebContents(); | ||||
|  | ||||
| const dingSound = new Audio("../resources/sounds/ding.ogg"); | ||||
| const dingSound = new Audio( | ||||
|   new URL("resources/sounds/ding.ogg", bundleUrl).href, | ||||
| ); | ||||
|  | ||||
| export class ServerManagerView { | ||||
|   $addServerButton: HTMLButtonElement; | ||||
| @@ -152,12 +156,12 @@ export class ServerManagerView { | ||||
|       ConfigUtil.getConfigItem("useSystemProxy", false) | ||||
|         ? {mode: "system"} | ||||
|         : ConfigUtil.getConfigItem("useManualProxy", false) | ||||
|         ? { | ||||
|             pacScript: ConfigUtil.getConfigItem("proxyPAC", ""), | ||||
|             proxyRules: ConfigUtil.getConfigItem("proxyRules", ""), | ||||
|             proxyBypassRules: ConfigUtil.getConfigItem("proxyBypass", ""), | ||||
|           } | ||||
|         : {mode: "direct"}, | ||||
|           ? { | ||||
|               pacScript: ConfigUtil.getConfigItem("proxyPAC", ""), | ||||
|               proxyRules: ConfigUtil.getConfigItem("proxyRules", ""), | ||||
|               proxyBypassRules: ConfigUtil.getConfigItem("proxyBypass", ""), | ||||
|             } | ||||
|           : {mode: "direct"}, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -319,7 +323,15 @@ export class ServerManagerView { | ||||
|     const servers = DomainUtil.getDomains(); | ||||
|     if (servers.length > 0) { | ||||
|       for (const [i, server] of servers.entries()) { | ||||
|         this.initServer(server, i); | ||||
|         const tab = this.initServer(server, i); | ||||
|         (async () => { | ||||
|           const serverConf = await DomainUtil.updateSavedServer(server.url, i); | ||||
|           tab.setName(serverConf.alias); | ||||
|           tab.setIcon(DomainUtil.iconAsUrl(serverConf.icon)); | ||||
|           (await tab.webview).setUnsupportedMessage( | ||||
|             DomainUtil.getUnsupportedMessage(serverConf), | ||||
|           ); | ||||
|         })(); | ||||
|       } | ||||
|  | ||||
|       // Open last active tab | ||||
| @@ -328,11 +340,7 @@ export class ServerManagerView { | ||||
|         lastActiveTab = 0; | ||||
|       } | ||||
|  | ||||
|       // `checkDomain()` and `webview.load()` for lastActiveTab before the others | ||||
|       await DomainUtil.updateSavedServer( | ||||
|         servers[lastActiveTab].url, | ||||
|         lastActiveTab, | ||||
|       ); | ||||
|       // `webview.load()` for lastActiveTab before the others | ||||
|       await this.activateTab(lastActiveTab); | ||||
|       await Promise.all( | ||||
|         servers.map(async (server, i) => { | ||||
| @@ -342,7 +350,6 @@ export class ServerManagerView { | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|           await DomainUtil.updateSavedServer(server.url, i); | ||||
|           const tab = this.tabs[i]; | ||||
|           if (tab instanceof ServerTab) (await tab.webview).load(); | ||||
|         }), | ||||
| @@ -357,51 +364,54 @@ export class ServerManagerView { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   initServer(server: ServerConf, index: number): void { | ||||
|   initServer(server: ServerConf, index: number): ServerTab { | ||||
|     const tabIndex = this.getTabIndex(); | ||||
|     this.tabs.push( | ||||
|       new ServerTab({ | ||||
|         role: "server", | ||||
|         icon: server.icon, | ||||
|         name: server.alias, | ||||
|         $root: this.$tabsContainer, | ||||
|         onClick: this.activateLastTab.bind(this, index), | ||||
|     const tab = new ServerTab({ | ||||
|       role: "server", | ||||
|       icon: DomainUtil.iconAsUrl(server.icon), | ||||
|       name: server.alias, | ||||
|       $root: this.$tabsContainer, | ||||
|       onClick: this.activateLastTab.bind(this, index), | ||||
|       index, | ||||
|       tabIndex, | ||||
|       onHover: this.onHover.bind(this, index), | ||||
|       onHoverOut: this.onHoverOut.bind(this, index), | ||||
|       webview: WebView.create({ | ||||
|         $root: this.$webviewsContainer, | ||||
|         rootWebContents, | ||||
|         index, | ||||
|         tabIndex, | ||||
|         onHover: this.onHover.bind(this, index), | ||||
|         onHoverOut: this.onHoverOut.bind(this, index), | ||||
|         webview: WebView.create({ | ||||
|           $root: this.$webviewsContainer, | ||||
|           rootWebContents, | ||||
|           index, | ||||
|           tabIndex, | ||||
|           url: server.url, | ||||
|           role: "server", | ||||
|           hasPermission: (origin: string, permission: string) => | ||||
|             origin === server.url && permission === "notifications", | ||||
|           isActive: () => index === this.activeTabIndex, | ||||
|           switchLoading: async (loading: boolean, url: string) => { | ||||
|             if (loading) { | ||||
|               this.loading.add(url); | ||||
|             } else { | ||||
|               this.loading.delete(url); | ||||
|             } | ||||
|         url: server.url, | ||||
|         role: "server", | ||||
|         hasPermission: (origin: string, permission: string) => | ||||
|           origin === server.url && | ||||
|           permission === "notifications" && | ||||
|           ConfigUtil.getConfigItem("showNotification", true), | ||||
|         isActive: () => index === this.activeTabIndex, | ||||
|         switchLoading: async (loading: boolean, url: string) => { | ||||
|           if (loading) { | ||||
|             this.loading.add(url); | ||||
|           } else { | ||||
|             this.loading.delete(url); | ||||
|           } | ||||
|  | ||||
|             const tab = this.tabs[this.activeTabIndex]; | ||||
|             this.showLoading( | ||||
|               tab instanceof ServerTab && | ||||
|                 this.loading.has((await tab.webview).props.url), | ||||
|             ); | ||||
|           }, | ||||
|           onNetworkError: async (index: number) => { | ||||
|             await this.openNetworkTroubleshooting(index); | ||||
|           }, | ||||
|           onTitleChange: this.updateBadge.bind(this), | ||||
|           preload: "js/preload.js", | ||||
|         }), | ||||
|           const tab = this.tabs[this.activeTabIndex]; | ||||
|           this.showLoading( | ||||
|             tab instanceof ServerTab && | ||||
|               this.loading.has((await tab.webview).props.url), | ||||
|           ); | ||||
|         }, | ||||
|         onNetworkError: async (index: number) => { | ||||
|           await this.openNetworkTroubleshooting(index); | ||||
|         }, | ||||
|         onTitleChange: this.updateBadge.bind(this), | ||||
|         preload: url.pathToFileURL(path.join(bundlePath, "preload.js")).href, | ||||
|         unsupportedMessage: DomainUtil.getUnsupportedMessage(server), | ||||
|       }), | ||||
|     ); | ||||
|     }); | ||||
|     this.tabs.push(tab); | ||||
|     this.loading.add(server.url); | ||||
|     return tab; | ||||
|   } | ||||
|  | ||||
|   initActions(): void { | ||||
| @@ -415,7 +425,7 @@ export class ServerManagerView { | ||||
|       document.querySelectorAll(".server-icons"); | ||||
|     for (const [index, $serverImg] of $serverImgs.entries()) { | ||||
|       this.addContextMenu($serverImg, index); | ||||
|       if ($serverImg.src.includes("img/icon.png")) { | ||||
|       if ($serverImg.src === defaultIcon) { | ||||
|         this.displayInitialCharLogo($serverImg, index); | ||||
|       } | ||||
|  | ||||
| @@ -489,7 +499,7 @@ export class ServerManagerView { | ||||
|     const realmName = $webview.getAttribute("name"); | ||||
|  | ||||
|     if (realmName === null) { | ||||
|       $img.src = "/img/icon.png"; | ||||
|       $img.src = defaultIcon; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -543,7 +553,7 @@ export class ServerManagerView { | ||||
|   async openFunctionalTab(tabProps: { | ||||
|     name: string; | ||||
|     materialIcon: string; | ||||
|     makeView: () => Element; | ||||
|     makeView: () => Promise<Element>; | ||||
|     destroyView: () => void; | ||||
|   }): Promise<void> { | ||||
|     if (this.functionalTabs.has(tabProps.name)) { | ||||
| @@ -555,7 +565,7 @@ export class ServerManagerView { | ||||
|     this.functionalTabs.set(tabProps.name, index); | ||||
|  | ||||
|     const tabIndex = this.getTabIndex(); | ||||
|     const $view = tabProps.makeView(); | ||||
|     const $view = await tabProps.makeView(); | ||||
|     this.$webviewsContainer.append($view); | ||||
|  | ||||
|     this.tabs.push( | ||||
| @@ -586,8 +596,8 @@ export class ServerManagerView { | ||||
|     await this.openFunctionalTab({ | ||||
|       name: "Settings", | ||||
|       materialIcon: "settings", | ||||
|       makeView: () => { | ||||
|         this.preferenceView = new PreferenceView(); | ||||
|       makeView: async () => { | ||||
|         this.preferenceView = await PreferenceView.create(); | ||||
|         this.preferenceView.$view.classList.add("functional-view"); | ||||
|         return this.preferenceView.$view; | ||||
|       }, | ||||
| @@ -605,8 +615,8 @@ export class ServerManagerView { | ||||
|     await this.openFunctionalTab({ | ||||
|       name: "About", | ||||
|       materialIcon: "sentiment_very_satisfied", | ||||
|       makeView() { | ||||
|         aboutView = new AboutView(); | ||||
|       async makeView() { | ||||
|         aboutView = await AboutView.create(); | ||||
|         aboutView.$view.classList.add("functional-view"); | ||||
|         return aboutView.$view; | ||||
|       }, | ||||
| @@ -624,7 +634,7 @@ export class ServerManagerView { | ||||
|     reconnectUtil.pollInternetAndReload(); | ||||
|     await webview | ||||
|       .getWebContents() | ||||
|       .loadURL(`file://${rendererDirectory}/network.html`); | ||||
|       .loadURL(new URL("app/renderer/network.html", bundleUrl).href); | ||||
|   } | ||||
|  | ||||
|   async activateLastTab(index: number): Promise<void> { | ||||
| @@ -914,7 +924,7 @@ export class ServerManagerView { | ||||
|     ipcRenderer.on( | ||||
|       "permission-request", | ||||
|       async ( | ||||
|         event: Event, | ||||
|         event, | ||||
|         { | ||||
|           webContentsId, | ||||
|           origin, | ||||
| @@ -963,10 +973,7 @@ export class ServerManagerView { | ||||
|       await LinkUtil.openBrowser(new URL("https://zulip.com/help/")); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "reload-viewer", | ||||
|       this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index), | ||||
|     ); | ||||
|     ipcRenderer.on("reload-viewer", this.reloadView.bind(this)); | ||||
|  | ||||
|     ipcRenderer.on("reload-current-viewer", this.reloadCurrentView.bind(this)); | ||||
|  | ||||
| @@ -974,7 +981,7 @@ export class ServerManagerView { | ||||
|       ipcRenderer.send("reload-full-app"); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on("switch-server-tab", async (event: Event, index: number) => { | ||||
|     ipcRenderer.on("switch-server-tab", async (event, index: number) => { | ||||
|       await this.activateLastTab(index); | ||||
|     }); | ||||
|  | ||||
| @@ -982,7 +989,7 @@ export class ServerManagerView { | ||||
|       await this.openSettings("AddServer"); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on("reload-proxy", async (event: Event, showAlert: boolean) => { | ||||
|     ipcRenderer.on("reload-proxy", async (event, showAlert: boolean) => { | ||||
|       await this.loadProxy(); | ||||
|       if (showAlert) { | ||||
|         await dialog.showMessageBox({ | ||||
| @@ -993,12 +1000,12 @@ export class ServerManagerView { | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on("toggle-sidebar", async (event: Event, show: boolean) => { | ||||
|     ipcRenderer.on("toggle-sidebar", async (event, show: boolean) => { | ||||
|       // Toggle the left sidebar | ||||
|       this.toggleSidebar(show); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on("toggle-silent", async (event: Event, state: boolean) => | ||||
|     ipcRenderer.on("toggle-silent", async (event, state: boolean) => | ||||
|       Promise.all( | ||||
|         this.tabs.map(async (tab) => { | ||||
|           if (tab instanceof ServerTab) | ||||
| @@ -1009,7 +1016,7 @@ export class ServerManagerView { | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "toggle-autohide-menubar", | ||||
|       async (event: Event, autoHideMenubar: boolean, updateMenu: boolean) => { | ||||
|       async (event, autoHideMenubar: boolean, updateMenu: boolean) => { | ||||
|         if (updateMenu) { | ||||
|           ipcRenderer.send("update-menu", { | ||||
|             tabs: this.tabsForIpc, | ||||
| @@ -1021,11 +1028,7 @@ export class ServerManagerView { | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "toggle-dnd", | ||||
|       async ( | ||||
|         event: Event, | ||||
|         state: boolean, | ||||
|         newSettings: Partial<DndSettings>, | ||||
|       ) => { | ||||
|       async (event, state: boolean, newSettings: Partial<DndSettings>) => { | ||||
|         this.toggleDndButton(state); | ||||
|         ipcRenderer.send( | ||||
|           "forward-message", | ||||
| @@ -1037,16 +1040,11 @@ export class ServerManagerView { | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "update-realm-name", | ||||
|       (event: Event, serverURL: string, realmName: string) => { | ||||
|       (event, serverURL: string, realmName: string) => { | ||||
|         for (const [index, domain] of DomainUtil.getDomains().entries()) { | ||||
|           if (domain.url.includes(serverURL)) { | ||||
|             const serverTooltipSelector = ".tab .server-tooltip"; | ||||
|             const serverTooltips = document.querySelectorAll( | ||||
|               serverTooltipSelector, | ||||
|             ); | ||||
|             serverTooltips[index].textContent = realmName; | ||||
|             this.tabs[index].props.name = realmName; | ||||
|  | ||||
|           if (domain.url === serverURL) { | ||||
|             const tab = this.tabs[index]; | ||||
|             if (tab instanceof ServerTab) tab.setName(realmName); | ||||
|             domain.alias = realmName; | ||||
|             DomainUtil.updateDomain(index, domain); | ||||
|             // Update the realm name also on the Window menu | ||||
| @@ -1061,18 +1059,15 @@ export class ServerManagerView { | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "update-realm-icon", | ||||
|       async (event: Event, serverURL: string, iconURL: string) => { | ||||
|       async (event, serverURL: string, iconURL: string) => { | ||||
|         await Promise.all( | ||||
|           DomainUtil.getDomains().map(async (domain, index) => { | ||||
|             if (domain.url.includes(serverURL)) { | ||||
|               const localIconUrl: string = await DomainUtil.saveServerIcon( | ||||
|                 iconURL, | ||||
|               ); | ||||
|               const serverImgsSelector = ".tab .server-icons"; | ||||
|               const serverImgs: NodeListOf<HTMLImageElement> = | ||||
|                 document.querySelectorAll(serverImgsSelector); | ||||
|               serverImgs[index].src = localIconUrl; | ||||
|               domain.icon = localIconUrl; | ||||
|             if (domain.url === serverURL) { | ||||
|               const localIconPath = await DomainUtil.saveServerIcon(iconURL); | ||||
|               const tab = this.tabs[index]; | ||||
|               if (tab instanceof ServerTab) | ||||
|                 tab.setIcon(DomainUtil.iconAsUrl(localIconPath)); | ||||
|               domain.icon = localIconPath; | ||||
|               DomainUtil.updateDomain(index, domain); | ||||
|             } | ||||
|           }), | ||||
| @@ -1089,61 +1084,56 @@ export class ServerManagerView { | ||||
|       this.$fullscreenPopup.classList.remove("show"); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "focus-webview-with-id", | ||||
|       async (event: Event, webviewId: number) => | ||||
|         Promise.all( | ||||
|           this.tabs.map(async (tab) => { | ||||
|             if ( | ||||
|               tab instanceof ServerTab && | ||||
|               (await tab.webview).webContentsId === webviewId | ||||
|             ) { | ||||
|               const concurrentTab: HTMLButtonElement = document.querySelector( | ||||
|                 `div[data-tab-id="${CSS.escape(`${tab.props.tabIndex}`)}"]`, | ||||
|               )!; | ||||
|               concurrentTab.click(); | ||||
|             } | ||||
|           }), | ||||
|         ), | ||||
|     ipcRenderer.on("focus-webview-with-id", async (event, webviewId: number) => | ||||
|       Promise.all( | ||||
|         this.tabs.map(async (tab) => { | ||||
|           if ( | ||||
|             tab instanceof ServerTab && | ||||
|             (await tab.webview).webContentsId === webviewId | ||||
|           ) { | ||||
|             const concurrentTab: HTMLButtonElement = document.querySelector( | ||||
|               `div[data-tab-id="${CSS.escape(`${tab.props.tabIndex}`)}"]`, | ||||
|             )!; | ||||
|             concurrentTab.click(); | ||||
|           } | ||||
|         }), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     ipcRenderer.on( | ||||
|       "render-taskbar-icon", | ||||
|       (event: Event, messageCount: number) => { | ||||
|         // Create a canvas from unread messagecounts | ||||
|         function createOverlayIcon(messageCount: number): HTMLCanvasElement { | ||||
|           const canvas = document.createElement("canvas"); | ||||
|           canvas.height = 128; | ||||
|           canvas.width = 128; | ||||
|           canvas.style.letterSpacing = "-5px"; | ||||
|           const ctx = canvas.getContext("2d")!; | ||||
|           ctx.fillStyle = "#f42020"; | ||||
|           ctx.beginPath(); | ||||
|           ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI); | ||||
|           ctx.fill(); | ||||
|           ctx.textAlign = "center"; | ||||
|           ctx.fillStyle = "white"; | ||||
|           if (messageCount > 99) { | ||||
|             ctx.font = "65px Helvetica"; | ||||
|             ctx.fillText("99+", 64, 85); | ||||
|           } else if (messageCount < 10) { | ||||
|             ctx.font = "90px Helvetica"; | ||||
|             ctx.fillText(String(Math.min(99, messageCount)), 64, 96); | ||||
|           } else { | ||||
|             ctx.font = "85px Helvetica"; | ||||
|             ctx.fillText(String(Math.min(99, messageCount)), 64, 90); | ||||
|           } | ||||
|  | ||||
|           return canvas; | ||||
|     ipcRenderer.on("render-taskbar-icon", (event, messageCount: number) => { | ||||
|       // Create a canvas from unread message counts | ||||
|       function createOverlayIcon(messageCount: number): HTMLCanvasElement { | ||||
|         const canvas = document.createElement("canvas"); | ||||
|         canvas.height = 128; | ||||
|         canvas.width = 128; | ||||
|         canvas.style.letterSpacing = "-5px"; | ||||
|         const ctx = canvas.getContext("2d")!; | ||||
|         ctx.fillStyle = "#f42020"; | ||||
|         ctx.beginPath(); | ||||
|         ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI); | ||||
|         ctx.fill(); | ||||
|         ctx.textAlign = "center"; | ||||
|         ctx.fillStyle = "white"; | ||||
|         if (messageCount > 99) { | ||||
|           ctx.font = "65px Helvetica"; | ||||
|           ctx.fillText("99+", 64, 85); | ||||
|         } else if (messageCount < 10) { | ||||
|           ctx.font = "90px Helvetica"; | ||||
|           ctx.fillText(String(Math.min(99, messageCount)), 64, 96); | ||||
|         } else { | ||||
|           ctx.font = "85px Helvetica"; | ||||
|           ctx.fillText(String(Math.min(99, messageCount)), 64, 90); | ||||
|         } | ||||
|  | ||||
|         ipcRenderer.send( | ||||
|           "update-taskbar-icon", | ||||
|           createOverlayIcon(messageCount).toDataURL(), | ||||
|           String(messageCount), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|         return canvas; | ||||
|       } | ||||
|  | ||||
|       ipcRenderer.send( | ||||
|         "update-taskbar-icon", | ||||
|         createOverlayIcon(messageCount).toDataURL(), | ||||
|         String(messageCount), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     ipcRenderer.on("copy-zulip-url", async () => { | ||||
|       clipboard.writeText(await this.getCurrentActiveServer()); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| export interface NotificationData { | ||||
| export type NotificationData = { | ||||
|   close: () => void; | ||||
|   title: string; | ||||
|   dir: NotificationDirection; | ||||
| @@ -9,7 +9,7 @@ export interface NotificationData { | ||||
|   tag: string; | ||||
|   icon: string; | ||||
|   data: unknown; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function newNotification( | ||||
|   title: string, | ||||
| @@ -18,7 +18,7 @@ export function newNotification( | ||||
| ): NotificationData { | ||||
|   const notification = new Notification(title, {...options, silent: true}); | ||||
|   for (const type of ["click", "close", "error", "show"]) { | ||||
|     notification.addEventListener(type, (ev: Event) => { | ||||
|     notification.addEventListener(type, (ev) => { | ||||
|       if (type === "click") ipcRenderer.send("focus-this-webview"); | ||||
|       if (!dispatch(type, ev)) { | ||||
|         ev.preventDefault(); | ||||
|   | ||||
| @@ -1,41 +1,21 @@ | ||||
| import {app} from "@electron/remote"; | ||||
|  | ||||
| import {html} from "../../../common/html"; | ||||
| import {bundleUrl} from "../../../common/paths.js"; | ||||
|  | ||||
| export class AboutView { | ||||
|   static async create(): Promise<AboutView> { | ||||
|     return new AboutView( | ||||
|       await (await fetch(new URL("app/renderer/about.html", bundleUrl))).text(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   readonly $view: HTMLElement; | ||||
|  | ||||
|   constructor() { | ||||
|   private constructor(templateHtml: string) { | ||||
|     this.$view = document.createElement("div"); | ||||
|     const $shadow = this.$view.attachShadow({mode: "open"}); | ||||
|     $shadow.innerHTML = html` | ||||
|       <link rel="stylesheet" href="css/about.css" /> | ||||
|       <!-- Initially hidden to prevent FOUC --> | ||||
|       <div class="about" hidden> | ||||
|         <img class="logo" src="../resources/zulip.png" /> | ||||
|         <p class="detail" id="version">v${app.getVersion()}</p> | ||||
|         <div class="maintenance-info"> | ||||
|           <p class="detail maintainer"> | ||||
|             Maintained by | ||||
|             <a | ||||
|               href="https://zulip.com" | ||||
|               target="_blank" | ||||
|               rel="noopener noreferrer" | ||||
|               >Zulip</a | ||||
|             > | ||||
|           </p> | ||||
|           <p class="detail license"> | ||||
|             Available under the | ||||
|             <a | ||||
|               href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE" | ||||
|               target="_blank" | ||||
|               rel="noopener noreferrer" | ||||
|               >Apache 2.0 License</a | ||||
|             > | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|     `.html; | ||||
|     $shadow.innerHTML = templateHtml; | ||||
|     $shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`; | ||||
|   } | ||||
|  | ||||
|   destroy() { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| export function init( | ||||
|   $reconnectButton: Element, | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import type {Html} from "../../../../common/html"; | ||||
| import {html} from "../../../../common/html"; | ||||
| import {generateNodeFromHtml} from "../../components/base"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import type {Html} from "../../../../common/html.js"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import {generateNodeFromHtml} from "../../components/base.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
|  | ||||
| interface BaseSectionProps { | ||||
| type BaseSectionProps = { | ||||
|   $element: HTMLElement; | ||||
|   disabled?: boolean; | ||||
|   value: boolean; | ||||
|   clickHandler: () => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function generateSettingOption(props: BaseSectionProps): void { | ||||
|   const {$element, disabled, value, clickHandler} = props; | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import * as DomainUtil from "../../utils/domain-util"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
| import * as DomainUtil from "../../utils/domain-util.js"; | ||||
|  | ||||
| import {reloadApp} from "./base-section"; | ||||
| import {initFindAccounts} from "./find-accounts"; | ||||
| import {initServerInfoForm} from "./server-info-form"; | ||||
| import {reloadApp} from "./base-section.js"; | ||||
| import {initFindAccounts} from "./find-accounts.js"; | ||||
| import {initServerInfoForm} from "./server-info-form.js"; | ||||
|  | ||||
| interface ConnectedOrgSectionProps { | ||||
| type ConnectedOrgSectionProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initConnectedOrgSection({ | ||||
|   $root, | ||||
| @@ -21,7 +21,7 @@ export function initConnectedOrgSection({ | ||||
|     <div class="settings-pane" id="server-settings-pane"> | ||||
|       <div class="page-title">${t.__("Connected organizations")}</div> | ||||
|       <div class="title" id="existing-servers"> | ||||
|         ${t.__("All the connected orgnizations will appear here.")} | ||||
|         ${t.__("All the connected organizations will appear here.")} | ||||
|       </div> | ||||
|       <div id="server-info-container"></div> | ||||
|       <div id="new-org-button"> | ||||
| @@ -42,7 +42,9 @@ export function initConnectedOrgSection({ | ||||
|     "#find-accounts-container", | ||||
|   )!; | ||||
|  | ||||
|   const noServerText = t.__("All the connected orgnizations will appear here"); | ||||
|   const noServerText = t.__( | ||||
|     "All the connected organizations will appear here.", | ||||
|   ); | ||||
|   // Show noServerText if no servers are there otherwise hide it | ||||
|   $existingServers.textContent = servers.length === 0 ? noServerText : ""; | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as LinkUtil from "../../../../common/link-util"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {generateNodeFromHtml} from "../../components/base"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as LinkUtil from "../../../../common/link-util.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import {generateNodeFromHtml} from "../../components/base.js"; | ||||
|  | ||||
| interface FindAccountsProps { | ||||
| type FindAccountsProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| async function findAccounts(url: string): Promise<void> { | ||||
|   if (!url) { | ||||
|   | ||||
| @@ -7,22 +7,22 @@ import * as remote from "@electron/remote"; | ||||
| import {app, dialog, session} from "@electron/remote"; | ||||
| import Tagify from "@yaireo/tagify"; | ||||
| import ISO6391 from "iso-639-1"; | ||||
| import * as z from "zod"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import * as ConfigUtil from "../../../../common/config-util"; | ||||
| import * as EnterpriseUtil from "../../../../common/enterprise-util"; | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import supportedLocales from "../../../../translations/supported-locales.json"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import supportedLocales from "../../../../../public/translations/supported-locales.json"; | ||||
| import * as ConfigUtil from "../../../../common/config-util.js"; | ||||
| import * as EnterpriseUtil from "../../../../common/enterprise-util.js"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
|  | ||||
| import {generateSelectHtml, generateSettingOption} from "./base-section"; | ||||
| import {generateSelectHtml, generateSettingOption} from "./base-section.js"; | ||||
|  | ||||
| const currentBrowserWindow = remote.getCurrentWindow(); | ||||
|  | ||||
| interface GeneralSectionProps { | ||||
| type GeneralSectionProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|   $root.innerHTML = html` | ||||
| @@ -356,7 +356,8 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|         const newValue = !ConfigUtil.getConfigItem("silent", true); | ||||
|         ConfigUtil.setConfigItem("silent", newValue); | ||||
|         updateSilentOption(); | ||||
|         ipcRenderer.sendTo( | ||||
|         ipcRenderer.send( | ||||
|           "forward-to", | ||||
|           currentBrowserWindow.webContents.id, | ||||
|           "toggle-silent", | ||||
|           newValue, | ||||
| @@ -460,9 +461,8 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|       filters: [{name: "CSS file", extensions: ["css"]}], | ||||
|     }; | ||||
|  | ||||
|     const {filePaths, canceled} = await dialog.showOpenDialog( | ||||
|       showDialogOptions, | ||||
|     ); | ||||
|     const {filePaths, canceled} = | ||||
|       await dialog.showOpenDialog(showDialogOptions); | ||||
|     if (!canceled) { | ||||
|       ConfigUtil.setConfigItem("customCSS", filePaths[0]); | ||||
|       ipcRenderer.send("forward-message", "hard-reload"); | ||||
| @@ -529,9 +529,8 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|       properties: ["openDirectory"], | ||||
|     }; | ||||
|  | ||||
|     const {filePaths, canceled} = await dialog.showOpenDialog( | ||||
|       showDialogOptions, | ||||
|     ); | ||||
|     const {filePaths, canceled} = | ||||
|       await dialog.showOpenDialog(showDialogOptions); | ||||
|     if (!canceled) { | ||||
|       ConfigUtil.setConfigItem("downloadsPath", filePaths[0]); | ||||
|       const downloadFolderPath: HTMLElement = $root.querySelector( | ||||
| @@ -592,7 +591,7 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|   } | ||||
|  | ||||
|   function initSpellChecker(): void { | ||||
|     // The elctron API is a no-op on macOS and macOS default spellchecker is used. | ||||
|     // The Electron API is a no-op on macOS and macOS default spellchecker is used. | ||||
|     if (process.platform === "darwin") { | ||||
|       const note: HTMLElement = $root.querySelector("#note")!; | ||||
|       note.append(t.__("On macOS, the OS spellchecker is used.")); | ||||
| @@ -610,13 +609,15 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|       const spellDiv: HTMLElement = $root.querySelector("#spellcheck-langs")!; | ||||
|       spellDiv.innerHTML += html` | ||||
|         <div class="setting-description">${t.__("Spellchecker Languages")}</div> | ||||
|         <input name="spellcheck" placeholder="Enter Languages" /> | ||||
|         <div id="spellcheck-langs-value"> | ||||
|           <input name="spellcheck" placeholder="Enter Languages" /> | ||||
|         </div> | ||||
|       `.html; | ||||
|  | ||||
|       const availableLanguages = session.fromPartition( | ||||
|         "persist:webviewsession", | ||||
|       ).availableSpellCheckerLanguages; | ||||
|       let languagePairs: Map<string, string> = new Map(); | ||||
|       let languagePairs = new Map<string, string>(); | ||||
|       for (const l of availableLanguages) { | ||||
|         if (ISO6391.validate(l)) { | ||||
|           languagePairs.set(ISO6391.getName(l), l); | ||||
| @@ -652,8 +653,20 @@ export function initGeneralSection({$root}: GeneralSectionProps): void { | ||||
|           maxItems: Number.POSITIVE_INFINITY, | ||||
|           closeOnSelect: false, | ||||
|           highlightFirst: true, | ||||
|           position: "manual", | ||||
|           classname: "settings-tagify-dropdown", | ||||
|         }, | ||||
|       }); | ||||
|       tagify.DOM.input.addEventListener("focus", () => { | ||||
|         tagify.dropdown.show(); | ||||
|         $root | ||||
|           .querySelector("#spellcheck-langs-value")! | ||||
|           .append(tagify.DOM.dropdown); | ||||
|       }); | ||||
|       tagify.DOM.input.addEventListener("blur", () => { | ||||
|         tagify.dropdown.hide(true); | ||||
|         tagify.DOM.dropdown.remove(); | ||||
|       }); | ||||
|  | ||||
|       const configuredLanguages: string[] = ( | ||||
|         ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [] | ||||
|   | ||||
| @@ -1,20 +1,18 @@ | ||||
| import type {Html} from "../../../../common/html"; | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import type {NavItem} from "../../../../common/types"; | ||||
| import {generateNodeFromHtml} from "../../components/base"; | ||||
| import type {Html} from "../../../../common/html.js"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import type {NavItem} from "../../../../common/types.js"; | ||||
| import {generateNodeFromHtml} from "../../components/base.js"; | ||||
|  | ||||
| interface PreferenceNavProps { | ||||
| type PreferenceNavProps = { | ||||
|   $root: Element; | ||||
|   onItemSelected: (navItem: NavItem) => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export default class PreferenceNav { | ||||
|   props: PreferenceNavProps; | ||||
|   navItems: NavItem[]; | ||||
|   $el: Element; | ||||
|   constructor(props: PreferenceNavProps) { | ||||
|     this.props = props; | ||||
|   constructor(private readonly props: PreferenceNavProps) { | ||||
|     this.navItems = [ | ||||
|       "General", | ||||
|       "Network", | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import * as ConfigUtil from "../../../../common/config-util"; | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import * as ConfigUtil from "../../../../common/config-util.js"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
|  | ||||
| import {generateSettingOption} from "./base-section"; | ||||
| import {generateSettingOption} from "./base-section.js"; | ||||
|  | ||||
| interface NetworkSectionProps { | ||||
| type NetworkSectionProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initNetworkSection({$root}: NetworkSectionProps): void { | ||||
|   $root.innerHTML = html` | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| import {dialog} from "@electron/remote"; | ||||
|  | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as LinkUtil from "../../../../common/link-util"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {generateNodeFromHtml} from "../../components/base"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import * as DomainUtil from "../../utils/domain-util"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as LinkUtil from "../../../../common/link-util.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import {generateNodeFromHtml} from "../../components/base.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
| import * as DomainUtil from "../../utils/domain-util.js"; | ||||
|  | ||||
| interface NewServerFormProps { | ||||
| type NewServerFormProps = { | ||||
|   $root: Element; | ||||
|   onChange: () => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initNewServerForm({$root, onChange}: NewServerFormProps): void { | ||||
|   const $newServerForm = generateNodeFromHtml(html` | ||||
|   | ||||
| @@ -1,46 +1,37 @@ | ||||
| import type {IpcRendererEvent} from "electron/renderer"; | ||||
| import process from "node:process"; | ||||
|  | ||||
| import type {DndSettings} from "../../../../common/dnd-util"; | ||||
| import {html} from "../../../../common/html"; | ||||
| import type {NavItem} from "../../../../common/types"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import type {DndSettings} from "../../../../common/dnd-util.js"; | ||||
| import {bundleUrl} from "../../../../common/paths.js"; | ||||
| import type {NavItem} from "../../../../common/types.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
|  | ||||
| import {initConnectedOrgSection} from "./connected-org-section"; | ||||
| import {initGeneralSection} from "./general-section"; | ||||
| import Nav from "./nav"; | ||||
| import {initNetworkSection} from "./network-section"; | ||||
| import {initServersSection} from "./servers-section"; | ||||
| import {initShortcutsSection} from "./shortcuts-section"; | ||||
| import {initConnectedOrgSection} from "./connected-org-section.js"; | ||||
| import {initGeneralSection} from "./general-section.js"; | ||||
| import Nav from "./nav.js"; | ||||
| import {initNetworkSection} from "./network-section.js"; | ||||
| import {initServersSection} from "./servers-section.js"; | ||||
| import {initShortcutsSection} from "./shortcuts-section.js"; | ||||
|  | ||||
| export class PreferenceView { | ||||
|   static async create(): Promise<PreferenceView> { | ||||
|     return new PreferenceView( | ||||
|       await ( | ||||
|         await fetch(new URL("app/renderer/preference.html", bundleUrl)) | ||||
|       ).text(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   readonly $view: HTMLElement; | ||||
|   private readonly $shadow: ShadowRoot; | ||||
|   private readonly $settingsContainer: Element; | ||||
|   private readonly nav: Nav; | ||||
|   private navItem: NavItem = "General"; | ||||
|  | ||||
|   constructor() { | ||||
|   private constructor(templateHtml: string) { | ||||
|     this.$view = document.createElement("div"); | ||||
|     this.$shadow = this.$view.attachShadow({mode: "open"}); | ||||
|     this.$shadow.innerHTML = html` | ||||
|       <link | ||||
|         rel="stylesheet" | ||||
|         href="${require.resolve("../../../css/fonts.css")}" | ||||
|       /> | ||||
|       <link | ||||
|         rel="stylesheet" | ||||
|         href="${require.resolve("../../../css/preference.css")}" | ||||
|       /> | ||||
|       <link | ||||
|         rel="stylesheet" | ||||
|         href="${require.resolve("@yaireo/tagify/dist/tagify.css")}" | ||||
|       /> | ||||
|       <!-- Initially hidden to prevent FOUC --> | ||||
|       <div id="content" hidden> | ||||
|         <div id="sidebar"></div> | ||||
|         <div id="settings-container"></div> | ||||
|       </div> | ||||
|     `.html; | ||||
|     this.$shadow.innerHTML = templateHtml; | ||||
|  | ||||
|     const $sidebarContainer = this.$shadow.querySelector("#sidebar")!; | ||||
|     this.$settingsContainer = this.$shadow.querySelector( | ||||
| @@ -63,29 +54,33 @@ export class PreferenceView { | ||||
|     this.navItem = navItem; | ||||
|     this.nav.select(navItem); | ||||
|     switch (navItem) { | ||||
|       case "AddServer": | ||||
|       case "AddServer": { | ||||
|         initServersSection({ | ||||
|           $root: this.$settingsContainer, | ||||
|         }); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case "General": | ||||
|       case "General": { | ||||
|         initGeneralSection({ | ||||
|           $root: this.$settingsContainer, | ||||
|         }); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case "Organizations": | ||||
|       case "Organizations": { | ||||
|         initConnectedOrgSection({ | ||||
|           $root: this.$settingsContainer, | ||||
|         }); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case "Network": | ||||
|       case "Network": { | ||||
|         initNetworkSection({ | ||||
|           $root: this.$settingsContainer, | ||||
|         }); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case "Shortcuts": { | ||||
|         initShortcutsSection({ | ||||
| @@ -94,8 +89,9 @@ export class PreferenceView { | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       default: | ||||
|       default: { | ||||
|         ((n: never) => n)(navItem); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     window.location.hash = `#${navItem}`; | ||||
| @@ -120,16 +116,22 @@ export class PreferenceView { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private readonly handleToggleSidebar = (_event: Event, state: boolean) => { | ||||
|   private readonly handleToggleSidebar = ( | ||||
|     _event: IpcRendererEvent, | ||||
|     state: boolean, | ||||
|   ) => { | ||||
|     this.handleToggle("sidebar-option", state); | ||||
|   }; | ||||
|  | ||||
|   private readonly handleToggleMenubar = (_event: Event, state: boolean) => { | ||||
|   private readonly handleToggleMenubar = ( | ||||
|     _event: IpcRendererEvent, | ||||
|     state: boolean, | ||||
|   ) => { | ||||
|     this.handleToggle("menubar-option", state); | ||||
|   }; | ||||
|  | ||||
|   private readonly handleToggleDnd = ( | ||||
|     _event: Event, | ||||
|     _event: IpcRendererEvent, | ||||
|     _state: boolean, | ||||
|     newSettings: Partial<DndSettings>, | ||||
|   ) => { | ||||
|   | ||||
| @@ -1,25 +1,28 @@ | ||||
| import {dialog} from "@electron/remote"; | ||||
|  | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as Messages from "../../../../common/messages"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import type {ServerConf} from "../../../../common/types"; | ||||
| import {generateNodeFromHtml} from "../../components/base"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer"; | ||||
| import * as DomainUtil from "../../utils/domain-util"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as Messages from "../../../../common/messages.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
| import type {ServerConf} from "../../../../common/types.js"; | ||||
| import {generateNodeFromHtml} from "../../components/base.js"; | ||||
| import {ipcRenderer} from "../../typed-ipc-renderer.js"; | ||||
| import * as DomainUtil from "../../utils/domain-util.js"; | ||||
|  | ||||
| interface ServerInfoFormProps { | ||||
| type ServerInfoFormProps = { | ||||
|   $root: Element; | ||||
|   server: ServerConf; | ||||
|   index: number; | ||||
|   onChange: () => void; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initServerInfoForm(props: ServerInfoFormProps): void { | ||||
|   const $serverInfoForm = generateNodeFromHtml(html` | ||||
|     <div class="settings-card"> | ||||
|       <div class="server-info-left"> | ||||
|         <img class="server-info-icon" src="${props.server.icon}" /> | ||||
|         <img | ||||
|           class="server-info-icon" | ||||
|           src="${DomainUtil.iconAsUrl(props.server.icon)}" | ||||
|         /> | ||||
|         <div class="server-info-row"> | ||||
|           <span class="server-info-alias">${props.server.alias}</span> | ||||
|           <i class="material-icons open-tab-button">open_in_new</i> | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
|  | ||||
| import {reloadApp} from "./base-section"; | ||||
| import {initNewServerForm} from "./new-server-form"; | ||||
| import {reloadApp} from "./base-section.js"; | ||||
| import {initNewServerForm} from "./new-server-form.js"; | ||||
|  | ||||
| interface ServersSectionProps { | ||||
| type ServersSectionProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function initServersSection({$root}: ServersSectionProps): void { | ||||
|   $root.innerHTML = html` | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import process from "node:process"; | ||||
|  | ||||
| import {html} from "../../../../common/html"; | ||||
| import * as LinkUtil from "../../../../common/link-util"; | ||||
| import * as t from "../../../../common/translation-util"; | ||||
| import {html} from "../../../../common/html.js"; | ||||
| import * as LinkUtil from "../../../../common/link-util.js"; | ||||
| import * as t from "../../../../common/translation-util.js"; | ||||
|  | ||||
| interface ShortcutsSectionProps { | ||||
| type ShortcutsSectionProps = { | ||||
|   $root: Element; | ||||
| } | ||||
| }; | ||||
|  | ||||
| // eslint-disable-next-line complexity | ||||
| export function initShortcutsSection({$root}: ShortcutsSectionProps): void { | ||||
|   | ||||
| @@ -1,68 +1,21 @@ | ||||
| import {contextBridge, webFrame} from "electron/renderer"; | ||||
| import fs from "node:fs"; | ||||
| import {contextBridge} from "electron/renderer"; | ||||
|  | ||||
| import electron_bridge, {bridgeEvents} from "./electron-bridge"; | ||||
| import * as NetworkError from "./pages/network"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer"; | ||||
| import electron_bridge, {bridgeEvents} from "./electron-bridge.js"; | ||||
| import * as NetworkError from "./pages/network.js"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer.js"; | ||||
|  | ||||
| contextBridge.exposeInMainWorld("raw_electron_bridge", electron_bridge); | ||||
| contextBridge.exposeInMainWorld("electron_bridge", electron_bridge); | ||||
|  | ||||
| ipcRenderer.on("logout", () => { | ||||
|   if (bridgeEvents.emit("logout")) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Create the menu for the below | ||||
|   const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!; | ||||
|   dropdown.click(); | ||||
|  | ||||
|   const nodes: NodeListOf<HTMLElement> = document.querySelectorAll( | ||||
|     ".dropdown-menu li:last-child a", | ||||
|   ); | ||||
|   nodes[nodes.length - 1].click(); | ||||
|   bridgeEvents.emit("logout"); | ||||
| }); | ||||
|  | ||||
| ipcRenderer.on("show-keyboard-shortcuts", () => { | ||||
|   if (bridgeEvents.emit("show-keyboard-shortcuts")) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Create the menu for the below | ||||
|   const node: HTMLElement = document.querySelector( | ||||
|     "a[data-overlay-trigger=keyboard-shortcuts]", | ||||
|   )!; | ||||
|   // Additional check | ||||
|   if (node.textContent!.trim().toLowerCase() === "keyboard shortcuts (?)") { | ||||
|     node.click(); | ||||
|   } else { | ||||
|     // Atleast click the dropdown | ||||
|     const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!; | ||||
|     dropdown.click(); | ||||
|   } | ||||
|   bridgeEvents.emit("show-keyboard-shortcuts"); | ||||
| }); | ||||
|  | ||||
| ipcRenderer.on("show-notification-settings", () => { | ||||
|   if (bridgeEvents.emit("show-notification-settings")) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Create the menu for the below | ||||
|   const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!; | ||||
|   dropdown.click(); | ||||
|  | ||||
|   const nodes: NodeListOf<HTMLElement> = document.querySelectorAll( | ||||
|     ".dropdown-menu li a", | ||||
|   ); | ||||
|   nodes[2].click(); | ||||
|  | ||||
|   const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll( | ||||
|     ".normal-settings-list li div", | ||||
|   ); | ||||
|  | ||||
|   // Wait until the notification dom element shows up | ||||
|   setTimeout(() => { | ||||
|     notificationItem[2].click(); | ||||
|   }, 100); | ||||
|   bridgeEvents.emit("show-notification-settings"); | ||||
| }); | ||||
|  | ||||
| window.addEventListener("load", () => { | ||||
| @@ -74,8 +27,3 @@ window.addEventListener("load", () => { | ||||
|   const $settingsButton = document.querySelector("#settings")!; | ||||
|   NetworkError.init($reconnectButton, $settingsButton); | ||||
| }); | ||||
|  | ||||
| (async () => | ||||
|   webFrame.executeJavaScript( | ||||
|     fs.readFileSync(require.resolve("./injected"), "utf8"), | ||||
|   ))(); | ||||
|   | ||||
| @@ -6,19 +6,16 @@ import process from "node:process"; | ||||
|  | ||||
| import {BrowserWindow, Menu, Tray} from "@electron/remote"; | ||||
|  | ||||
| import * as ConfigUtil from "../../common/config-util"; | ||||
| import type {RendererMessage} from "../../common/typed-ipc"; | ||||
| import * as ConfigUtil from "../../common/config-util.js"; | ||||
| import {publicPath} from "../../common/paths.js"; | ||||
| import type {RendererMessage} from "../../common/typed-ipc.js"; | ||||
|  | ||||
| import type {ServerManagerView} from "./main"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer"; | ||||
| import type {ServerManagerView} from "./main.js"; | ||||
| import {ipcRenderer} from "./typed-ipc-renderer.js"; | ||||
|  | ||||
| let tray: ElectronTray | null = null; | ||||
|  | ||||
| const iconDir = "../../resources/tray"; | ||||
|  | ||||
| const traySuffix = "tray"; | ||||
|  | ||||
| const appIcon = path.join(__dirname, iconDir, traySuffix); | ||||
| const appIcon = path.join(publicPath, "resources/tray/tray"); | ||||
|  | ||||
| const iconPath = (): string => { | ||||
|   if (process.platform === "linux") { | ||||
| @@ -36,14 +33,21 @@ let unread = 0; | ||||
|  | ||||
| const trayIconSize = (): number => { | ||||
|   switch (process.platform) { | ||||
|     case "darwin": | ||||
|     case "darwin": { | ||||
|       return 20; | ||||
|     case "win32": | ||||
|     } | ||||
|  | ||||
|     case "win32": { | ||||
|       return 100; | ||||
|     case "linux": | ||||
|     } | ||||
|  | ||||
|     case "linux": { | ||||
|       return 100; | ||||
|     default: | ||||
|     } | ||||
|  | ||||
|     default: { | ||||
|       return 80; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @@ -134,7 +138,7 @@ function sendAction<Channel extends keyof RendererMessage>( | ||||
|     win.restore(); | ||||
|   } | ||||
|  | ||||
|   ipcRenderer.sendTo(win.webContents.id, channel, ...args); | ||||
|   ipcRenderer.send("forward-to", win.webContents.id, channel, ...args); | ||||
| } | ||||
|  | ||||
| const createTray = function (): void { | ||||
| @@ -172,7 +176,7 @@ const createTray = function (): void { | ||||
| }; | ||||
|  | ||||
| export function initializeTray(serverManagerView: ServerManagerView) { | ||||
|   ipcRenderer.on("destroytray", (_event: Event) => { | ||||
|   ipcRenderer.on("destroytray", () => { | ||||
|     if (!tray) { | ||||
|       return; | ||||
|     } | ||||
| @@ -185,7 +189,7 @@ export function initializeTray(serverManagerView: ServerManagerView) { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   ipcRenderer.on("tray", (_event: Event, arg: number): void => { | ||||
|   ipcRenderer.on("tray", (_event, arg: number): void => { | ||||
|     if (!tray) { | ||||
|       return; | ||||
|     } | ||||
| @@ -236,5 +240,3 @@ export function initializeTray(serverManagerView: ServerManagerView) { | ||||
|     createTray(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export {}; | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import type { | ||||
|   MainCall, | ||||
|   MainMessage, | ||||
|   RendererMessage, | ||||
| } from "../../common/typed-ipc"; | ||||
| } from "../../common/typed-ipc.js"; | ||||
|  | ||||
| type RendererListener<Channel extends keyof RendererMessage> = | ||||
|   RendererMessage[Channel] extends (...args: infer Args) => void | ||||
| @@ -37,6 +37,12 @@ export const ipcRenderer: { | ||||
|     rendererChannel: Channel, | ||||
|     ...args: Parameters<RendererMessage[Channel]> | ||||
|   ): void; | ||||
|   send<Channel extends keyof RendererMessage>( | ||||
|     channel: "forward-to", | ||||
|     webContentsId: number, | ||||
|     rendererChannel: Channel, | ||||
|     ...args: Parameters<RendererMessage[Channel]> | ||||
|   ): void; | ||||
|   send<Channel extends keyof MainMessage>( | ||||
|     channel: Channel, | ||||
|     ...args: Parameters<MainMessage[Channel]> | ||||
| @@ -56,11 +62,6 @@ export const ipcRenderer: { | ||||
|       : never, | ||||
|     transfer?: MessagePort[], | ||||
|   ): void; | ||||
|   sendTo<Channel extends keyof RendererMessage>( | ||||
|     webContentsId: number, | ||||
|     channel: Channel, | ||||
|     ...args: Parameters<RendererMessage[Channel]> | ||||
|   ): void; | ||||
|   sendToHost<Channel extends keyof RendererMessage>( | ||||
|     channel: Channel, | ||||
|     ...args: Parameters<RendererMessage[Channel]> | ||||
|   | ||||
| @@ -2,27 +2,33 @@ import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
|  | ||||
| import {app, dialog} from "@electron/remote"; | ||||
| import * as Sentry from "@sentry/electron"; | ||||
| import * as Sentry from "@sentry/electron/renderer"; | ||||
| import {JsonDB} from "node-json-db"; | ||||
| import {DataError} from "node-json-db/dist/lib/Errors"; | ||||
| import * as z from "zod"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import * as EnterpriseUtil from "../../../common/enterprise-util"; | ||||
| import Logger from "../../../common/logger-util"; | ||||
| import * as Messages from "../../../common/messages"; | ||||
| import type {ServerConf} from "../../../common/types"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import * as EnterpriseUtil from "../../../common/enterprise-util.js"; | ||||
| import Logger from "../../../common/logger-util.js"; | ||||
| import * as Messages from "../../../common/messages.js"; | ||||
| import * as t from "../../../common/translation-util.js"; | ||||
| import type {ServerConf} from "../../../common/types.js"; | ||||
| import defaultIcon from "../../img/icon.png"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| const logger = new Logger({ | ||||
|   file: "domain-util.log", | ||||
| }); | ||||
|  | ||||
| const defaultIconUrl = "../renderer/img/icon.png"; | ||||
| // For historical reasons, we store this string in domain.json to denote a | ||||
| // missing icon; it does not change with the actual icon location. | ||||
| export const defaultIconSentinel = "../renderer/img/icon.png"; | ||||
|  | ||||
| const serverConfSchema = z.object({ | ||||
|   url: z.string(), | ||||
|   url: z.string().url(), | ||||
|   alias: z.string(), | ||||
|   icon: z.string(), | ||||
|   zulipVersion: z.string().default("unknown"), | ||||
|   zulipFeatureLevel: z.number().default(0), | ||||
| }); | ||||
|  | ||||
| let db!: JsonDB; | ||||
| @@ -78,7 +84,7 @@ export async function addDomain(server: { | ||||
|     db.push("/domains[]", server, true); | ||||
|     reloadDb(); | ||||
|   } else { | ||||
|     server.icon = defaultIconUrl; | ||||
|     server.icon = defaultIconSentinel; | ||||
|     serverConfSchema.parse(server); | ||||
|     db.push("/domains[]", server, true); | ||||
|     reloadDb(); | ||||
| @@ -129,27 +135,34 @@ async function getServerSettings(domain: string): Promise<ServerConf> { | ||||
| } | ||||
|  | ||||
| export async function saveServerIcon(iconURL: string): Promise<string> { | ||||
|   return ipcRenderer.invoke("save-server-icon", iconURL); | ||||
|   return ( | ||||
|     (await ipcRenderer.invoke("save-server-icon", iconURL)) ?? | ||||
|     defaultIconSentinel | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export async function updateSavedServer( | ||||
|   url: string, | ||||
|   index: number, | ||||
| ): Promise<void> { | ||||
| ): Promise<ServerConf> { | ||||
|   // Does not promise successful update | ||||
|   const oldIcon = getDomain(index).icon; | ||||
|   const serverConf = getDomain(index); | ||||
|   const oldIcon = serverConf.icon; | ||||
|   try { | ||||
|     const newServerConf = await checkDomain(url, true); | ||||
|     const localIconUrl = await saveServerIcon(newServerConf.icon); | ||||
|     if (!oldIcon || localIconUrl !== "../renderer/img/icon.png") { | ||||
|     if (!oldIcon || localIconUrl !== defaultIconSentinel) { | ||||
|       newServerConf.icon = localIconUrl; | ||||
|       updateDomain(index, newServerConf); | ||||
|       reloadDb(); | ||||
|     } | ||||
|  | ||||
|     return newServerConf; | ||||
|   } catch (error: unknown) { | ||||
|     logger.log("Could not update server icon."); | ||||
|     logger.log(error); | ||||
|     Sentry.captureException(error); | ||||
|     return serverConf; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -189,3 +202,28 @@ export function formatUrl(domain: string): string { | ||||
|  | ||||
|   return `https://${domain}`; | ||||
| } | ||||
|  | ||||
| export function getUnsupportedMessage(server: ServerConf): string | undefined { | ||||
|   if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) { | ||||
|     const realm = new URL(server.url).hostname; | ||||
|     return t.__( | ||||
|       "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.", | ||||
|       {server: realm, version: server.zulipVersion}, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return undefined; | ||||
| } | ||||
|  | ||||
| export function iconAsUrl(iconPath: string): string { | ||||
|   if (iconPath === defaultIconSentinel) return defaultIcon; | ||||
|  | ||||
|   try { | ||||
|     return `data:application/octet-stream;base64,${fs.readFileSync( | ||||
|       iconPath, | ||||
|       "base64", | ||||
|     )}`; | ||||
|   } catch { | ||||
|     return defaultIcon; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,22 +1,20 @@ | ||||
| import * as backoff from "backoff"; | ||||
|  | ||||
| import {html} from "../../../common/html"; | ||||
| import Logger from "../../../common/logger-util"; | ||||
| import type WebView from "../components/webview"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import {html} from "../../../common/html.js"; | ||||
| import Logger from "../../../common/logger-util.js"; | ||||
| import type WebView from "../components/webview.js"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| const logger = new Logger({ | ||||
|   file: "domain-util.log", | ||||
| }); | ||||
|  | ||||
| export default class ReconnectUtil { | ||||
|   webview: WebView; | ||||
|   url: string; | ||||
|   alreadyReloaded: boolean; | ||||
|   fibonacciBackoff: backoff.Backoff; | ||||
|  | ||||
|   constructor(webview: WebView) { | ||||
|     this.webview = webview; | ||||
|     this.url = webview.props.url; | ||||
|     this.alreadyReloaded = false; | ||||
|     this.fibonacciBackoff = backoff.fibonacci({ | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {ipcRenderer} from "../typed-ipc-renderer"; | ||||
| import {ipcRenderer} from "../typed-ipc-renderer.js"; | ||||
|  | ||||
| export const connectivityError: string[] = [ | ||||
|   "ERR_INTERNET_DISCONNECTED", | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| <!DOCTYPE html> | ||||
| <!doctype html> | ||||
| <html lang="en" class="responsive desktop"> | ||||
|   <head> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | ||||
|     <meta | ||||
|       http-equiv="Content-Security-Policy" | ||||
|       content="default-src 'none'; connect-src 'self'; font-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'" | ||||
|     /> | ||||
|     <meta name="viewport" content="width=device-width" /> | ||||
|     <title>Zulip</title> | ||||
|     <link rel="stylesheet" href="css/fonts.css" /> | ||||
|     <link rel="stylesheet" href="css/main.css" type="text/css" media="screen" /> | ||||
|     <link rel="stylesheet" href="css/main.css" /> | ||||
|   </head> | ||||
|  | ||||
|   <body> | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| <!DOCTYPE html> | ||||
| <!doctype html> | ||||
| <html lang="en" class="responsive desktop"> | ||||
|   <head> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | ||||
|     <meta | ||||
|       http-equiv="Content-Security-Policy" | ||||
|       content="default-src 'none'; connect-src 'self'; img-src 'self'; script-src 'self'; style-src 'self'" | ||||
|     /> | ||||
|     <meta name="viewport" content="width=device-width" /> | ||||
|     <title>Zulip - Network Troubleshooting</title> | ||||
|     <link | ||||
|   | ||||
							
								
								
									
										10
									
								
								app/renderer/preference.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <!doctype html> | ||||
| <meta charset="UTF-8" /> | ||||
| <link rel="stylesheet" href="css/fonts.css" /> | ||||
| <link rel="stylesheet" href="css/preference.css" /> | ||||
|  | ||||
| <!-- Initially hidden to prevent FOUC --> | ||||
| <div id="content" hidden> | ||||
|   <div id="sidebar"></div> | ||||
|   <div id="settings-container"></div> | ||||
| </div> | ||||
| @@ -1,134 +0,0 @@ | ||||
| { | ||||
| 	"About Zulip": "О Зулипу", | ||||
| 	"Actual Size": "Стварна величина", | ||||
| 	"Add Custom Certificates": "Додајте прилагођене цертификате", | ||||
| 	"Add Organization": "Додај организацију", | ||||
| 	"Add a Zulip organization": "Додајте Зулип организацију", | ||||
| 	"Add custom CSS": "Додајте прилагођени ЦСС", | ||||
| 	"Advanced": "Напредно", | ||||
| 	"All the connected organizations will appear here": "Овде ће се појавити све повезане организације", | ||||
| 	"Always start minimized": "Увек започните минимизирано", | ||||
| 	"App Updates": "Апп Упдатес", | ||||
| 	"Appearance": "Изглед", | ||||
| 	"Application Shortcuts": "Пречице за апликације", | ||||
| 	"Are you sure you want to disconnect this organization?": "Јесте ли сигурни да желите прекинути везу с овом организацијом?", | ||||
| 	"Auto hide Menu bar": "Ауто хиде Мену бар", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Аутоматско скривање траке менија (притисните тастер Алт да бисте приказали)", | ||||
| 	"Back": "Назад", | ||||
| 	"Bounce dock on new private message": "Одскочите у нову приватну поруку", | ||||
| 	"Certificate file": "Датотека сертификата", | ||||
| 	"Change": "Цханге", | ||||
| 	"Check for Updates": "Провери ажурирања", | ||||
| 	"Close": "Близу", | ||||
| 	"Connect": "Повежи", | ||||
| 	"Connect to another organization": "Повежите се са другом организацијом", | ||||
| 	"Connected organizations": "Повезане организације", | ||||
| 	"Copy": "Копирај", | ||||
| 	"Copy Zulip URL": "Цопи Зулип УРЛ", | ||||
| 	"Create a new organization": "Направите нову организацију", | ||||
| 	"Cut": "Цут", | ||||
| 	"Default download location": "Дефаулт довнлоад лоцатион", | ||||
| 	"Delete": "Обриши", | ||||
| 	"Desktop App Settings": "Подешавања апликације за десктоп рачунаре", | ||||
| 	"Desktop Notifications": "Обавештења о радној површини", | ||||
| 	"Desktop Settings": "Десктоп Сеттингс", | ||||
| 	"Disconnect": "Дисцоннецт", | ||||
| 	"Download App Logs": "Довнлоад Апп Логс", | ||||
| 	"Edit": "Уредити", | ||||
| 	"Edit Shortcuts": "Уреди пречице", | ||||
| 	"Enable auto updates": "Омогући аутоматско ажурирање", | ||||
| 	"Enable error reporting (requires restart)": "Омогући извештавање о грешкама (захтева поновно покретање)", | ||||
| 	"Enable spellchecker (requires restart)": "Омогући провјеру правописа (захтијева поновно покретање)", | ||||
| 	"Factory Reset": "Фацтори Ресет", | ||||
| 	"File": "Филе", | ||||
| 	"Find accounts": "Нађи рачуне", | ||||
| 	"Find accounts by email": "Пронађите рачуне путем е-поште", | ||||
| 	"Flash taskbar on new message": "Фласх трака задатака у новој поруци", | ||||
| 	"Forward": "Напријед", | ||||
| 	"Functionality": "Функционалност", | ||||
| 	"General": "Генерал", | ||||
| 	"Get beta updates": "Набавите бета ажурирања", | ||||
| 	"Hard Reload": "Хард Релоад", | ||||
| 	"Help": "Помоћ", | ||||
| 	"Help Center": "Центар за помоћ", | ||||
| 	"History": "Хистори", | ||||
| 	"History Shortcuts": "Историјске пречице", | ||||
| 	"Keyboard Shortcuts": "Пречице на тастатури", | ||||
| 	"Log Out": "Одјавити се", | ||||
| 	"Log Out of Organization": "Одјавите се из организације", | ||||
| 	"Manual proxy configuration": "Мануал проки цонфигуратион", | ||||
| 	"Minimize": "Минимизе", | ||||
| 	"Mute all sounds from Zulip": "Искључите све звукове из Зулипа", | ||||
| 	"NO": "НЕ", | ||||
| 	"Network": "Мрежа", | ||||
| 	"OR": "ОР", | ||||
| 	"Organization URL": "УРЛ организације", | ||||
| 	"Organizations": "Организације", | ||||
| 	"Paste": "Пасте", | ||||
| 	"Paste and Match Style": "Залепите и подесите стил", | ||||
| 	"Proxy": "Заступник", | ||||
| 	"Proxy bypass rules": "Проки бипасс правила", | ||||
| 	"Proxy rules": "Проки рулес", | ||||
| 	"Quit": "Одустати", | ||||
| 	"Quit Zulip": "Куит Зулип", | ||||
| 	"Redo": "Редо", | ||||
| 	"Release Notes": "Релеасе Нотес", | ||||
| 	"Reload": "Освежи", | ||||
| 	"Report an Issue": "Пријавите проблем", | ||||
| 	"Save": "сачувати", | ||||
| 	"Select All": "Изабери све", | ||||
| 	"Settings": "Подешавања", | ||||
| 	"Shortcuts": "Пречице", | ||||
| 	"Show App Logs": "Прикажи дневнике апликација", | ||||
| 	"Show app icon in system tray": "Покажи икону апликације у системској палети", | ||||
| 	"Show app unread badge": "Покажи непрочитану значку апликације", | ||||
| 	"Show desktop notifications": "Прикажи обавештења радне површине", | ||||
| 	"Show downloaded files in file manager": "Прикажи преузете датотеке у управитељу датотека", | ||||
| 	"Show sidebar": "Схов сидебар", | ||||
| 	"Start app at login": "Покрените апликацију приликом пријављивања", | ||||
| 	"Switch to Next Organization": "Пребаци се на следећу организацију", | ||||
| 	"Switch to Previous Organization": "Пребаци се на претходну организацију", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "Пречице за десктоп апликације проширују Зулип вебаппове", | ||||
| 	"This will delete all application data including all added accounts and preferences": "Ово ће избрисати све податке о апликацији, укључујући све додатне налоге и поставке", | ||||
| 	"Tip": "Савет", | ||||
| 	"Toggle DevTools for Active Tab": "Пребаци ДевТоолс за Ацтиве Таб", | ||||
| 	"Toggle DevTools for Zulip App": "Пребаци ДевТоолс за Зулип Апп", | ||||
| 	"Toggle Do Not Disturb": "Тоггле До Нот Дистурб", | ||||
| 	"Toggle Full Screen": "Тоггле Фулл Сцреен", | ||||
| 	"Toggle Sidebar": "Тоггле Сидебар", | ||||
| 	"Toggle Tray Icon": "Тоггле Траи Ицон", | ||||
| 	"Tools": "Алати", | ||||
| 	"Undo": "Ундо", | ||||
| 	"Upload": "Отпремити", | ||||
| 	"Use system proxy settings (requires restart)": "Користи поставке системског прокија (потребно је поново покренути)", | ||||
| 	"View": "Поглед", | ||||
| 	"View Shortcuts": "Прикажи пречице", | ||||
| 	"Window": "Прозор", | ||||
| 	"Window Shortcuts": "Пречице за прозор", | ||||
| 	"YES": "ДА", | ||||
| 	"Zoom In": "Увеличати", | ||||
| 	"Zoom Out": "Зоом Оут", | ||||
| 	"Zulip Help": "Зулип Хелп", | ||||
| 	"keyboard shortcuts": "пречице на тастатури", | ||||
| 	"script": "скрипта", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| } | ||||
| @@ -1,134 +0,0 @@ | ||||
| { | ||||
| 	"About Zulip": "Giới thiệu", | ||||
| 	"Actual Size": "Kích thước thực", | ||||
| 	"Add Custom Certificates": "Thêm chứng chỉ tự tùy chỉnh", | ||||
| 	"Add Organization": "Thêm nhóm", | ||||
| 	"Add a Zulip organization": "Thêm nhóm Zulip", | ||||
| 	"Add custom CSS": "Thêm chỉnh sửa CSS", | ||||
| 	"Advanced": "Nâng cao", | ||||
| 	"All the connected organizations will appear here": "Tất cả các nhóm đã kết nối sẽ hiển thị tại đây", | ||||
| 	"Always start minimized": "Luôn thu nhỏ", | ||||
| 	"App Updates": "Cập nhật", | ||||
| 	"Appearance": "Giao diện", | ||||
| 	"Application Shortcuts": "Phím tắt", | ||||
| 	"Are you sure you want to disconnect this organization?": "Bạn có chắc muốn ngừng kết nối với nhóm này?", | ||||
| 	"Auto hide Menu bar": "Tự động ẩn thanh công cụ", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Tự động ẩn thanh công cụ (Ấn phím Alt để hiển thị)", | ||||
| 	"Back": "Quay lại", | ||||
| 	"Bounce dock on new private message": "Bounce dock trên tin nhắn mới", | ||||
| 	"Certificate file": "Giấy chứng nhận", | ||||
| 	"Change": "Thay đổi", | ||||
| 	"Check for Updates": "Kiểm tra cập nhật", | ||||
| 	"Close": "Tắt", | ||||
| 	"Connect": "Kết nối", | ||||
| 	"Connect to another organization": "Kết nối với tổ chức khác", | ||||
| 	"Connected organizations": "Tổ chức kết nối", | ||||
| 	"Copy": "Sao chép", | ||||
| 	"Copy Zulip URL": "Sao chép đường dẫn", | ||||
| 	"Create a new organization": "Tạo nhóm mới", | ||||
| 	"Cut": "Cắt", | ||||
| 	"Default download location": "Nơi lưu tập tin mặc định", | ||||
| 	"Delete": "Xóa", | ||||
| 	"Desktop App Settings": "Desktop App Settings", | ||||
| 	"Desktop Notifications": "Thông báo trên giao diện máy tính", | ||||
| 	"Desktop Settings": "Cài đặt ứng dụng", | ||||
| 	"Disconnect": "Ngắt kết nối", | ||||
| 	"Download App Logs": "Download App Logs", | ||||
| 	"Edit": "Chỉnh sửa", | ||||
| 	"Edit Shortcuts": "Edit Shortcuts", | ||||
| 	"Enable auto updates": "Enable auto updates", | ||||
| 	"Enable error reporting (requires restart)": "Enable error reporting (requires restart)", | ||||
| 	"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", | ||||
| 	"Factory Reset": "Factory Reset", | ||||
| 	"File": "File", | ||||
| 	"Find accounts": "Find accounts", | ||||
| 	"Find accounts by email": "Find accounts by email", | ||||
| 	"Flash taskbar on new message": "Flash taskbar on new message", | ||||
| 	"Forward": "Forward", | ||||
| 	"Functionality": "Functionality", | ||||
| 	"General": "General", | ||||
| 	"Get beta updates": "Get beta updates", | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Help": "Help", | ||||
| 	"Help Center": "Help Center", | ||||
| 	"History": "History", | ||||
| 	"History Shortcuts": "History Shortcuts", | ||||
| 	"Keyboard Shortcuts": "Keyboard Shortcuts", | ||||
| 	"Log Out": "Log Out", | ||||
| 	"Log Out of Organization": "Log Out of Organization", | ||||
| 	"Manual proxy configuration": "Manual proxy configuration", | ||||
| 	"Minimize": "Minimize", | ||||
| 	"Mute all sounds from Zulip": "Mute all sounds from Zulip", | ||||
| 	"NO": "NO", | ||||
| 	"Network": "Network", | ||||
| 	"OR": "OR", | ||||
| 	"Organization URL": "Organization URL", | ||||
| 	"Organizations": "Organizations", | ||||
| 	"Paste": "Paste", | ||||
| 	"Paste and Match Style": "Paste and Match Style", | ||||
| 	"Proxy": "Proxy", | ||||
| 	"Proxy bypass rules": "Proxy bypass rules", | ||||
| 	"Proxy rules": "Proxy rules", | ||||
| 	"Quit": "Thoát", | ||||
| 	"Quit Zulip": "Thoát khỏi Zulip", | ||||
| 	"Redo": "Thực hiện lại", | ||||
| 	"Release Notes": "Release Notes", | ||||
| 	"Reload": "Tải lại", | ||||
| 	"Report an Issue": "Report an Issue", | ||||
| 	"Save": "Lưu", | ||||
| 	"Select All": "Select All", | ||||
| 	"Settings": "Cài đặt", | ||||
| 	"Shortcuts": "Shortcuts", | ||||
| 	"Show App Logs": "Show App Logs", | ||||
| 	"Show app icon in system tray": "Show app icon in system tray", | ||||
| 	"Show app unread badge": "Show app unread badge", | ||||
| 	"Show desktop notifications": "Show desktop notifications", | ||||
| 	"Show downloaded files in file manager": "Show downloaded files in file manager", | ||||
| 	"Show sidebar": "Show sidebar", | ||||
| 	"Start app at login": "Start app at login", | ||||
| 	"Switch to Next Organization": "Switch to Next Organization", | ||||
| 	"Switch to Previous Organization": "Switch to Previous Organization", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", | ||||
| 	"This will delete all application data including all added accounts and preferences": "Mọi dữ liệu trong ứng dụng, bao gồm tất cả tài khoản và tùy chỉnh được thêm vào, sẽ bị xóa", | ||||
| 	"Tip": "Mẹo", | ||||
| 	"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", | ||||
| 	"Toggle Do Not Disturb": "Toggle Do Not Disturb", | ||||
| 	"Toggle Full Screen": "Toggle Full Screen", | ||||
| 	"Toggle Sidebar": "Toggle Sidebar", | ||||
| 	"Toggle Tray Icon": "Toggle Tray Icon", | ||||
| 	"Tools": "Công cụ", | ||||
| 	"Undo": "Hủy thay đổi", | ||||
| 	"Upload": "Tải lên", | ||||
| 	"Use system proxy settings (requires restart)": "Chọn cài đặt system proxy (Yêu cầu khởi động lại)", | ||||
| 	"View": "Xem", | ||||
| 	"View Shortcuts": "Hiển thị phím tắt", | ||||
| 	"Window": "Ứng dụng Window", | ||||
| 	"Window Shortcuts": "Phím tắt trong Windows", | ||||
| 	"YES": "Có", | ||||
| 	"Zoom In": "Phóng to", | ||||
| 	"Zoom Out": "Thu nhỏ", | ||||
| 	"Zulip Help": "Trợ giúp", | ||||
| 	"keyboard shortcuts": "Phím tắt bàn phím", | ||||
| 	"script": "script", | ||||
| 	"Quit when the window is closed": "Thoát khi cửa sổ tắt", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
|   <dict> | ||||
|     <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | ||||
|     <true/> | ||||
|   </dict> | ||||
| </plist> | ||||
| @@ -1,16 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>com.apple.security.app-sandbox</key> | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.network.client</key> | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.files.user-selected.read-only</key> | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.files.user-selected.read-write</key> | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.files.downloads.read-write</key> | ||||
| 	<true/> | ||||
| </dict> | ||||
| </plist> | ||||
							
								
								
									
										113
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						| @@ -2,6 +2,97 @@ | ||||
|  | ||||
| All notable changes to the Zulip desktop app are documented in this file. | ||||
|  | ||||
| ### v5.10.5 --2024-01-25 | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 28.2.0. | ||||
|  | ||||
| **Enhancements**: | ||||
|  | ||||
| - Improved security hardening by setting a Content-Security-Policy for the app UI. | ||||
|  | ||||
| ### v5.10.4 --2024-01-09 | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 28.1.1. | ||||
|  | ||||
| ### v5.10.3 --2023-09-30 | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - Fixed an error in the third-party `gatemaker` library that broke the display of notifications for completed downloads. | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 25.8.4. | ||||
|  | ||||
| ### v5.10.2 --2023-09-14 | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Downgraded Electron to 25.8.1 to avoid a renderer process crash on Linux. | ||||
|  | ||||
| ### v5.10.1 --2023-09-13 | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 26.2.1. | ||||
|  | ||||
| ### v5.10.0 --2023-05-05 | ||||
|  | ||||
| **Removed features**: | ||||
|  | ||||
| - Removed support for Windows 8.1 and earlier, which reached end-of-life earlier this year and are [no longer supported](https://www.electronjs.org/blog/windows-7-to-8-1-deprecation-notice) by Electron. | ||||
| - Removed support for Zulip Server 3.x and earlier, which have been obsolete for more than 18 months, in accordance with our [release lifecycle](https://zulip.readthedocs.io/en/latest/overview/release-lifecycle.html). A notice will now be displayed when connecting to a server with an unsupported version. | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - Fixed display of the dropdown for the spellchecker languages setting. | ||||
| - Fixed various bugs related to displaying and updating organization icons. | ||||
| - Fixed settings to disable visual display of notifications. | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 24.2.0. | ||||
|  | ||||
| ### v5.9.5 --2023-02-06 | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - Fixed a hang on startup when an organization cannot be connected at startup. | ||||
|  | ||||
| **Enhancements**: | ||||
|  | ||||
| - Enabled Chromium sandboxing in remote renderer processes for improved security hardening. | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 22.2.0. | ||||
|  | ||||
| ### v5.9.4 --2023-01-04 | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - The `com.apple.quarantine` extended attribute is now correctly set for downloaded files on macOS. | ||||
| - The external link handler ignores invalid URLs. | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 22.0.0. | ||||
|  | ||||
| ### v5.9.3 --2022-04-28 | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - Fixed a bug in the automatic updater that would sometimes close the application instead of updating it. | ||||
|   (As with most updater fixes, this fix will take effect when updating _from_ 5.9.3. If you're having trouble updating _to_ 5.9.3, a workaround is to click **Install Later** rather than **Install and Relaunch**, then **Quit** from the menu bar and re-open the application manually.) | ||||
|  | ||||
| **Dependencies**: | ||||
|  | ||||
| - Upgraded all dependencies, including Electron 18.2.0. | ||||
|  | ||||
| ### v5.9.2 --2022-04-20 | ||||
|  | ||||
| **Dependencies**: | ||||
| @@ -157,10 +248,10 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| **New features**: | ||||
|  | ||||
| - Add a cancel button in the report-issue modal. | ||||
| - macOS: Use electron API to get dark tray icon instead of the green icon for the light theme. | ||||
| - macOS: Use Electron API to get dark tray icon instead of the green icon for the light theme. | ||||
| - Remove 'Reset App Data' option. Factory Reset option has been moved to Settings → General. | ||||
| - Support pkg installer on macOS. | ||||
| - Use electron 8 built-in spellchecker. Linux and Windows users can now choose upto three spellchecker languages from Settings → General. On macOS, default spellchecker is used. | ||||
| - Use Electron 8 built-in spellchecker. Linux and Windows users can now choose up to three spellchecker languages from Settings → General. On macOS, default spellchecker is used. | ||||
| - Setup Transifex for better synchronization of translations. The application now supports 41 languages instead of 21. | ||||
|  | ||||
| **Dependencies**: | ||||
| @@ -268,7 +359,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| - Document enterprise configuration features. | ||||
| - Update the Electron tutorial guide. | ||||
| - Explicitly address where to report bugs in `README.md`. | ||||
| - Fix typo in the link to server/webapp repository in `README.md`. | ||||
| - Fix typo in the link to server/web app repository in `README.md`. | ||||
| - Add documentation for translation. | ||||
|  | ||||
| ### v4.0.0 --2019-08-08 | ||||
| @@ -319,7 +410,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| **Development**: | ||||
|  | ||||
| - Migrate codebase to TypeScript. | ||||
| - Set the indent_size in `.editconfig` to 4. | ||||
| - Set the indent_size in `.editorconfig` to 4. | ||||
| - Use `.env` file for reading Sentry DSN. | ||||
|  | ||||
| **Documentation**: | ||||
| @@ -388,7 +479,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| - Fix typo in network error message. | ||||
| - Fix context menu not working on adding new org. | ||||
| - Fix reply from notification. | ||||
| - Fix shorcut section horizontal alignment. | ||||
| - Fix shortcut section horizontal alignment. | ||||
| - Fix broken link in docs. | ||||
| - Fix grammatical errors. | ||||
| - Fix typo error in issue template. | ||||
| @@ -427,7 +518,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| - Auto hide menubar on Windows/Linux. Add a setting option for the same. | ||||
| - Improve design of setting page. | ||||
| - Toggle app on clicking the tray icon (Linux). | ||||
| - Update sidebar realm name when it's changed in webapp. | ||||
| - Update sidebar realm name when it's changed in web app. | ||||
| - left-sidebar: Add initial character of realm name instead of default icon. | ||||
|  | ||||
| **Fixes**: | ||||
| @@ -464,7 +555,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| - Fix youtube video not playing in lightbox. | ||||
| - Fix YouTube video not playing in lightbox. | ||||
| - Fix realm name not escaped properly. | ||||
|  | ||||
| <hr> | ||||
| @@ -474,7 +565,7 @@ All notable changes to the Zulip desktop app are documented in this file. | ||||
| **New features**: | ||||
|  | ||||
| - Add a setting option to show downloaded file in file manager. | ||||
| - Added electron bridge to communicate with webapp in real time. | ||||
| - Added Electron bridge to communicate with web app in real time. | ||||
|  | ||||
| **Fixes**: | ||||
|  | ||||
| @@ -587,7 +678,7 @@ electron-updater - `v2.21.8` | ||||
|  | ||||
| - Add an option to download the file attachments instead of opening it in the browser | ||||
|  | ||||
| - Open image link in webapp lightbox | ||||
| - Open image link in web app lightbox | ||||
|  | ||||
| - Add scrollbar for list of organizations on overflow | ||||
|  | ||||
| @@ -624,7 +715,7 @@ electron-updater - `v2.21.8` | ||||
|  | ||||
| - Some users wanted to change the look of the Zulip. Now you have the power. Feel free to add your own CSS using the all-new setting option **Add Custom CSS** | ||||
|  | ||||
| - Added i18n locale helper script. Internalization is coming in the next release | ||||
| - Added i18n locale helper script. Internationalization is coming in the next release | ||||
|  | ||||
| - Added **What's new** in `help` submenu so that you can see all the latest changes in the app | ||||
|  | ||||
| @@ -1066,7 +1157,7 @@ Minor improvements | ||||
|  | ||||
| - Using two package.json structure | ||||
|  | ||||
| - Node integration disabled in main window due to jquery error | ||||
| - Node integration disabled in main window due to jQuery error | ||||
|  | ||||
| - Now using electron-builder for packaging instead of electron-packager | ||||
|  | ||||
|   | ||||
| @@ -49,7 +49,7 @@ If [NPM](https://www.npmjs.com/get-npm) and [node-gyp](https://github.com/nodejs | ||||
|  | ||||
| [node-windows]: https://nodejs.org/en/download/package-manager/#windows | ||||
|  | ||||
| - Also, install install Windows-Build-Tools to compile native node modules by using | ||||
| - Also, install Windows-Build-Tools to compile native node modules by using | ||||
|   ```sh | ||||
|   $ npm install --global windows-build-tools | ||||
|   ``` | ||||
|   | ||||
| @@ -5,13 +5,13 @@ | ||||
| - [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git) | ||||
| - [Node.js](https://nodejs.org) >= v6.9.0 | ||||
| - [python](https://www.python.org/downloads/release/python-2713/) (v2.7.x recommended) | ||||
| - [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via powershell) | ||||
| - [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via PowerShell) | ||||
|  | ||||
| ## System specific dependencies | ||||
|  | ||||
| - use only 32bit or 64bit for all of the installers, do not mix architectures | ||||
| - install using default settings | ||||
| - open Windows Powershell as Admin | ||||
| - open Windows PowerShell as Admin | ||||
|  | ||||
| ```powershell | ||||
| C:\Windows\system32> npm install --global --production windows-build-tools | ||||
|   | ||||
| @@ -38,7 +38,7 @@ You'll want Transifex's CLI client, `tx`. | ||||
|  | ||||
| Run `tx push -s`. | ||||
|  | ||||
| This uploads from `app/translations/en.json` to the | ||||
| This uploads from `public/translations/en.json` to the | ||||
| set of strings Transifex shows for contributors to translate. | ||||
| (See `.tx/config` for how that's configured.) | ||||
|  | ||||
| @@ -46,7 +46,7 @@ set of strings Transifex shows for contributors to translate. | ||||
|  | ||||
| Run `tools/tx-pull`. | ||||
|  | ||||
| This writes to files `app/translations/<lang>.json`. | ||||
| This writes to files `public/translations/<lang>.json`. | ||||
| (See `.tx/config` for how that's configured.) | ||||
|  | ||||
| Then look at the following sections to see if further updates are | ||||
| @@ -59,7 +59,7 @@ language. This happens when we've opened up a new language for people | ||||
| to contribute translations into in the Zulip project on Transifex, | ||||
| which we do when someone expresses interest in contributing them. | ||||
|  | ||||
| The locales for supported languages are stored in `app/translations/supported-locales.json` | ||||
| The locales for supported languages are stored in `public/translations/supported-locales.json` | ||||
|  | ||||
| So, when a new language is added, update the `supported-locales` module. | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|  | ||||
| [lr]: https://github.com/zulip/zulip-desktop/releases | ||||
|  | ||||
| ## OS X | ||||
| ## macOS | ||||
|  | ||||
| **DMG or zip**: | ||||
|  | ||||
| @@ -17,7 +17,7 @@ | ||||
|  | ||||
| **Using brew**: | ||||
|  | ||||
| 1. Run `brew cask install zulip` in your terminal | ||||
| 1. Run `brew install --cask zulip` in your terminal | ||||
| 2. The app will be installed in your `Applications` | ||||
| 3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`) | ||||
|  | ||||
| @@ -53,20 +53,20 @@ | ||||
|  | ||||
| - First download our signing key to make sure the deb you download is correct: | ||||
|  | ||||
| ``` | ||||
| sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9 | ||||
| ```bash | ||||
| sudo apt-key adv --keyserver keyserver.ubuntu.com --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9 | ||||
| ``` | ||||
|  | ||||
| - Add the repo to your apt source list : | ||||
|  | ||||
| ``` | ||||
| echo "deb https://dl.bintray.com/zulip/debian/ beta main" | | ||||
| ```bash | ||||
| echo "deb https://download.zulip.com/desktop/apt stable main" | | ||||
|   sudo tee -a /etc/apt/sources.list.d/zulip.list | ||||
| ``` | ||||
|  | ||||
| - Now install the client : | ||||
|  | ||||
| ``` | ||||
| ```bash | ||||
| sudo apt-get update | ||||
| sudo apt-get install zulip | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										16740
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										123
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,8 +1,8 @@ | ||||
| { | ||||
|   "name": "zulip", | ||||
|   "productName": "Zulip", | ||||
|   "version": "5.9.2", | ||||
|   "main": "./app/main", | ||||
|   "version": "5.10.5", | ||||
|   "main": "./dist-electron", | ||||
|   "description": "Zulip Desktop App", | ||||
|   "license": "Apache-2.0", | ||||
|   "copyright": "Kandra Labs, Inc.", | ||||
| @@ -18,36 +18,34 @@ | ||||
|     "url": "https://github.com/zulip/zulip-desktop/issues" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=12.10.0" | ||||
|     "node": ">=16.13.2" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "tsc && electron .", | ||||
|     "clean-ts-files": "git clean \"app/*.js\" -xf", | ||||
|     "start": "vite", | ||||
|     "watch-ts": "tsc -w", | ||||
|     "reinstall": "rimraf node_modules && npm install", | ||||
|     "postinstall": "electron-builder install-app-deps", | ||||
|     "lint-css": "stylelint \"app/**/*.css\"", | ||||
|     "lint-html": "htmlhint \"app/**/*.html\"", | ||||
|     "lint-js": "xo", | ||||
|     "prettier-non-js": "prettier --check --loglevel=warn . \"!**/*.{js,ts}\"", | ||||
|     "test": "tsc --noEmit && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js", | ||||
|     "test-e2e": "tsc && tape \"tests/**/*.js\"", | ||||
|     "pack": "tsc && electron-builder --dir", | ||||
|     "dist": "tsc && electron-builder", | ||||
|     "mas": "tsc && electron-builder --mac mas" | ||||
|     "prettier-non-js": "prettier --check --log-level=warn . \"!**/*.{js,ts}\"", | ||||
|     "test": "tsc && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js", | ||||
|     "test-e2e": "vite build && tape \"tests/**/*.js\"", | ||||
|     "pack": "vite build && electron-builder --dir", | ||||
|     "dist": "vite build && electron-builder", | ||||
|     "mas": "vite build && electron-builder --mac mas" | ||||
|   }, | ||||
|   "pre-commit": [ | ||||
|     "test" | ||||
|   ], | ||||
|   "build": { | ||||
|     "afterSign": "./scripts/notarize.js", | ||||
|     "appId": "org.zulip.zulip-electron", | ||||
|     "asar": true, | ||||
|     "asarUnpack": [ | ||||
|       "**/*.node" | ||||
|     ], | ||||
|     "files": [ | ||||
|       "app/**/*" | ||||
|       "dist-electron/**/*" | ||||
|     ], | ||||
|     "copyright": "©2020 Kandra Labs, Inc.", | ||||
|     "mac": { | ||||
| @@ -70,10 +68,9 @@ | ||||
|       ], | ||||
|       "darkModeSupport": true, | ||||
|       "artifactName": "${productName}-${version}-${arch}.${ext}", | ||||
|       "hardenedRuntime": true, | ||||
|       "entitlements": "build/entitlements.mac.plist", | ||||
|       "entitlementsInherit": "build/entitlements.mac.plist", | ||||
|       "gatekeeperAssess": false | ||||
|       "notarize": { | ||||
|         "teamId": "66KHCWMEYB" | ||||
|       } | ||||
|     }, | ||||
|     "linux": { | ||||
|       "category": "Chat;GNOME;GTK;Network;InstantMessaging", | ||||
| @@ -146,48 +143,47 @@ | ||||
|     "InstantMessaging" | ||||
|   ], | ||||
|   "dependencies": { | ||||
|     "@electron/remote": "^2.0.8", | ||||
|     "@sentry/electron": "^3.0.3", | ||||
|     "@yaireo/tagify": "^4.5.0", | ||||
|     "adm-zip": "^0.5.5", | ||||
|     "auto-launch": "^5.0.5", | ||||
|     "backoff": "^2.5.0", | ||||
|     "electron-log": "^4.3.5", | ||||
|     "electron-updater": "^5.0.1", | ||||
|     "electron-window-state": "^5.0.3", | ||||
|     "escape-goat": "^3.0.0", | ||||
|     "get-stream": "^6.0.1", | ||||
|     "i18n": "^0.14.1", | ||||
|     "iso-639-1": "^2.1.9", | ||||
|     "node-json-db": "^1.3.0", | ||||
|     "semver": "^7.3.5", | ||||
|     "zod": "^3.5.1" | ||||
|     "gatemaker": "https://github.com/andersk/gatemaker/archive/d31890ae1cb293faabcb1e4e465c673458f6eed2.tar.gz" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@electron/remote": "^2.0.8", | ||||
|     "@sentry/core": "^7.94.1", | ||||
|     "@sentry/electron": "^4.1.2", | ||||
|     "@types/adm-zip": "^0.5.0", | ||||
|     "@types/auto-launch": "^5.0.2", | ||||
|     "@types/backoff": "^2.5.2", | ||||
|     "@types/i18n": "^0.13.1", | ||||
|     "@types/node": "^16.11.26", | ||||
|     "@types/node": "~18.17.19", | ||||
|     "@types/requestidlecallback": "^0.3.4", | ||||
|     "@types/yaireo__tagify": "^4.3.2", | ||||
|     "dotenv": "^16.0.0", | ||||
|     "electron": "^18.0.1", | ||||
|     "electron-builder": "^23.0.3", | ||||
|     "electron-notarize": "^1.0.0", | ||||
|     "eslint-import-resolver-typescript": "^2.4.0", | ||||
|     "@yaireo/tagify": "^4.5.0", | ||||
|     "adm-zip": "^0.5.5", | ||||
|     "auto-launch": "^5.0.5", | ||||
|     "backoff": "^2.5.0", | ||||
|     "electron": "^28.1.1", | ||||
|     "electron-builder": "^24.6.4", | ||||
|     "electron-log": "^5.0.3", | ||||
|     "electron-updater": "^6.1.4", | ||||
|     "electron-window-state": "^5.0.3", | ||||
|     "escape-goat": "^4.0.0", | ||||
|     "htmlhint": "^1.1.2", | ||||
|     "i18n": "^0.15.1", | ||||
|     "iso-639-1": "^3.1.0", | ||||
|     "medium": "^1.2.0", | ||||
|     "playwright-core": "^1.19.1", | ||||
|     "node-json-db": "^1.3.0", | ||||
|     "playwright-core": "^1.41.0-alpha-jan-9-2024", | ||||
|     "pre-commit": "^1.2.2", | ||||
|     "prettier": "^2.3.2", | ||||
|     "rimraf": "^3.0.2", | ||||
|     "stylelint": "^14.5.3", | ||||
|     "stylelint-config-prettier": "^9.0.3", | ||||
|     "stylelint-config-standard": "^25.0.0", | ||||
|     "prettier": "^3.0.3", | ||||
|     "rimraf": "^5.0.0", | ||||
|     "semver": "^7.3.5", | ||||
|     "stylelint": "^16.1.0", | ||||
|     "stylelint-config-standard": "^36.0.0", | ||||
|     "tape": "^5.2.2", | ||||
|     "typescript": "^4.3.5", | ||||
|     "xo": "^0.48.0" | ||||
|     "typescript": "^5.0.4", | ||||
|     "vite": "^5.0.11", | ||||
|     "vite-plugin-electron": "^0.28.0", | ||||
|     "xo": "^0.56.0", | ||||
|     "zod": "^3.5.1" | ||||
|   }, | ||||
|   "prettier": { | ||||
|     "bracketSpacing": false, | ||||
| @@ -199,15 +195,6 @@ | ||||
|     "rules": { | ||||
|       "@typescript-eslint/no-dynamic-delete": "off", | ||||
|       "arrow-body-style": "error", | ||||
|       "import/extensions": [ | ||||
|         "error", | ||||
|         "always", | ||||
|         { | ||||
|           "pattern": { | ||||
|             "ts": "never" | ||||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "import/no-restricted-paths": [ | ||||
|         "error", | ||||
|         { | ||||
| @@ -216,8 +203,7 @@ | ||||
|               "target": "./app/common", | ||||
|               "from": "./app", | ||||
|               "except": [ | ||||
|                 "./common", | ||||
|                 "./translations" | ||||
|                 "./common" | ||||
|               ] | ||||
|             }, | ||||
|             { | ||||
| @@ -225,8 +211,7 @@ | ||||
|               "from": "./app", | ||||
|               "except": [ | ||||
|                 "./common", | ||||
|                 "./main", | ||||
|                 "./translations" | ||||
|                 "./main" | ||||
|               ] | ||||
|             }, | ||||
|             { | ||||
| @@ -235,7 +220,7 @@ | ||||
|               "except": [ | ||||
|                 "./common", | ||||
|                 "./renderer", | ||||
|                 "./translations" | ||||
|                 "./resources" | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
| @@ -255,6 +240,10 @@ | ||||
|         "error", | ||||
|         { | ||||
|           "paths": [ | ||||
|             { | ||||
|               "name": "@sentry/electron", | ||||
|               "message": "Use @sentry/electron/main, @sentry/electron/renderer, or @sentry/core." | ||||
|             }, | ||||
|             { | ||||
|               "name": "electron", | ||||
|               "message": "Use electron/main, electron/renderer, or electron/common." | ||||
| @@ -272,6 +261,10 @@ | ||||
|                 "ipcRenderer" | ||||
|               ], | ||||
|               "message": "Use typed-ipc-renderer." | ||||
|             }, | ||||
|             { | ||||
|               "name": "electron-log", | ||||
|               "message": "Use electron-log/main or electron-log/renderer." | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
| @@ -284,8 +277,8 @@ | ||||
|         } | ||||
|       ], | ||||
|       "strict": "error", | ||||
|       "unicorn/prefer-json-parse-buffer": "off", | ||||
|       "unicorn/prefer-module": "off" | ||||
|       "unicorn/prefer-module": "off", | ||||
|       "unicorn/prefer-top-level-await": "off" | ||||
|     }, | ||||
|     "envs": [ | ||||
|       "node", | ||||
| @@ -312,14 +305,10 @@ | ||||
|             } | ||||
|           ], | ||||
|           "unicorn/no-await-expression-member": "off" | ||||
|         }, | ||||
|         "settings": { | ||||
|           "import/resolver": "typescript" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "files": [ | ||||
|           "app/renderer/js/injected.ts", | ||||
|           "scripts/notarize.js", | ||||
|           "tests/**/*.js" | ||||
|         ], | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| # Link to the binary | ||||
| ln -sf '/opt/${productFilename}/${executable}' '/usr/bin/${executable}' | ||||
| ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' | ||||
|  | ||||
| # SUID chrome-sandbox for Electron 5+ | ||||
| chmod 4755 '/opt/${productFilename}/chrome-sandbox' || true | ||||
| chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || true | ||||
|  | ||||
| update-mime-database /usr/share/mime || true | ||||
| update-desktop-database /usr/share/applications || true | ||||
|   | ||||
| Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB | 
| Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 321 B | 
| Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B | 
| Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB | 
| Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB | 
| @@ -1,45 +1,49 @@ | ||||
| { | ||||
| 	"About Zulip": "حول \"زوليب\"", | ||||
| 	"Actual Size": "الحجم الفعلي", | ||||
| 	"Add Custom Certificates": "إضافة رخصة معدلة", | ||||
| 	"Add Organization": "إضافة منظمة", | ||||
| 	"Add a Zulip organization": "إضافة منظمة \"زوليب\"", | ||||
| 	"Add custom CSS": "إصافة CSS معدلة", | ||||
| 	"Add custom CSS": "إضافة CSS معدلة", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"Advanced": "متقدم", | ||||
| 	"All the connected organizations will appear here": "جميع المنظمات المتصلة تعرض هنا", | ||||
| 	"All the connected organizations will appear here.": "All the connected organizations will appear here.", | ||||
| 	"Always start minimized": "دائماً إبدأ بالقليل", | ||||
| 	"App Updates": "تحديث التطبيق", | ||||
| 	"App Updates": "تحديثات التطبيق", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Appearance": "المظهر", | ||||
| 	"Application Shortcuts": "إختصارات التطبيق", | ||||
| 	"Are you sure you want to disconnect this organization?": "هل أنت متأكد من فصل هذة المنظمة؟", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Auto hide Menu bar": "أخف القائمة تلقائياً", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "أخف القائمة تلقائياً (إضغط Alt لعرض القائمة)", | ||||
| 	"Back": "رجوع", | ||||
| 	"Bounce dock on new private message": "أخرج المنصة في حال رسالة خاصة جديدة", | ||||
| 	"Certificate file": "ملف الشهادة", | ||||
| 	"Change": "تغيير", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Check for Updates": "التحقق من التحديثات", | ||||
| 	"Close": "اغلاق", | ||||
| 	"Close": "إغلاق", | ||||
| 	"Connect": "اتصال", | ||||
| 	"Connect to another organization": "التوصيل مع منظمة أخرى", | ||||
| 	"Connected organizations": "المنظمات المتصلة", | ||||
| 	"Copy": "نسخ", | ||||
| 	"Copy Zulip URL": "نسخ رابط زوليب", | ||||
| 	"Create a new organization": "Create a new organization", | ||||
| 	"Create a new organization": "إنشاء منظمة جديدة", | ||||
| 	"Cut": "قص", | ||||
| 	"Default download location": "موقع التحميل الافتراضي", | ||||
| 	"Delete": "حذف", | ||||
| 	"Desktop App Settings": "إعدادت تطبيق سطح المكتب", | ||||
| 	"Desktop Notifications": "إشعارات سطح المكتب", | ||||
| 	"Desktop Settings": "إعدادات سطح المكتب", | ||||
| 	"Disconnect": "قطع الاتصال", | ||||
| 	"Download App Logs": "تنزيل سجلات التطبيق", | ||||
| 	"Edit": "تعديل", | ||||
| 	"Edit Shortcuts": "تعديل الاختصارات", | ||||
| 	"Emoji & Symbols": "Emoji & Symbols", | ||||
| 	"Enable auto updates": "تفعيل التحديثات التلقائية", | ||||
| 	"Enable error reporting (requires restart)": "تفعيل تقارير الأخطاء (يتطلب إعادة التشغيل)", | ||||
| 	"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", | ||||
| 	"Enter Full Screen": "Enter Full Screen", | ||||
| 	"Factory Reset": "إعادة ضبط المصنع", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"File": "ملف", | ||||
| 	"Find accounts": "Find accounts", | ||||
| 	"Find accounts by email": "Find accounts by email", | ||||
| @@ -51,6 +55,9 @@ | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Help": "Help", | ||||
| 	"Help Center": "Help Center", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Hide Zulip": "Hide Zulip", | ||||
| 	"History": "History", | ||||
| 	"History Shortcuts": "History Shortcuts", | ||||
| 	"Keyboard Shortcuts": "Keyboard Shortcuts", | ||||
| @@ -61,7 +68,9 @@ | ||||
| 	"Mute all sounds from Zulip": "Mute all sounds from Zulip", | ||||
| 	"NO": "NO", | ||||
| 	"Network": "Network", | ||||
| 	"Network and Proxy Settings": "Network and Proxy Settings", | ||||
| 	"OR": "OR", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Organization URL": "Organization URL", | ||||
| 	"Organizations": "Organizations", | ||||
| 	"Paste": "Paste", | ||||
| @@ -71,25 +80,27 @@ | ||||
| 	"Proxy rules": "Proxy rules", | ||||
| 	"Quit": "Quit", | ||||
| 	"Quit Zulip": "Quit Zulip", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Redo": "Redo", | ||||
| 	"Release Notes": "Release Notes", | ||||
| 	"Reload": "Reload", | ||||
| 	"Report an Issue": "Report an Issue", | ||||
| 	"Reset App Settings": "Reset App Settings", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", | ||||
| 	"Save": "Save", | ||||
| 	"Select All": "Select All", | ||||
| 	"Services": "Services", | ||||
| 	"Settings": "Settings", | ||||
| 	"Shortcuts": "Shortcuts", | ||||
| 	"Show App Logs": "Show App Logs", | ||||
| 	"Show app icon in system tray": "Show app icon in system tray", | ||||
| 	"Show app unread badge": "Show app unread badge", | ||||
| 	"Show desktop notifications": "Show desktop notifications", | ||||
| 	"Show downloaded files in file manager": "Show downloaded files in file manager", | ||||
| 	"Show sidebar": "Show sidebar", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Start app at login": "Start app at login", | ||||
| 	"Switch to Next Organization": "Switch to Next Organization", | ||||
| 	"Switch to Previous Organization": "Switch to Previous Organization", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", | ||||
| 	"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences", | ||||
| 	"Tip": "Tip", | ||||
| 	"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", | ||||
| @@ -99,6 +110,7 @@ | ||||
| 	"Toggle Tray Icon": "Toggle Tray Icon", | ||||
| 	"Tools": "Tools", | ||||
| 	"Undo": "Undo", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"Upload": "Upload", | ||||
| 	"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", | ||||
| 	"View": "View", | ||||
| @@ -106,29 +118,10 @@ | ||||
| 	"Window": "Window", | ||||
| 	"Window Shortcuts": "Window Shortcuts", | ||||
| 	"YES": "YES", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Zoom In": "Zoom In", | ||||
| 	"Zoom Out": "Zoom Out", | ||||
| 	"Zulip Help": "Zulip Help", | ||||
| 	"keyboard shortcuts": "keyboard shortcuts", | ||||
| 	"script": "script", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app." | ||||
| } | ||||
							
								
								
									
										127
									
								
								public/translations/be.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | ||||
| { | ||||
| 	"About Zulip": "Пра Zulip", | ||||
| 	"Actual Size": "Сапраўдны памер", | ||||
| 	"Add Organization": "Дадаць арганізацыю", | ||||
| 	"Add a Zulip organization": "Дадаць арганізацыю Zulip", | ||||
| 	"Add custom CSS": "Дадаць свой CSS", | ||||
| 	"AddServer": "Дадаць сэрвер", | ||||
| 	"Advanced": "Пашыраныя", | ||||
| 	"All the connected organizations will appear here.": "Тут з'явяцца ўсе звязаныя арганізацыі.", | ||||
| 	"Always start minimized": "Заўсёды адкрываць згорнутым", | ||||
| 	"App Updates": "Абнаўленні праграмы", | ||||
| 	"App language (requires restart)": "Мова праграмы (патрабуецца перазапуск)", | ||||
| 	"Appearance": "Выгляд", | ||||
| 	"Application Shortcuts": "Спалучэнні клавішаў", | ||||
| 	"Are you sure you want to disconnect this organization?": "Вы ўпэўненыя, што хочаце адключыць гэту арганізацыю?", | ||||
| 	"Ask where to save files before downloading": "Спытаць, куды захоўваць файлы перад сцягваннем", | ||||
| 	"Auto hide Menu bar": "Аўтаматычна хаваць радок меню", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Аўтаматычна хаваць радок меню (для выявы націсніце клавішу Alt)", | ||||
| 	"Back": "Назад", | ||||
| 	"Bounce dock on new private message": "Подпрыгваючы dock пры новым асабістым паведамленні", | ||||
| 	"Change": "Змяніць", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Змяніце мову ў: Сістэмныя налады → Клавіятура → Тэкст → Правапіс.", | ||||
| 	"Check for Updates": "Праверыць наяўнасць абнаўленняў", | ||||
| 	"Close": "Закрыць", | ||||
| 	"Connect": "Падлучыць", | ||||
| 	"Connect to another organization": "Падлучыць да іншай арганізацыі", | ||||
| 	"Connected organizations": "Падлучаныя арганізацыі", | ||||
| 	"Copy": "Капіяваць", | ||||
| 	"Copy Zulip URL": "Капіяваць Zulip URL", | ||||
| 	"Create a new organization": "Стварыць новую арганізацыю", | ||||
| 	"Cut": "Выразаць", | ||||
| 	"Default download location": "Месца сцягвання па змаўчанні", | ||||
| 	"Delete": "Выдаліць", | ||||
| 	"Desktop Notifications": "Апавяшчэнні для ПК", | ||||
| 	"Desktop Settings": "Налады для ПК", | ||||
| 	"Disconnect": "Адлучыць", | ||||
| 	"Download App Logs": "Сцягнуць журналы праграмаў", | ||||
| 	"Edit": "Рэдагаваць", | ||||
| 	"Edit Shortcuts": "Рэдагаваць cпалучэнні клавішаў", | ||||
| 	"Emoji & Symbols": "Эмодзі і сімвалы", | ||||
| 	"Enable auto updates": "Увамкнуць аўтаматычнае абнаўленне", | ||||
| 	"Enable error reporting (requires restart)": "Увамкнуць справаздачу аб памылках (патрабуецца перазапуск)", | ||||
| 	"Enable spellchecker (requires restart)": "Увамкнуць праверку правапісу (патрабуецца перазапуск)", | ||||
| 	"Enter Full Screen": "Пераход у поўнаэкранны рэжым", | ||||
| 	"Factory Reset": "Аднаўленне заводскіх наладаў", | ||||
| 	"Factory Reset Data": "Аднаўленне даных да заводскіх наладаў", | ||||
| 	"File": "Файл", | ||||
| 	"Find accounts": "Знайсці ўліковыя запісы", | ||||
| 	"Find accounts by email": "Знайсці ўліковыя запісы паводле email", | ||||
| 	"Flash taskbar on new message": "Успыхваць на панэлі заданняў пры новым асабістым паведамленні ", | ||||
| 	"Forward": "Пераадрасаваць", | ||||
| 	"Functionality": "Функцыянальнасць", | ||||
| 	"General": "Агульныя", | ||||
| 	"Get beta updates": "Атрымлівць бэта-абнаўленні", | ||||
| 	"Hard Reload": "Апаратнае пераладаванне", | ||||
| 	"Help": "Даведка", | ||||
| 	"Help Center": "Цэнтр даведак", | ||||
| 	"Hide": "Схаваць", | ||||
| 	"Hide Others": "Схаваць іншыя", | ||||
| 	"Hide Zulip": "Схаваць Zulip", | ||||
| 	"History": "Гісторыя", | ||||
| 	"History Shortcuts": "Гісторыя cпалучэнняў клавішаў", | ||||
| 	"Keyboard Shortcuts": "Спалучэнні клавішаў", | ||||
| 	"Log Out": "Выйсці з уліковага запісу", | ||||
| 	"Log Out of Organization": "Выйсці з уліковага запісу арганізацыі", | ||||
| 	"Manual proxy configuration": "Ручная налада проксі", | ||||
| 	"Minimize": "Згарнуць", | ||||
| 	"Mute all sounds from Zulip": "Адключыць усе гукі з Zulip", | ||||
| 	"NO": "NO", | ||||
| 	"Network": "Сетка", | ||||
| 	"Network and Proxy Settings": "Налады сеткі і проксі", | ||||
| 	"OR": "OR", | ||||
| 	"On macOS, the OS spellchecker is used.": "У macOS выкарыстоўваецца сістэмная праверка правапісу.", | ||||
| 	"Organization URL": "URL арганізацыі", | ||||
| 	"Organizations": "Арганізацыі", | ||||
| 	"Paste": "Уставіць", | ||||
| 	"Paste and Match Style": "Уставіць і ўзгадніць стыль", | ||||
| 	"Proxy": "Проксі", | ||||
| 	"Proxy bypass rules": "Правілы абыходу проксі", | ||||
| 	"Proxy rules": "Правілы проксі", | ||||
| 	"Quit": "Выйсці", | ||||
| 	"Quit Zulip": "Выйсці з Zulip", | ||||
| 	"Quit when the window is closed": "Выйсці, калі акно зачыненае", | ||||
| 	"Redo": "Узнавіць", | ||||
| 	"Release Notes": "Заўвагі да выпуску", | ||||
| 	"Reload": "Пераладаваць", | ||||
| 	"Report an Issue": "Паведаміць аб праблеме", | ||||
| 	"Reset App Settings": "Скінуць налады праграмы", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Скінуць усю праграму, выдаліўшы такім чынам усе звязаныя арганізацыі і ўліковыя запісы.", | ||||
| 	"Save": "Захаваць", | ||||
| 	"Select All": "Выбраць усё", | ||||
| 	"Services": "Сэрвісы", | ||||
| 	"Settings": "Налады", | ||||
| 	"Shortcuts": "Спалучэнні клавішаў", | ||||
| 	"Show app icon in system tray": "Паказаць значок праграмы ў вобласці паведамленняў", | ||||
| 	"Show app unread badge": "Паказваць значок непрачытаных паведамленняў", | ||||
| 	"Show desktop notifications": "Паказваць апавяшчэнні на працоўным стале", | ||||
| 	"Show sidebar": "Паказваць бакавую панэль", | ||||
| 	"Spellchecker Languages": "Мовы для праверкі правапісу", | ||||
| 	"Start app at login": "Запусціць праграму пры ўваходзе ва ўліковы запіс", | ||||
| 	"Switch to Next Organization": "Пераключыцца на наступную арганізацыю", | ||||
| 	"Switch to Previous Organization": "Пераключыцца на папярэднюю арганізацыю", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "Гэтыя спалучэнні клавішаў пашыраюць магчымасці Zulip", | ||||
| 	"Tip": "Парада", | ||||
| 	"Toggle DevTools for Active Tab": "Увамкнуць DevTools для актыўнай укладкі", | ||||
| 	"Toggle DevTools for Zulip App": "Перамкнуць DevTools для праграмы Zulip", | ||||
| 	"Toggle Do Not Disturb": "Перамкнуць рэжым \"Не турбаваць\"", | ||||
| 	"Toggle Full Screen": "Перамкнуць \"На ўвесь экран\"", | ||||
| 	"Toggle Sidebar": "Перамкнуць бакавую панэль", | ||||
| 	"Toggle Tray Icon": "Перамкнуць значок у вобласці паведамленняў", | ||||
| 	"Tools": "Інструменты", | ||||
| 	"Undo": "Адрабіць", | ||||
| 	"Unhide": "Зрабіць бачным", | ||||
| 	"Upload": "Заладаваць", | ||||
| 	"Use system proxy settings (requires restart)": "Выкарыстоўваць сістэмныя налады проксі (патрабуе перазапуск)", | ||||
| 	"View": "Прагляд", | ||||
| 	"View Shortcuts": "Спалучэнні клавішаў прагляду", | ||||
| 	"Window": "Акно", | ||||
| 	"Window Shortcuts": "Спалучэнні клавішаў акна", | ||||
| 	"YES": "ТАК", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "Вы можаце выбраць максімум 3 мовы для праверкі правапісу.", | ||||
| 	"Zoom In": "Павялічыць", | ||||
| 	"Zoom Out": "Паменшыць", | ||||
| 	"keyboard shortcuts": "спалучэнні клавішаў", | ||||
| 	"script": "скрыпт", | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "На {{{server}}} працуе састарэлая версія сервера Zulip {{{version}}}. У гэтай праграме ён можа працаваць часткова." | ||||
| } | ||||
| @@ -1,23 +1,25 @@ | ||||
| { | ||||
| 	"About Zulip": "Относно Zulip", | ||||
| 	"Actual Size": "Действителен размер", | ||||
| 	"Add Custom Certificates": "Добавяне на персонализирани сертификати", | ||||
| 	"Add Organization": "Добавяне на организация", | ||||
| 	"Add a Zulip organization": "Добавете организация Zulip", | ||||
| 	"Add custom CSS": "Добавете персонализиран CSS", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"Advanced": "напреднал", | ||||
| 	"All the connected organizations will appear here": "Всички свързани организации ще се появят тук", | ||||
| 	"All the connected organizations will appear here.": "All the connected organizations will appear here.", | ||||
| 	"Always start minimized": "Винаги започвайте да минимизирате", | ||||
| 	"App Updates": "Актуализации на приложения", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Appearance": "Външен вид", | ||||
| 	"Application Shortcuts": "Клавишни комбинации за приложения", | ||||
| 	"Are you sure you want to disconnect this organization?": "Наистина ли искате да прекъснете връзката с тази организация?", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Auto hide Menu bar": "Автоматично скриване на лентата с менюта", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Автоматично скриване на лентата с менюта (натиснете клавиша Alt за показване)", | ||||
| 	"Back": "обратно", | ||||
| 	"Bounce dock on new private message": "Прескочи док в новото лично съобщение", | ||||
| 	"Certificate file": "Файл за сертификат", | ||||
| 	"Change": "промяна", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Check for Updates": "Провери за обновления", | ||||
| 	"Close": "Близо", | ||||
| 	"Connect": "Свържете", | ||||
| @@ -29,17 +31,19 @@ | ||||
| 	"Cut": "Разрез", | ||||
| 	"Default download location": "Място на изтегляне по подразбиране", | ||||
| 	"Delete": "Изтрий", | ||||
| 	"Desktop App Settings": "Настройки на приложението за работния плот", | ||||
| 	"Desktop Notifications": "Известия за работния плот", | ||||
| 	"Desktop Settings": "Настройки на работния плот", | ||||
| 	"Disconnect": "Прекъсване на връзката", | ||||
| 	"Download App Logs": "Изтеглете регистрационни файлове на приложенията", | ||||
| 	"Edit": "редактиране", | ||||
| 	"Edit Shortcuts": "Редактиране на преки пътища", | ||||
| 	"Emoji & Symbols": "Emoji & Symbols", | ||||
| 	"Enable auto updates": "Активиране на автоматичните актуализации", | ||||
| 	"Enable error reporting (requires restart)": "Активиране на отчитането за грешки (изисква се рестартиране)", | ||||
| 	"Enable spellchecker (requires restart)": "Активиране на проверката на правописа (изисква се рестартиране)", | ||||
| 	"Enter Full Screen": "Enter Full Screen", | ||||
| 	"Factory Reset": "Фабрично нулиране", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"File": "досие", | ||||
| 	"Find accounts": "Намерете профили", | ||||
| 	"Find accounts by email": "Намерете профили по имейл", | ||||
| @@ -51,6 +55,9 @@ | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Help": "Помогне", | ||||
| 	"Help Center": "Помощен център", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Hide Zulip": "Hide Zulip", | ||||
| 	"History": "история", | ||||
| 	"History Shortcuts": "Преки пътища в историята", | ||||
| 	"Keyboard Shortcuts": "Комбинация от клавиши", | ||||
| @@ -61,7 +68,9 @@ | ||||
| 	"Mute all sounds from Zulip": "Заглуши всички звуци от Zulip", | ||||
| 	"NO": "НЕ", | ||||
| 	"Network": "мрежа", | ||||
| 	"Network and Proxy Settings": "Network and Proxy Settings", | ||||
| 	"OR": "ИЛИ", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Organization URL": "URL адрес на организацията", | ||||
| 	"Organizations": "организации", | ||||
| 	"Paste": "паста", | ||||
| @@ -71,25 +80,27 @@ | ||||
| 	"Proxy rules": "Прокси правила", | ||||
| 	"Quit": "напускам", | ||||
| 	"Quit Zulip": "Прекрати Zulip", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Redo": "ремонтирам", | ||||
| 	"Release Notes": "Бележки към изданието", | ||||
| 	"Reload": "Презареди", | ||||
| 	"Report an Issue": "Подаване на сигнал за проблем", | ||||
| 	"Reset App Settings": "Reset App Settings", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", | ||||
| 	"Save": "Запази", | ||||
| 	"Select All": "Избери всички", | ||||
| 	"Services": "Services", | ||||
| 	"Settings": "Настройки", | ||||
| 	"Shortcuts": "Shortcuts", | ||||
| 	"Show App Logs": "Показване на регистрационните файлове на приложенията", | ||||
| 	"Show app icon in system tray": "Показване на иконата на приложението в системната област", | ||||
| 	"Show app unread badge": "Показване на непрочетената значка на приложението", | ||||
| 	"Show desktop notifications": "Показване на известията на работния плот", | ||||
| 	"Show downloaded files in file manager": "Показване на изтеглените файлове във файловия мениджър", | ||||
| 	"Show sidebar": "Показване на страничната лента", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Start app at login": "Стартирайте приложението при влизане", | ||||
| 	"Switch to Next Organization": "Превключване към следваща организация", | ||||
| 	"Switch to Previous Organization": "Превключване към предишна организация", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "Тези клавишни комбинации за настолни приложения разширяват webapp на Zulip", | ||||
| 	"This will delete all application data including all added accounts and preferences": "Това ще изтрие всички данни за приложения, включително всички добавени акаунти и предпочитания", | ||||
| 	"Tip": "Бакшиш", | ||||
| 	"Toggle DevTools for Active Tab": "Превключете DevTools за Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Превключете DevTools за Zulip App", | ||||
| @@ -99,6 +110,7 @@ | ||||
| 	"Toggle Tray Icon": "Превключете иконата на тава", | ||||
| 	"Tools": "Инструменти", | ||||
| 	"Undo": "премахвам", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"Upload": "Качи", | ||||
| 	"Use system proxy settings (requires restart)": "Използване на системните прокси настройки (изисква рестартиране)", | ||||
| 	"View": "изглед", | ||||
| @@ -106,29 +118,10 @@ | ||||
| 	"Window": "прозорец", | ||||
| 	"Window Shortcuts": "Клавишни комбинации", | ||||
| 	"YES": "ДА", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Zoom In": "Увеличавам", | ||||
| 	"Zoom Out": "Отдалечавам", | ||||
| 	"Zulip Help": "Помощ за Zulip", | ||||
| 	"keyboard shortcuts": "комбинация от клавиши", | ||||
| 	"script": "писменост", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app." | ||||
| } | ||||
							
								
								
									
										127
									
								
								public/translations/bn.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | ||||
| { | ||||
| 	"About Zulip": "যুলিপ সম্পর্কে ", | ||||
| 	"Actual Size": "প্রকৃত সাইজ ", | ||||
| 	"Add Organization": "সংস্থা যুক্ত করুন", | ||||
| 	"Add a Zulip organization": "একটি যুলিপ প্রতিষ্ঠান যুক্ত করুন", | ||||
| 	"Add custom CSS": "কাস্টম সিএসএস যুক্ত করুন", | ||||
| 	"AddServer": "এড সার্ভার ", | ||||
| 	"Advanced": "অগ্রসর ", | ||||
| 	"All the connected organizations will appear here.": "All the connected organizations will appear here.", | ||||
| 	"Always start minimized": "সব সময় মিনিমাইজড ভাবে শুরু করুন ", | ||||
| 	"App Updates": "অ্যাপ আপডেট", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Appearance": "প্রকাশ", | ||||
| 	"Application Shortcuts": "অ্যাপ্লিকেশান শর্টকাট ", | ||||
| 	"Are you sure you want to disconnect this organization?": "আপনি কি নিশ্চিত যে আপনি এই সংস্থার সংযোগ বিচ্ছিন্ন করতে চান ?", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Auto hide Menu bar": "অটো মেনুবার হাইড করুন ", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "অটো মেনুবার হাইড করুন (দেখার জন্য অল্টার কি চাপুন)", | ||||
| 	"Back": "পেছন", | ||||
| 	"Bounce dock on new private message": "ব্যাক্তিগত মেসেজে ডক বাউন্স করুন ", | ||||
| 	"Change": "পরিবর্তন", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "ভাষা পরিবর্তন করতে  সিস্টেম প্রেফারেন্স  →  কীবোর্ড → টেক্সট  → স্পেলিং এ যান ", | ||||
| 	"Check for Updates": "আপডেট চেক করুন", | ||||
| 	"Close": "বন্ধ করুন", | ||||
| 	"Connect": "সংযুক্ত করুন", | ||||
| 	"Connect to another organization": "অন্য একটি সংস্থার সাথে সংযুক্ত করুন", | ||||
| 	"Connected organizations": "সংযুক্ত সংস্থা সমূহ ", | ||||
| 	"Copy": "কপি", | ||||
| 	"Copy Zulip URL": "যুলিপ  ইউআরএল কপি করুন ", | ||||
| 	"Create a new organization": "নতুন সংস্থা তৈরি করুন ", | ||||
| 	"Cut": "কাট ", | ||||
| 	"Default download location": "Default download location", | ||||
| 	"Delete": "ডিলিট", | ||||
| 	"Desktop Notifications": "ডেস্কটপ নোটিফিকেশান ", | ||||
| 	"Desktop Settings": "ডেস্কটপ সেটিংস", | ||||
| 	"Disconnect": "সংযোগ বিছিন্ন করুন", | ||||
| 	"Download App Logs": "অ্যাপ লগ ডাউনলোড করুন ", | ||||
| 	"Edit": "এডিট", | ||||
| 	"Edit Shortcuts": "শর্টকাটগুলো এডিট করুন ", | ||||
| 	"Emoji & Symbols": "Emoji & Symbols", | ||||
| 	"Enable auto updates": "অটো আপডেট চালু করুন", | ||||
| 	"Enable error reporting (requires restart)": "Enable error reporting (requires restart)", | ||||
| 	"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", | ||||
| 	"Enter Full Screen": "Enter Full Screen", | ||||
| 	"Factory Reset": "ফ্যাক্টরি রিসেট", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"File": "ফাইল", | ||||
| 	"Find accounts": "অ্যাকাউন্ট খুজুন", | ||||
| 	"Find accounts by email": "ইমেইল ব্যাবহার করে অ্যাকাউন্ট খুজুন", | ||||
| 	"Flash taskbar on new message": "Flash taskbar on new message", | ||||
| 	"Forward": "ফরওয়ার্ড", | ||||
| 	"Functionality": "Functionality", | ||||
| 	"General": "সাধারন", | ||||
| 	"Get beta updates": "Get beta updates", | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Help": "সাহায্য", | ||||
| 	"Help Center": "সাহায্য কেন্দ্র", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Hide Zulip": "Hide Zulip", | ||||
| 	"History": "ইতিহাস", | ||||
| 	"History Shortcuts": "History Shortcuts", | ||||
| 	"Keyboard Shortcuts": "Keyboard Shortcuts", | ||||
| 	"Log Out": "লগ আউট", | ||||
| 	"Log Out of Organization": "সংস্থা থেকে লগ আউট করুন", | ||||
| 	"Manual proxy configuration": "Manual proxy configuration", | ||||
| 	"Minimize": "ছোট করুন", | ||||
| 	"Mute all sounds from Zulip": "Mute all sounds from Zulip", | ||||
| 	"NO": "না", | ||||
| 	"Network": "Network", | ||||
| 	"Network and Proxy Settings": "Network and Proxy Settings", | ||||
| 	"OR": "অথবা", | ||||
| 	"On macOS, the OS spellchecker is used.": "ম্যাক ওএস এ , ওএস এর স্পেলচেকার ব্যাবহার করা হয় ।", | ||||
| 	"Organization URL": "Organization URL", | ||||
| 	"Organizations": "সংস্থাসমূহ", | ||||
| 	"Paste": "Paste", | ||||
| 	"Paste and Match Style": "Paste and Match Style", | ||||
| 	"Proxy": "Proxy", | ||||
| 	"Proxy bypass rules": "Proxy bypass rules", | ||||
| 	"Proxy rules": "Proxy rules", | ||||
| 	"Quit": "Quit", | ||||
| 	"Quit Zulip": "Quit Zulip", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Redo": "Redo", | ||||
| 	"Release Notes": "Release Notes", | ||||
| 	"Reload": "Reload", | ||||
| 	"Report an Issue": "Report an Issue", | ||||
| 	"Reset App Settings": "Reset App Settings", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", | ||||
| 	"Save": "সেভ", | ||||
| 	"Select All": "Select All", | ||||
| 	"Services": "Services", | ||||
| 	"Settings": "সেটিংস", | ||||
| 	"Shortcuts": "শর্টকাট সমূহ", | ||||
| 	"Show app icon in system tray": "Show app icon in system tray", | ||||
| 	"Show app unread badge": "Show app unread badge", | ||||
| 	"Show desktop notifications": "Show desktop notifications", | ||||
| 	"Show sidebar": "Show sidebar", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Start app at login": "Start app at login", | ||||
| 	"Switch to Next Organization": "Switch to Next Organization", | ||||
| 	"Switch to Previous Organization": "Switch to Previous Organization", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", | ||||
| 	"Tip": "টিপ", | ||||
| 	"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", | ||||
| 	"Toggle Do Not Disturb": "Toggle Do Not Disturb", | ||||
| 	"Toggle Full Screen": "Toggle Full Screen", | ||||
| 	"Toggle Sidebar": "Toggle Sidebar", | ||||
| 	"Toggle Tray Icon": "Toggle Tray Icon", | ||||
| 	"Tools": "Tools", | ||||
| 	"Undo": "অ্যান্ডু ", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"Upload": "আপলোড", | ||||
| 	"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", | ||||
| 	"View": "View", | ||||
| 	"View Shortcuts": "View Shortcuts", | ||||
| 	"Window": "Window", | ||||
| 	"Window Shortcuts": "Window Shortcuts", | ||||
| 	"YES": "হ্যাঁ", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Zoom In": "Zoom In", | ||||
| 	"Zoom Out": "জুম আউট", | ||||
| 	"keyboard shortcuts": "keyboard shortcuts", | ||||
| 	"script": "স্ক্রিপ্ট ", | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app." | ||||
| } | ||||
| @@ -1,47 +1,51 @@ | ||||
| { | ||||
| 	"About Zulip": "关于Zulip", | ||||
| 	"Actual Size": "真实大小", | ||||
| 	"Add Custom Certificates": "添加自定义证书\n\n", | ||||
| 	"Add Organization": "添加组织\n\n", | ||||
| 	"Add a Zulip organization": "添加Zulip组织\n\n", | ||||
| 	"Add custom CSS": "添加自定义样式(CSS)", | ||||
| 	"Advanced": "高级", | ||||
| 	"All the connected organizations will appear here": "All the connected organizations will appear here", | ||||
| 	"About Zulip": "About Zulip", | ||||
| 	"Actual Size": "Actual Size", | ||||
| 	"Add Organization": "Add Organization", | ||||
| 	"Add a Zulip organization": "Add a Zulip organization", | ||||
| 	"Add custom CSS": "Add custom CSS", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"Advanced": "Advanced", | ||||
| 	"All the connected organizations will appear here.": "All the connected organizations will appear here.", | ||||
| 	"Always start minimized": "Always start minimized", | ||||
| 	"App Updates": "应用更新", | ||||
| 	"App Updates": "App Updates", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Appearance": "Appearance", | ||||
| 	"Application Shortcuts": "快捷键", | ||||
| 	"Application Shortcuts": "Application Shortcuts", | ||||
| 	"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Auto hide Menu bar": "Auto hide Menu bar", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)", | ||||
| 	"Back": "后退", | ||||
| 	"Back": "Back", | ||||
| 	"Bounce dock on new private message": "Bounce dock on new private message", | ||||
| 	"Certificate file": "Certificate file", | ||||
| 	"Change": "更改", | ||||
| 	"Check for Updates": "检查更新...", | ||||
| 	"Close": "关闭", | ||||
| 	"Connect": "连接", | ||||
| 	"Connect to another organization": "连接到另一个组织", | ||||
| 	"Connected organizations": "连接的组织", | ||||
| 	"Copy": "复制", | ||||
| 	"Copy Zulip URL": "复制Zulip地址(URL)", | ||||
| 	"Create a new organization": "创建新的组织", | ||||
| 	"Cut": "剪切", | ||||
| 	"Default download location": "缺省下载位置", | ||||
| 	"Delete": "删除", | ||||
| 	"Desktop App Settings": "Desktop App Settings", | ||||
| 	"Change": "ālêštkâri", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Check for Updates": "Check for Updates", | ||||
| 	"Close": "bastên", | ||||
| 	"Connect": "Connect", | ||||
| 	"Connect to another organization": "Connect to another organization", | ||||
| 	"Connected organizations": "Connected organizations", | ||||
| 	"Copy": "Copy", | ||||
| 	"Copy Zulip URL": "Copy Zulip URL", | ||||
| 	"Create a new organization": "Create a new organization", | ||||
| 	"Cut": "Cut", | ||||
| 	"Default download location": "Default download location", | ||||
| 	"Delete": "pāk kerdên", | ||||
| 	"Desktop Notifications": "Desktop Notifications", | ||||
| 	"Desktop Settings": "Desktop Settings", | ||||
| 	"Disconnect": "断开", | ||||
| 	"Disconnect": "Disconnect", | ||||
| 	"Download App Logs": "Download App Logs", | ||||
| 	"Edit": "编辑", | ||||
| 	"Edit": "ālêšt", | ||||
| 	"Edit Shortcuts": "Edit Shortcuts", | ||||
| 	"Emoji & Symbols": "Emoji & Symbols", | ||||
| 	"Enable auto updates": "Enable auto updates", | ||||
| 	"Enable error reporting (requires restart)": "Enable error reporting (requires restart)", | ||||
| 	"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", | ||||
| 	"Enter Full Screen": "Enter Full Screen", | ||||
| 	"Factory Reset": "Factory Reset", | ||||
| 	"File": "文件", | ||||
| 	"Find accounts": "查找账户", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"File": "fāyl", | ||||
| 	"Find accounts": "jostên hêsāvā mêntori", | ||||
| 	"Find accounts by email": "Find accounts by email", | ||||
| 	"Flash taskbar on new message": "Flash taskbar on new message", | ||||
| 	"Forward": "Forward", | ||||
| @@ -51,45 +55,52 @@ | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Help": "Help", | ||||
| 	"Help Center": "Help Center", | ||||
| 	"History": "历史消息", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Hide Zulip": "Hide Zulip", | ||||
| 	"History": "History", | ||||
| 	"History Shortcuts": "History Shortcuts", | ||||
| 	"Keyboard Shortcuts": "Keyboard Shortcuts", | ||||
| 	"Log Out": "退出", | ||||
| 	"Log Out": "Log Out", | ||||
| 	"Log Out of Organization": "Log Out of Organization", | ||||
| 	"Manual proxy configuration": "Manual proxy configuration", | ||||
| 	"Minimize": "最小化", | ||||
| 	"Minimize": "Minimize", | ||||
| 	"Mute all sounds from Zulip": "Mute all sounds from Zulip", | ||||
| 	"NO": "NO", | ||||
| 	"Network": "Network", | ||||
| 	"OR": "或", | ||||
| 	"Organization URL": "社群网址", | ||||
| 	"Network and Proxy Settings": "Network and Proxy Settings", | ||||
| 	"OR": "OR", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Organization URL": "Organization URL", | ||||
| 	"Organizations": "Organizations", | ||||
| 	"Paste": "粘贴", | ||||
| 	"Paste and Match Style": "粘贴并匹配格式", | ||||
| 	"Paste": "Paste", | ||||
| 	"Paste and Match Style": "Paste and Match Style", | ||||
| 	"Proxy": "Proxy", | ||||
| 	"Proxy bypass rules": "Proxy bypass rules", | ||||
| 	"Proxy rules": "Proxy rules", | ||||
| 	"Quit": "Quit", | ||||
| 	"Quit Zulip": "Quit Zulip", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Redo": "Redo", | ||||
| 	"Release Notes": "Release Notes", | ||||
| 	"Reload": "Reload", | ||||
| 	"Report an Issue": "Report an Issue", | ||||
| 	"Save": "保存", | ||||
| 	"Select All": "全选", | ||||
| 	"Settings": "设置", | ||||
| 	"Reset App Settings": "Reset App Settings", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", | ||||
| 	"Save": "zaft kerdên", | ||||
| 	"Select All": "Select All", | ||||
| 	"Services": "Services", | ||||
| 	"Settings": "sāmovā", | ||||
| 	"Shortcuts": "Shortcuts", | ||||
| 	"Show App Logs": "Show App Logs", | ||||
| 	"Show app icon in system tray": "Show app icon in system tray", | ||||
| 	"Show app unread badge": "Show app unread badge", | ||||
| 	"Show desktop notifications": "Show desktop notifications", | ||||
| 	"Show downloaded files in file manager": "Show downloaded files in file manager", | ||||
| 	"Show sidebar": "显示侧边栏", | ||||
| 	"Show sidebar": "Show sidebar", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Start app at login": "Start app at login", | ||||
| 	"Switch to Next Organization": "Switch to Next Organization", | ||||
| 	"Switch to Previous Organization": "Switch to Previous Organization", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", | ||||
| 	"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences", | ||||
| 	"Tip": "Tip", | ||||
| 	"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", | ||||
| @@ -97,38 +108,20 @@ | ||||
| 	"Toggle Full Screen": "Toggle Full Screen", | ||||
| 	"Toggle Sidebar": "Toggle Sidebar", | ||||
| 	"Toggle Tray Icon": "Toggle Tray Icon", | ||||
| 	"Tools": "工具", | ||||
| 	"Undo": "撤销", | ||||
| 	"Upload": "上传", | ||||
| 	"Tools": "Tools", | ||||
| 	"Undo": "Undo", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"Upload": "Upload", | ||||
| 	"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", | ||||
| 	"View": "View", | ||||
| 	"View Shortcuts": "View Shortcuts", | ||||
| 	"Window": "Window", | ||||
| 	"Window Shortcuts": "Window Shortcuts", | ||||
| 	"YES": "确认", | ||||
| 	"YES": "YES", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Zoom In": "Zoom In", | ||||
| 	"Zoom Out": "Zoom Out", | ||||
| 	"Zulip Help": "Zulip Help", | ||||
| 	"keyboard shortcuts": "keyboard shortcuts", | ||||
| 	"script": "script", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app." | ||||
| } | ||||
| @@ -1,23 +1,25 @@ | ||||
| { | ||||
| 	"About Zulip": "Quant a Zulip", | ||||
| 	"Actual Size": "Actual Size", | ||||
| 	"Add Custom Certificates": "Add Custom Certificates", | ||||
| 	"Add Organization": "Add Organization", | ||||
| 	"Add a Zulip organization": "Add a Zulip organization", | ||||
| 	"Add custom CSS": "Add custom CSS", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"Advanced": "Advanced", | ||||
| 	"All the connected organizations will appear here": "All the connected organizations will appear here", | ||||
| 	"All the connected organizations will appear here.": "All the connected organizations will appear here.", | ||||
| 	"Always start minimized": "Always start minimized", | ||||
| 	"App Updates": "App Updates", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Appearance": "Appearance", | ||||
| 	"Application Shortcuts": "Application Shortcuts", | ||||
| 	"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Auto hide Menu bar": "Auto hide Menu bar", | ||||
| 	"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)", | ||||
| 	"Back": "Back", | ||||
| 	"Bounce dock on new private message": "Bounce dock on new private message", | ||||
| 	"Certificate file": "Certificate file", | ||||
| 	"Change": "Change", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Check for Updates": "Check for Updates", | ||||
| 	"Close": "Tancar", | ||||
| 	"Connect": "Connect", | ||||
| @@ -29,17 +31,19 @@ | ||||
| 	"Cut": "Cut", | ||||
| 	"Default download location": "Default download location", | ||||
| 	"Delete": "Elimina", | ||||
| 	"Desktop App Settings": "Configuració de l'aplicació d'escriptori", | ||||
| 	"Desktop Notifications": "Desktop Notifications", | ||||
| 	"Desktop Settings": "Configuració d'escriptori", | ||||
| 	"Disconnect": "Disconnect", | ||||
| 	"Download App Logs": "Download App Logs", | ||||
| 	"Edit": "Edita", | ||||
| 	"Edit Shortcuts": "Edit Shortcuts", | ||||
| 	"Emoji & Symbols": "Emoji & Symbols", | ||||
| 	"Enable auto updates": "Enable auto updates", | ||||
| 	"Enable error reporting (requires restart)": "Enable error reporting (requires restart)", | ||||
| 	"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", | ||||
| 	"Enter Full Screen": "Enter Full Screen", | ||||
| 	"Factory Reset": "Factory Reset", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"File": "Fitxer", | ||||
| 	"Find accounts": "Find accounts", | ||||
| 	"Find accounts by email": "Find accounts by email", | ||||
| @@ -48,20 +52,25 @@ | ||||
| 	"Functionality": "Functionality", | ||||
| 	"General": "General", | ||||
| 	"Get beta updates": "Get beta updates", | ||||
| 	"Hard Reload": "Hard Reload", | ||||
| 	"Hard Reload": "Recàrrega forçada", | ||||
| 	"Help": "Help", | ||||
| 	"Help Center": "Centre d'ajuda", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Hide Zulip": "Hide Zulip", | ||||
| 	"History": "Historial", | ||||
| 	"History Shortcuts": "Dreceres d'historial", | ||||
| 	"Keyboard Shortcuts": "Keyboard Shortcuts", | ||||
| 	"Log Out": "Log Out", | ||||
| 	"Log Out of Organization": "Log Out of Organization", | ||||
| 	"Log Out": "Tanca la sessió", | ||||
| 	"Log Out of Organization": "Tanca la sessió de l'organització", | ||||
| 	"Manual proxy configuration": "Manual proxy configuration", | ||||
| 	"Minimize": "Minimize", | ||||
| 	"Mute all sounds from Zulip": "Silencia tots els sons de Zulip", | ||||
| 	"NO": "NO", | ||||
| 	"Network": "Network", | ||||
| 	"Network and Proxy Settings": "Network and Proxy Settings", | ||||
| 	"OR": "OR", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Organization URL": "URL d'organització", | ||||
| 	"Organizations": "Organizations", | ||||
| 	"Paste": "Paste", | ||||
| @@ -71,25 +80,27 @@ | ||||
| 	"Proxy rules": "Proxy rules", | ||||
| 	"Quit": "Quit", | ||||
| 	"Quit Zulip": "Quit Zulip", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Redo": "Redo", | ||||
| 	"Release Notes": "Release Notes", | ||||
| 	"Reload": "Reload", | ||||
| 	"Reload": "Recarrega", | ||||
| 	"Report an Issue": "Report an Issue", | ||||
| 	"Reset App Settings": "Reinicia la configuració de l'aplicació", | ||||
| 	"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", | ||||
| 	"Save": "Guardar", | ||||
| 	"Select All": "Select All", | ||||
| 	"Services": "Services", | ||||
| 	"Settings": "Configuració", | ||||
| 	"Shortcuts": "Shortcuts", | ||||
| 	"Show App Logs": "Show App Logs", | ||||
| 	"Show app icon in system tray": "Show app icon in system tray", | ||||
| 	"Show app unread badge": "Mostrar una marca en la icona si hi ha missatges no llegits", | ||||
| 	"Show desktop notifications": "Show desktop notifications", | ||||
| 	"Show downloaded files in file manager": "Show downloaded files in file manager", | ||||
| 	"Show sidebar": "Show sidebar", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Start app at login": "Start app at login", | ||||
| 	"Switch to Next Organization": "Switch to Next Organization", | ||||
| 	"Switch to Previous Organization": "Switch to Previous Organization", | ||||
| 	"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", | ||||
| 	"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences", | ||||
| 	"Tip": "Tip", | ||||
| 	"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", | ||||
| 	"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", | ||||
| @@ -99,6 +110,7 @@ | ||||
| 	"Toggle Tray Icon": "Toggle Tray Icon", | ||||
| 	"Tools": "Tools", | ||||
| 	"Undo": "Undo", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"Upload": "Pujada", | ||||
| 	"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", | ||||
| 	"View": "View", | ||||
| @@ -106,29 +118,10 @@ | ||||
| 	"Window": "Window", | ||||
| 	"Window Shortcuts": "Window Shortcuts", | ||||
| 	"YES": "YES", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Zoom In": "Zoom In", | ||||
| 	"Zoom Out": "Zoom Out", | ||||
| 	"Zulip Help": "Zulip Help", | ||||
| 	"keyboard shortcuts": "keyboard shortcuts", | ||||
| 	"script": "script", | ||||
| 	"Quit when the window is closed": "Quit when the window is closed", | ||||
| 	"Ask where to save files before downloading": "Ask where to save files before downloading", | ||||
| 	"Services": "Services", | ||||
| 	"Hide": "Hide", | ||||
| 	"Hide Others": "Hide Others", | ||||
| 	"Unhide": "Unhide", | ||||
| 	"AddServer": "AddServer", | ||||
| 	"App language (requires restart)": "App language (requires restart)", | ||||
| 	"Factory Reset Data": "Factory Reset Data", | ||||
| 	"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.", | ||||
| 	"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", | ||||
| 	"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", | ||||
| 	"Copy Link": "Copy Link", | ||||
| 	"Copy Image": "Copy Image", | ||||
| 	"Copy Image URL": "Copy Image URL", | ||||
| 	"No Suggestion Found": "No Suggestion Found", | ||||
| 	"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", | ||||
| 	"Spellchecker Languages": "Spellchecker Languages", | ||||
| 	"Add to Dictionary": "Add to Dictionary", | ||||
| 	"Look Up": "Look Up" | ||||
| 	"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app." | ||||
| } | ||||