Compare commits

..

1 Commits

Author SHA1 Message Date
akashnimare
7194e788e4 Add a setting option to control auto-updates 2017-09-12 21:43:53 +05:30
233 changed files with 4443 additions and 27035 deletions

View File

@@ -1,11 +1,15 @@
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.css,*.html,*.js,*.json,*.ts}]
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

15
.gitattributes vendored
View File

@@ -1,14 +1 @@
* text=auto eol=lf
*.gif binary
*.jpg binary
*.jpeg binary
*.eot binary
*.woff binary
*.woff2 binary
*.svg binary
*.ttf binary
*.png binary
*.otf binary
*.tif binary
*.ogg binary
* text=auto

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: zulip
patreon: zulip
open_collective: zulip

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Clear steps to reproduce the issue. -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):**
- Operating System:
<!-- (Platform and Version) e.g. macOS 10.13.6 / Windows 10 (1803) / Ubuntu 18.04 x64 -->
- Zulip Desktop Version:
<!-- e.g. 5.2.0 -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,4 +0,0 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
---

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Problem Description**
<!-- Please add a clear and concise description of what the problem is. -->
**Proposed Solution**
<!-- Describe the solution you'd like in a clear and concise manner -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -1,18 +0,0 @@
---
<!--
Remove the fields that are not appropriate
Please include:
-->
**What's this PR do?**
**Any background context you want to provide?**
**Screenshots?**
**You have tested this PR on:**
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS

View File

@@ -1,15 +0,0 @@
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm test

24
.gitignore vendored
View File

@@ -1,22 +1,12 @@
# Dependency directory
/node_modules/
# Dependency directories
node_modules/
# npm cache directory
.npm
# transifexrc - if user prefers it to be in working tree
.transifexrc
# Compiled binary build directory
dist/
#snap generated files
snap/parts
snap/prime
snap/snap
snap/stage
snap/*.snap
# Logs
logs
*.log
@@ -33,12 +23,4 @@ yarn-error.log*
# miscellaneous
.idea
config.gypi
# Test generated files
# tests/package.json
.python-version
# Ignore all the typescript compiled files
app/**/*.js
config.gypi

View File

@@ -1,19 +0,0 @@
{
"attr-value-not-empty": false,
"attr-no-duplication": true,
"doctype-first": true,
"spec-char-escape": true,
"id-unique": true,
"src-not-empty": true,
"title-require": true,
"alt-require": false,
"doctype-html5": true,
"id-class-value": "dash",
"style-disabled": false,
"inline-style-disabled": false,
"inline-script-disabled": false,
"id-class-ad-disabled": false,
"href-abs-or-rel": false,
"attr-unsafe-chars": true,
"head-script-disabled": true
}

View File

@@ -1,5 +0,0 @@
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
Rishi Gupta <rishig@zulip.com> <rishig@zulipchat.com>
Rishi Gupta <rishig@zulip.com> <rishig@users.noreply.github.com>
Tim Abbott <tabbott@zulip.com> <tabbott@zulipchat.com>
Tim Abbott <tabbott@zulip.com> <tabbott@mit.edu>

1
.node-version Normal file
View File

@@ -0,0 +1 @@
6.9.4

View File

@@ -1,3 +0,0 @@
/app/**/*.js
/app/translations/*.json
/dist

View File

@@ -1,4 +0,0 @@
*.js
*.ts
/app/translations/*.json
/dist

1
.python-version Normal file
View File

@@ -0,0 +1 @@
2.7.9

View File

@@ -1,9 +0,0 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
"color-named": "never",
"color-no-hex": true,
"font-family-no-missing-generic-family-keyword": [true, {"ignoreFontFamilies": ["Material Icons"]}],
"selector-type-no-unknown": [true, {"ignoreTypes": ["send-feedback", "webview"]}],
}
}

36
.travis.yml Normal file
View File

@@ -0,0 +1,36 @@
sudo: required
dist: trusty
os:
- osx
- linux
addons:
apt:
packages:
- build-essential
- libxext-dev
- libxtst-dev
- libxkbfile-dev
language: node_js
node_js:
- '6'
before_install:
- npm install -g gulp
- npm install
cache:
directories:
- node_modules
- app/node_modules
script:
- npm run travis
notifications:
webhooks:
urls:
- https://zulip.org/zulipbot/travis
on_success: always
on_failure: always

View File

@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[zulip.desktopjson]
file_filter = app/translations/<lang>.json
minimum_perc = 0
source_file = app/translations/en.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,50 +2,46 @@
Thanks for taking the time to contribute!
The following is a set of guidelines for contributing to Zulip's desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
The following is a set of guidelines for contributing to Zulip Electron Desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
## Getting Started
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.us/essential-electron) great article.
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](http://jlord.us/essential-electron/) great article.
## 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 webapp 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).
* If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/electron).
## Issue
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-desktop/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-desktop/issues/new).
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-electron/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-electron/issues/new).
The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
Please pay attention to the following points while opening an issue.
### Does it happen on web browsers? (especially Chrome)
Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application.
If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip).
### Write detailed information
Detailed information is very helpful to understand an issue.
For example:
* How to reproduce the issue, step-by-step.
* The expected behavior (or what is wrong).
* Screenshots for GUI issues.
* The application version.
* The operating system.
* The Zulip-Desktop version.
- How to reproduce the issue, step-by-step.
- The expected behavior (or what is wrong).
- Screenshots for GUI issues.
- The application version.
- The operating system.
- The Zulip-Desktop version.
## Pull Requests
Pull Requests are always welcome.
Pull Requests are always welcome.
1. When you edit the code, please run `npm run test` to check the formatting of your code before you `git commit`.
2. Ensure the PR description clearly describes the problem and solution. It should include:
- The operating system on which you tested.
- The Zulip-Desktop version on which you tested.
- The relevant issue number, if applicable.
* The operating system on which you tested.
* The Zulip-Desktop version on which you tested.
* The relevant issue number, if applicable.

7
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,7 @@
---
Please include:
- `Operating System`
- `Clear steps to reproduce the issue`
- `Relevant error messages and/or screenshots`

View File

@@ -1,41 +1,39 @@
# Zulip Desktop Client
[![Build Status](https://travis-ci.com/zulip/zulip-desktop.svg?branch=main)](https://travis-ci.com/github/zulip/zulip-desktop)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-desktop?branch=main&svg=true)](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/main)
[![Build Status](https://travis-ci.org/zulip/zulip-electron.svg?branch=master)](https://travis-ci.org/zulip/zulip-electron)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-electron?branch=master&svg=true)](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org)
Desktop client for Zulip. Available for Mac, Linux, and Windows.
Desktop client for Zulip. Available for Mac, Linux and Windows.
![screenshot](https://i.imgur.com/s1o6TRA.png)
![screenshot](https://i.imgur.com/vekKnW4.png)
<img src="http://i.imgur.com/ChzTq4F.png"/>
# Download
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).
Please see [installation guide](./how-to-install.md).
# Features
* Sign in to multiple teams
* Native desktop Notifications
* SpellChecker
* OSX/Win/Linux installers
* Automatic Updates (macOS/Windows)
* Keyboard shortcuts
- Sign in to multiple organizations
- Desktop notifications with inline reply
- Tray/dock integration
- Multi-language spell checker
- Automatic updates
Description | Keys
-----------------------| -----------------------
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
# Reporting issues
This desktop client shares most of its code with the Zulip webapp.
Issues in an individual organization's Zulip window should be reported
in the [Zulip server and webapp
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).
# Development
Please see our [development guide](./development.md) to get started and run app locally.
# Contribute
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!
Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md).
If you want to contribute please make sure to read [our documentation about contributing](./CONTRIBUTING.md) first.
* [Issue Tracker](https://github.com/zulip/zulip-electron/issues)
* [Source Code](https://github.com/zulip/zulip-electron/)
# License
Released under the [Apache-2.0](./LICENSE) license.

View File

@@ -1,126 +0,0 @@
import electron from "electron";
import fs from "fs";
import path from "path";
import {JsonDB} from "node-json-db";
import type {DNDSettings} from "./dnd-util";
import * as EnterpriseUtil from "./enterprise-util";
import Logger from "./logger-util";
export interface Config extends DNDSettings {
appLanguage: string | null;
autoHideMenubar: boolean;
autoUpdate: boolean;
badgeOption: boolean;
betaUpdate: boolean;
customCSS: string | false | null;
dnd: boolean;
dndPreviousSettings: Partial<DNDSettings>;
dockBouncing: boolean;
downloadsPath: string;
enableSpellchecker: boolean;
errorReporting: boolean;
lastActiveTab: number;
promptDownload: boolean;
proxyBypass: string;
proxyPAC: string;
proxyRules: string;
quitOnClose: boolean;
showSidebar: boolean;
spellcheckerLanguages: string[] | null;
startAtLogin: boolean;
startMinimized: boolean;
systemProxyRules: string;
trayIcon: boolean;
useManualProxy: boolean;
useProxy: boolean;
useSystemProxy: boolean;
}
/* To make the util runnable in both main and renderer process */
const {app, dialog} = process.type === "renderer" ? electron.remote : electron;
const logger = new Logger({
file: "config-util.log",
});
let db: JsonDB;
reloadDB();
export function getConfigItem<Key extends keyof Config>(
key: Key,
defaultValue: Config[Key],
): Config[Key] {
try {
db.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
const value = db.getData("/")[key];
if (value === undefined) {
setConfigItem(key, defaultValue);
return defaultValue;
}
return value;
}
// This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
const value = db.getData("/")[key];
return value !== undefined;
}
export function setConfigItem<Key extends keyof Config>(
key: Key,
value: Config[Key],
override?: boolean,
): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// If item is in global config and we're not trying to override
return;
}
db.push(`/${key}`, value, true);
db.save();
}
export function removeConfigItem(key: string): void {
db.delete(`/${key}`);
db.save();
}
function reloadDB(): void {
const settingsJsonPath = path.join(
app.getPath("userData"),
"/config/settings.json",
);
try {
const file = fs.readFileSync(settingsJsonPath, "utf8");
JSON.parse(file);
} catch (error: unknown) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
"Error saving settings",
"We encountered an error while saving the settings.",
);
logger.error("Error while JSON parsing settings.json: ");
logger.error(error);
logger.reportSentry(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,62 +0,0 @@
import electron from "electron";
import fs from "fs";
const {app} = process.type === "renderer" ? electron.remote : electron;
let setupCompleted = false;
const zulipDir = app.getPath("userData");
const logDir = `${zulipDir}/Logs/`;
const configDir = `${zulipDir}/config/`;
export const initSetUp = (): void => {
// If it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) {
fs.mkdirSync(zulipDir);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
// Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
const domainJson = `${zulipDir}/domain.json`;
const settingsJson = `${zulipDir}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`;
const configData = [
{
path: domainJson,
fileName: "domain.json",
},
{
path: settingsJson,
fileName: "settings.json",
},
{
path: updatesJson,
fileName: "updates.json",
},
];
for (const data of configData) {
if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName);
fs.unlinkSync(data.path);
}
}
// `window-state.json` is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson);
}
}
setupCompleted = true;
}
};

View File

@@ -1,55 +0,0 @@
import * as ConfigUtil from "./config-util";
type SettingName = keyof DNDSettings;
export interface DNDSettings {
showNotification: boolean;
silent: boolean;
flashTaskbarOnMessage: boolean;
}
interface Toggle {
dnd: boolean;
newSettings: Partial<DNDSettings>;
}
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem("dnd", false);
const dndSettingList: SettingName[] = ["showNotification", "silent"];
if (process.platform === "win32") {
dndSettingList.push("flashTaskbarOnMessage");
}
let newSettings: Partial<DNDSettings>;
if (dnd) {
const oldSettings: Partial<DNDSettings> = {};
newSettings = {};
// Iterate through the dndSettingList.
for (const settingName of dndSettingList) {
// Store the current value of setting.
oldSettings[settingName] = ConfigUtil.getConfigItem(
settingName,
settingName !== "silent",
);
// New value of setting.
newSettings[settingName] = settingName === "silent";
}
// Store old value in oldSettings.
ConfigUtil.setConfigItem("dndPreviousSettings", oldSettings);
} else {
newSettings = ConfigUtil.getConfigItem("dndPreviousSettings", {
showNotification: true,
silent: false,
...(process.platform === "win32" && {flashTaskbarOnMessage: true}),
});
}
for (const settingName of dndSettingList) {
ConfigUtil.setConfigItem(settingName, newSettings[settingName]!);
}
ConfigUtil.setConfigItem("dnd", dnd);
return {dnd, newSettings};
}

View File

@@ -1,85 +0,0 @@
import fs from "fs";
import path from "path";
import type {Config} from "./config-util";
import Logger from "./logger-util";
interface EnterpriseConfig extends Config {
presetOrganizations: string[];
}
const logger = new Logger({
file: "enterprise-util.log",
});
let enterpriseSettings: Partial<EnterpriseConfig>;
let configFile: boolean;
reloadDB();
function reloadDB(): void {
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
if (process.platform === "win32") {
enterpriseFile =
"C:\\Program Files\\Zulip-Desktop-Config\\global_config.json";
}
enterpriseFile = path.resolve(enterpriseFile);
if (fs.existsSync(enterpriseFile)) {
configFile = true;
try {
const file = fs.readFileSync(enterpriseFile, "utf8");
enterpriseSettings = JSON.parse(file);
} catch (error: unknown) {
logger.log("Error while JSON parsing global_config.json: ");
logger.log(error);
}
} else {
configFile = false;
}
}
export function hasConfigFile(): boolean {
return configFile;
}
export function getConfigItem<Key extends keyof EnterpriseConfig>(
key: Key,
defaultValue: EnterpriseConfig[Key],
): EnterpriseConfig[Key] {
reloadDB();
if (!configFile) {
return defaultValue;
}
const value = enterpriseSettings[key];
return value === undefined ? defaultValue : (value as EnterpriseConfig[Key]);
}
export function configItemExists(key: keyof EnterpriseConfig): boolean {
reloadDB();
if (!configFile) {
return false;
}
return enterpriseSettings[key] !== undefined;
}
export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists("presetOrganizations")) {
return false;
}
const presetOrgs = enterpriseSettings.presetOrganizations;
if (!Array.isArray(presetOrgs)) {
throw new TypeError("Expected array for presetOrgs");
}
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

View File

@@ -1,26 +0,0 @@
import {htmlEscape} from "escape-goat";
export class HTML {
html: string;
constructor({html}: {html: string}) {
this.html = html;
}
join(htmls: readonly HTML[]): HTML {
return new HTML({html: htmls.map((html) => html.html).join(this.html)});
}
}
export function html(
template: TemplateStringsArray,
...values: unknown[]
): HTML {
let html = template[0];
for (const [index, value] of values.entries()) {
html += value instanceof HTML ? value.html : htmlEscape(String(value));
html += template[index + 1];
}
return new HTML({html});
}

View File

@@ -1,115 +0,0 @@
import {Console} from "console"; // eslint-disable-line node/prefer-global/console
import electron from "electron";
import fs from "fs";
import os from "os";
import {initSetUp} from "./default-util";
import {captureException, sentryInit} from "./sentry-util";
const {app} = process.type === "renderer" ? electron.remote : electron;
interface LoggerOptions {
file?: string;
}
initSetUp();
let reportErrors = true;
if (process.type === "renderer") {
// Report Errors to Sentry only if it is enabled in settings
// Gets the value of reportErrors from config-util for renderer process
// For main process, sentryInit() is handled in index.js
const {ipcRenderer} = electron;
ipcRenderer.send("error-reporting");
ipcRenderer.on(
"error-reporting-val",
(_event: Event, errorReporting: boolean) => {
reportErrors = errorReporting;
if (reportErrors) {
sentryInit();
}
},
);
}
const logDir = `${app.getPath("userData")}/Logs`;
type Level = "log" | "debug" | "info" | "warn" | "error";
export default class Logger {
nodeConsole: Console;
constructor(options: LoggerOptions = {}) {
let {file = "console.log"} = options;
file = `${logDir}/${file}`;
// Trim log according to type of process
if (process.type === "renderer") {
requestIdleCallback(async () => this.trimLog(file));
} else {
process.nextTick(async () => this.trimLog(file));
}
const fileStream = fs.createWriteStream(file, {flags: "a"});
const nodeConsole = new Console(fileStream);
this.nodeConsole = nodeConsole;
}
_log(type: Level, ...args: unknown[]): void {
args.unshift(this.getTimestamp() + " |\t");
args.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...args);
console[type](...args);
}
log(...args: unknown[]): void {
this._log("log", ...args);
}
debug(...args: unknown[]): void {
this._log("debug", ...args);
}
info(...args: unknown[]): void {
this._log("info", ...args);
}
warn(...args: unknown[]): void {
this._log("warn", ...args);
}
error(...args: unknown[]): void {
this._log("error", ...args);
}
getTimestamp(): string {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
reportSentry(error: unknown): void {
if (reportErrors) {
captureException(error);
}
}
async trimLog(file: string): Promise<void> {
const data = await fs.promises.readFile(file, "utf8");
const MAX_LOG_FILE_LINES = 500;
const logs = data.split(os.EOL);
const logLength = logs.length - 1;
// Keep bottom MAX_LOG_FILE_LINES of each log instance
if (logLength > MAX_LOG_FILE_LINES) {
const trimmedLogs = logs.slice(logLength - MAX_LOG_FILE_LINES);
const toWrite = trimmedLogs.join(os.EOL);
await fs.promises.writeFile(file, toWrite);
}
}
}

View File

@@ -1,43 +0,0 @@
interface 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
• You can connect to that URL in a web browser.
• If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.
• It's a Zulip server. (The oldest supported version is 1.6).
• The server has a valid certificate.
• The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
}
export function noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.
Please contact your server administrator.`;
}
export function enterpriseOrgError(
length: number,
domains: string[],
): DialogBoxError {
let domainList = "";
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
title: `Could not add the following ${
length === 1 ? "organization" : "organizations"
}`,
content: `${domainList}\nPlease contact your system administrator.`,
};
}
export function orgRemovalError(url: string): DialogBoxError {
return {
title: `Removing ${url} is a restricted operation.`,
content: "Please contact your system administrator.",
};
}

View File

@@ -1,20 +0,0 @@
import electron from "electron";
import {init} from "@sentry/electron";
const {app} = process.type === "renderer" ? electron.remote : electron;
export const sentryInit = (): void => {
if (app.isPackaged) {
init({
dsn: "https://628dc2f2864243a08ead72e63f94c7b1@sentry.io/204668",
// We should ignore this error since it's harmless and we know the reason behind this
// This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs
ignoreErrors: ["does not appear to be a valid Zulip server"],
/// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
}
};
export {captureException} from "@sentry/electron";

View File

@@ -1,18 +0,0 @@
import path from "path";
import i18n from "i18n";
import * as ConfigUtil from "./config-util";
i18n.configure({
directory: path.join(__dirname, "../translations/"),
updateFiles: false,
});
/* Fetches the current appLocale from settings.json */
const appLocale = ConfigUtil.getConfigItem("appLanguage", "en");
/* If no locale present in the json, en is set default */
export function __(phrase: string): string {
return i18n.__({phrase, locale: appLocale ? appLocale : "en"});
}

View File

@@ -1,90 +0,0 @@
import type {DNDSettings} from "./dnd-util";
import type {MenuProps, NavItem, ServerConf} from "./types";
export interface MainMessage {
"clear-app-settings": () => void;
downloadFile: (url: string, downloadPath: string) => void;
"error-reporting": () => void;
"fetch-user-agent": () => string;
"focus-app": () => void;
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
"quit-app": () => void;
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
"realm-name-changed": (serverURL: string, realmName: string) => void;
"reload-full-app": () => void;
"save-last-tab": (index: number) => void;
"set-spellcheck-langs": () => void;
"switch-server-tab": (index: number) => void;
"toggle-app": () => void;
"toggle-badge-option": (newValue: boolean) => void;
"toggle-menubar": (showMenubar: boolean) => void;
toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
"unread-count": (unreadCount: number) => void;
"update-badge": (messageCount: number) => void;
"update-menu": (props: MenuProps) => void;
"update-taskbar-icon": (data: string, text: string) => void;
}
export interface MainCall {
"get-server-settings": (domain: string) => ServerConf;
"is-online": (url: string) => boolean;
"save-server-icon": (iconURL: string) => string;
}
export interface RendererMessage {
back: () => void;
"copy-zulip-url": () => void;
destroytray: () => void;
downloadFileCompleted: (filePath: string, fileName: string) => void;
downloadFileFailed: (state: string) => void;
"enter-fullscreen": () => void;
"error-reporting-val": (errorReporting: boolean) => void;
focus: () => void;
"focus-webview-with-id": (webviewId: number) => void;
forward: () => void;
"hard-reload": () => void;
"leave-fullscreen": () => void;
"log-out": () => void;
logout: () => void;
"new-server": () => void;
"open-about": () => void;
"open-feedback-modal": () => void;
"open-help": () => void;
"open-network-settings": () => void;
"open-org-tab": () => void;
"open-settings": () => void;
"permission-request": (
options: {webContentsId: number | null; origin: string; permission: string},
rendererCallbackId: number,
) => void;
"reload-current-viewer": () => void;
"reload-proxy": (showAlert: boolean) => void;
"reload-viewer": () => void;
"render-taskbar-icon": (messageCount: number) => void;
"set-active": () => void;
"set-idle": () => void;
"show-keyboard-shortcuts": () => void;
"show-network-error": (index: number) => void;
"show-notification-settings": () => void;
"switch-server-tab": (index: number) => void;
"switch-settings-nav": (navItem: NavItem) => void;
"tab-devtools": () => void;
"toggle-autohide-menubar": (
autoHideMenubar: boolean,
updateMenu: boolean,
) => void;
"toggle-dnd": (state: boolean, newSettings: Partial<DNDSettings>) => void;
"toggle-menubar-setting": (state: boolean) => void;
"toggle-sidebar": (show: boolean) => void;
"toggle-sidebar-setting": (state: boolean) => void;
"toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void;
toggletray: () => void;
tray: (arg: number) => void;
"update-realm-icon": (serverURL: string, iconURL: string) => void;
"update-realm-name": (serveRURL: string, realmName: string) => void;
"webview-reload": () => void;
zoomActualSize: () => void;
zoomIn: () => void;
zoomOut: () => void;
}

View File

@@ -1,25 +0,0 @@
export interface MenuProps {
tabs: TabData[];
activeTabIndex?: number;
enableMenu?: boolean;
}
export type NavItem =
| "General"
| "Network"
| "AddServer"
| "Organizations"
| "Shortcuts";
export interface ServerConf {
url: string;
alias: string;
icon: string;
}
export interface TabData {
role: string;
name: string;
index: number;
webviewName: string;
}

59
app/main/autoupdater.js Normal file
View File

@@ -0,0 +1,59 @@
'use strict';
const fs = require('fs');
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
function appUpdater() {
// Don't initiate auto-updates in development and on Linux system
// since autoUpdater doesn't work on Linux
if (isDev || process.platform === 'linux') {
return;
}
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
if (!fs.existsSync(LogsDir)) {
fs.mkdirSync(LogsDir);
}
// Log whats happening
const log = require('electron-log');
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = 'info';
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
// Ask the user if update is available
// eslint-disable-next-line no-unused-vars
autoUpdater.on('update-downloaded', event => {
// Ask user to update the app
dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application'
}, response => {
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
});
});
// Init for updates
autoUpdater.checkForUpdates();
}
module.exports = {
appUpdater
};

View File

@@ -1,116 +0,0 @@
import {app, dialog, session, shell} from "electron";
import util from "util";
import log from "electron-log";
import type {UpdateDownloadedEvent, UpdateInfo} from "electron-updater";
import {autoUpdater} from "electron-updater";
import * as ConfigUtil from "../common/config-util";
import {linuxUpdateNotification} from "./linuxupdater"; // Required only in case of linux
const sleep = util.promisify(setTimeout);
export async function appUpdater(updateFromMenu = false): Promise<void> {
// Don't initiate auto-updates in development
if (!app.isPackaged) {
return;
}
if (process.platform === "linux" && !process.env.APPIMAGE) {
const ses = session.fromPartition("persist:webviewsession");
await linuxUpdateNotification(ses);
return;
}
let updateAvailable = false;
// Create Logs directory
const LogsDir = `${app.getPath("userData")}/Logs`;
// Log whats happening
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = "info";
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false);
autoUpdater.allowPrerelease = isBetaUpdate;
const eventsListenerRemove = ["update-available", "update-not-available"];
autoUpdater.on("update-available", async (info: UpdateInfo) => {
if (updateFromMenu) {
updateAvailable = true;
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
for (const event of eventsListenerRemove) {
autoUpdater.removeAllListeners(event);
}
await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail:
"The update will be downloaded in the background. You will be notified when it is ready to be installed.",
});
}
});
autoUpdater.on("update-not-available", async () => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
await dialog.showMessageBox({
message: "No updates available",
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`,
});
}
});
autoUpdater.on("error", async (error: Error) => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
const messageText = updateAvailable
? "Unable to download the updates"
: "Unable to check for updates";
const {response} = await dialog.showMessageBox({
type: "error",
buttons: ["Manual Download", "Cancel"],
message: messageText,
detail: `Error: ${error.message}
The latest version of Zulip Desktop is available at -
https://zulip.com/apps/.
Current Version: ${app.getVersion()}`,
});
if (response === 0) {
await shell.openExternal("https://zulip.com/apps/");
}
}
});
// Ask the user if update is available
autoUpdater.on("update-downloaded", async (event: UpdateDownloadedEvent) => {
// Ask user to update the app
const {response} = await dialog.showMessageBox({
type: "question",
buttons: ["Install and Relaunch", "Install Later"],
defaultId: 0,
message: `A new update ${event.version} has been downloaded`,
detail: "It will be installed the next time you restart the application",
});
if (response === 0) {
await sleep(1000);
autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}
});
// Init for updates
await autoUpdater.checkForUpdates();
}

View File

@@ -1,61 +0,0 @@
import electron, {app} from "electron";
import * as ConfigUtil from "../common/config-util";
import {send} from "./typed-ipc-main";
function showBadgeCount(
messageCount: number,
mainWindow: electron.BrowserWindow,
): void {
if (process.platform === "win32") {
updateOverlayIcon(messageCount, mainWindow);
} else {
app.badgeCount = messageCount;
}
}
function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === "win32") {
mainWindow.setOverlayIcon(null, "");
} else {
app.badgeCount = 0;
}
}
export function updateBadge(
badgeCount: number,
mainWindow: electron.BrowserWindow,
): void {
if (ConfigUtil.getConfigItem("badgeOption", true)) {
showBadgeCount(badgeCount, mainWindow);
} else {
hideBadgeCount(mainWindow);
}
}
function updateOverlayIcon(
messageCount: number,
mainWindow: electron.BrowserWindow,
): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(
ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, "");
} else {
send(mainWindow.webContents, "render-taskbar-icon", messageCount);
}
}
export function updateTaskbarIcon(
data: string,
text: string,
mainWindow: electron.BrowserWindow,
): void {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}

264
app/main/index.js Normal file
View File

@@ -0,0 +1,264 @@
'use strict';
const path = require('path');
const electron = require('electron');
const electronLocalshortcut = require('electron-localshortcut');
const windowStateKeeper = require('electron-window-state');
const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater');
const { setAutoLaunch } = require('./startup');
const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
let badgeCount;
let isQuitting = false;
// Load this url in main window
const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
const isAlreadyRunning = app.makeSingleInstance(() => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.show();
}
});
if (isAlreadyRunning) {
return app.quit();
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = () => {
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
};
function createMainWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 600
});
// Let's keep the window position global so that we can access it in other process
global.mainWindowState = mainWindowState;
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
title: 'Zulip',
icon: iconPath(),
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 600,
minHeight: 500,
webPreferences: {
plugins: true,
allowDisplayingInsecureContent: true,
nodeIntegration: true
},
show: false
});
win.on('focus', () => {
win.webContents.send('focus');
});
win.once('ready-to-show', () => {
win.show();
});
win.loadURL(mainURL);
// Keep the app running in background on close event
win.on('close', e => {
if (!isQuitting) {
e.preventDefault();
if (process.platform === 'darwin') {
app.hide();
} else {
win.hide();
}
}
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
win.setTitle('Zulip');
win.on('enter-full-screen', () => {
win.webContents.send('enter-fullscreen');
});
win.on('leave-full-screen', () => {
win.webContents.send('leave-fullscreen');
});
// To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', e => {
if (e) {
win.webContents.send('destroytray');
}
});
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
mainWindowState.manage(win);
return win;
}
function registerLocalShortcuts(page) {
// Somehow, reload action cannot be overwritten by the menu item
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
page.send('reload-viewer');
});
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
page.send('back');
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
page.send('forward');
});
}
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
callback(true);
});
app.on('window-all-closed', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
app.on('ready', () => {
appMenu.setMenu({
tabs: []
});
mainWindow = createMainWindow();
const page = mainWindow.webContents;
registerLocalShortcuts(page);
page.on('dom-ready', () => {
mainWindow.show();
});
page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows
appUpdater();
});
electron.powerMonitor.on('resume', () => {
page.send('reload-viewer');
});
ipcMain.on('focus-app', () => {
mainWindow.show();
});
ipcMain.on('quit-app', () => {
app.quit();
});
// Reload full app not just webview, useful in debugging
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
page.send('destroytray');
});
ipcMain.on('clear-app-settings', () => {
global.mainWindowState.unmanage(mainWindow);
app.relaunch();
app.exit();
});
ipcMain.on('toggle-app', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('update-badge', (event, messageCount) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount);
});
ipcMain.on('update-taskbar-icon', (event, data, text) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipcMain.on('forward-message', (event, listener, ...params) => {
page.send(listener, ...params);
});
ipcMain.on('update-menu', (event, props) => {
appMenu.setMenu(props);
});
ipcMain.on('register-server-tab-shortcut', (event, index) => {
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
// Array index == Shown index - 1
page.send('switch-server-tab', index - 1);
});
});
ipcMain.on('local-shortcuts', (event, enable) => {
if (enable) {
registerLocalShortcuts(page);
} else {
electronLocalshortcut.unregisterAll(mainWindow);
}
});
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue);
});
});
app.on('will-quit', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('before-quit', () => {
isQuitting = true;
});
// Send crash reports
process.on('uncaughtException', err => {
console.error(err);
console.error(err.stack);
});

View File

@@ -1,502 +0,0 @@
import electron, {app, dialog, session} from "electron";
import fs from "fs";
import path from "path";
import windowStateKeeper from "electron-window-state";
import * as ConfigUtil from "../common/config-util";
import {sentryInit} from "../common/sentry-util";
import type {RendererMessage} from "../common/typed-ipc";
import type {MenuProps} from "../common/types";
import {appUpdater} from "./autoupdater";
import * as BadgeSettings from "./badge-settings";
import * as AppMenu from "./menu";
import * as ProxyUtil from "./proxy-util";
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request";
import {setAutoLaunch} from "./startup";
import {ipcMain, send} from "./typed-ipc-main";
const {GDK_BACKEND} = process.env;
let mainWindowState: windowStateKeeper.State;
// Prevent window being garbage collected
let mainWindow: Electron.BrowserWindow;
let badgeCount: number;
let isQuitting = false;
// Load this url in main window
const mainURL = "file://" + path.join(__dirname, "../renderer", "main.html");
const permissionCallbacks = new Map();
let nextPermissionCallbackId = 0;
const APP_ICON = path.join(__dirname, "../resources", "Icon");
const iconPath = (): string =>
APP_ICON + (process.platform === "win32" ? ".ico" : ".png");
// Toggle the app window
const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
};
function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults
mainWindowState = windowStateKeeper({
defaultWidth: 1100,
defaultHeight: 720,
path: `${app.getPath("userData")}/config`,
});
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
title: "Zulip",
icon: iconPath(),
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 500,
minHeight: 400,
webPreferences: {
contextIsolation: false,
enableRemoteModule: true,
nodeIntegration: true,
partition: "persist:webviewsession",
webviewTag: true,
worldSafeExecuteJavaScript: true,
},
show: false,
});
win.on("focus", () => {
send(win.webContents, "focus");
});
(async () => win.loadURL(mainURL))();
// Keep the app running in background on close event
win.on("close", (event) => {
if (ConfigUtil.getConfigItem("quitOnClose", false)) {
app.quit();
}
if (!isQuitting) {
event.preventDefault();
if (process.platform === "darwin") {
app.hide();
} else {
win.hide();
}
}
});
win.setTitle("Zulip");
win.on("enter-full-screen", () => {
send(win.webContents, "enter-fullscreen");
});
win.on("leave-full-screen", () => {
send(win.webContents, "leave-fullscreen");
});
// To destroy tray icon when navigate to a new URL
win.webContents.on("will-navigate", (event) => {
if (event) {
send(win.webContents, "destroytray");
}
});
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
mainWindowState.manage(win);
return win;
}
// Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch("force-color-profile", "srgb");
(async () => {
if (!app.requestSingleInstanceLock()) {
app.quit();
return;
}
await app.whenReady();
if (process.env.GDK_BACKEND !== GDK_BACKEND) {
console.warn(
"Reverting GDK_BACKEND to work around https://github.com/electron/electron/issues/28436",
);
if (GDK_BACKEND === undefined) {
delete process.env.GDK_BACKEND;
} else {
process.env.GDK_BACKEND = GDK_BACKEND;
}
}
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.show();
}
});
ipcMain.on(
"permission-callback",
(event: Event, permissionCallbackId: number, grant: boolean) => {
permissionCallbacks.get(permissionCallbackId)(grant);
permissionCallbacks.delete(permissionCallbackId);
},
);
// This event is only available on macOS. Triggers when you click on the dock icon.
app.on("activate", () => {
if (mainWindow) {
// If there is already a window show it
mainWindow.show();
} else {
mainWindow = createMainWindow();
}
});
const ses = session.fromPartition("persist:webviewsession");
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
ipcMain.on("set-spellcheck-langs", () => {
ses.setSpellCheckerLanguages(
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [],
);
});
AppMenu.setMenu({
tabs: [],
});
mainWindow = createMainWindow();
// Auto-hide menu bar on Windows + Linux
if (process.platform !== "darwin") {
const shouldHideMenu = ConfigUtil.getConfigItem("autoHideMenubar", false);
mainWindow.autoHideMenuBar = shouldHideMenu;
mainWindow.setMenuBarVisibility(!shouldHideMenu);
}
// Initialize sentry for main process
const errorReporting = ConfigUtil.getConfigItem("errorReporting", true);
if (errorReporting) {
sentryInit();
}
const isSystemProxy = ConfigUtil.getConfigItem("useSystemProxy", false);
if (isSystemProxy) {
(async () => ProxyUtil.resolveSystemProxy(mainWindow))();
}
const page = mainWindow.webContents;
page.on("dom-ready", () => {
if (ConfigUtil.getConfigItem("startMinimized", false)) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
ipcMain.on("fetch-user-agent", (event) => {
event.returnValue = session
.fromPartition("persist:webviewsession")
.getUserAgent();
});
ipcMain.handle("get-server-settings", async (event, domain: string) =>
_getServerSettings(domain, ses),
);
ipcMain.handle("save-server-icon", async (event, url: string) =>
_saveServerIcon(url, ses),
);
ipcMain.handle("is-online", async (event, url: string) =>
_isOnline(url, ses),
);
page.once("did-frame-finish-load", async () => {
// Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem("autoUpdate", true)) {
await appUpdater();
}
});
app.on(
"certificate-error",
(
event: Event,
webContents: Electron.WebContents,
urlString: string,
error: string,
) => {
const url = new URL(urlString);
dialog.showErrorBox(
"Certificate error",
`The server presented an invalid certificate for ${url.origin}:
${error}`,
);
},
);
page.session.setPermissionRequestHandler(
(webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
const permissionCallbackId = nextPermissionCallbackId++;
permissionCallbacks.set(permissionCallbackId, callback);
send(
page,
"permission-request",
{
webContentsId:
webContents.id === mainWindow.webContents.id
? null
: webContents.id,
origin,
permission,
},
permissionCallbackId,
);
},
);
// Temporarily remove this event
// electron.powerMonitor.on('resume', () => {
// mainWindow.reload();
// send(page, 'destroytray');
// });
ipcMain.on("focus-app", () => {
mainWindow.show();
});
ipcMain.on("quit-app", () => {
app.quit();
});
// Reload full app not just webview, useful in debugging
ipcMain.on("reload-full-app", () => {
mainWindow.reload();
send(page, "destroytray");
});
ipcMain.on("clear-app-settings", () => {
mainWindowState.unmanage();
app.relaunch();
app.exit();
});
ipcMain.on("toggle-app", () => {
toggleApp();
});
ipcMain.on("toggle-badge-option", () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on(
"toggle-menubar",
(_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
send(page, "toggle-autohide-menubar", showMenubar, true);
},
);
ipcMain.on(
"update-badge",
(_event: Electron.IpcMainEvent, messageCount: number) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
send(page, "tray", messageCount);
},
);
ipcMain.on(
"update-taskbar-icon",
(_event: Electron.IpcMainEvent, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
},
);
ipcMain.on(
"forward-message",
<Channel extends keyof RendererMessage>(
_event: Electron.IpcMainEvent,
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
send(page, listener, ...parameters);
},
);
ipcMain.on(
"update-menu",
(_event: Electron.IpcMainEvent, props: MenuProps) => {
AppMenu.setMenu(props);
if (props.activeTabIndex !== undefined) {
const activeTab = props.tabs[props.activeTabIndex];
mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
}
},
);
ipcMain.on(
"toggleAutoLauncher",
async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
},
);
ipcMain.on(
"downloadFile",
(_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url);
page.session.once("will-download", async (_event: Event, item) => {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
const showDialogOptions: electron.SaveDialogOptions = {
defaultPath: path.join(downloadPath, item.getFilename()),
};
item.setSaveDialogOptions(showDialogOptions);
} else {
const getTimeStamp = (): number => {
const date = new Date();
return date.getTime();
};
const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
const filePath = path.join(downloadPath, item.getFilename());
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath: string = fs.existsSync(filePath)
? updatedFilePath
: filePath;
item.setSavePath(setFilePath);
}
const updatedListener = (_event: Event, state: string): void => {
switch (state) {
case "interrupted": {
// Can interrupted to due to network error, cancel download then
console.log(
"Download interrupted, cancelling and fallback to dialog download.",
);
item.cancel();
break;
}
case "progressing": {
if (item.isPaused()) {
item.cancel();
}
// This event can also be used to show progress in percentage in future.
break;
}
default: {
console.info("Unknown updated state of download item");
}
}
};
item.on("updated", updatedListener);
item.once("done", (_event: Event, state) => {
if (state === "completed") {
send(
page,
"downloadFileCompleted",
item.getSavePath(),
path.basename(item.getSavePath()),
);
} else {
console.log("Download failed state:", state);
send(page, "downloadFileFailed", state);
}
// To stop item for listening to updated events of this file
item.removeListener("updated", updatedListener);
});
});
},
);
ipcMain.on(
"realm-name-changed",
(_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
send(page, "update-realm-name", serverURL, realmName);
},
);
ipcMain.on(
"realm-icon-changed",
(_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
send(page, "update-realm-icon", serverURL, iconURL);
},
);
// Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load.
ipcMain.on("error-reporting", (event: Electron.IpcMainEvent) => {
send(event.sender, "error-reporting-val", errorReporting);
});
ipcMain.on(
"save-last-tab",
(_event: Electron.IpcMainEvent, index: number) => {
ConfigUtil.setConfigItem("lastActiveTab", index);
},
);
// Update user idle status for each realm after every 15s
const idleCheckInterval = 15 * 1000; // 15 seconds
setInterval(() => {
// Set user idle if no activity in 1 second (idleThresholdSeconds)
const idleThresholdSeconds = 1; // 1 second
const idleState = electron.powerMonitor.getSystemIdleState(
idleThresholdSeconds,
);
if (idleState === "active") {
send(page, "set-active");
} else {
send(page, "set-idle");
}
}, idleCheckInterval);
})();
app.on("before-quit", () => {
isQuitting = true;
});
// Send crash reports
process.on("uncaughtException", (error) => {
console.error(error);
console.error(error.stack);
});

View File

@@ -1,59 +0,0 @@
import {app, dialog} from "electron";
import fs from "fs";
import path from "path";
import {JsonDB} from "node-json-db";
import Logger from "../common/logger-util";
const logger = new Logger({
file: "linux-update-util.log",
});
let db: JsonDB;
reloadDB();
export function getUpdateItem(key: string, defaultValue: unknown = null): any {
reloadDB();
const value = db.getData("/")[key];
if (value === undefined) {
setUpdateItem(key, defaultValue);
return defaultValue;
}
return value;
}
export function setUpdateItem(key: string, value: unknown): void {
db.push(`/${key}`, value, true);
reloadDB();
}
export function removeUpdateItem(key: string): void {
db.delete(`/${key}`);
reloadDB();
}
function reloadDB(): void {
const linuxUpdateJsonPath = path.join(
app.getPath("userData"),
"/config/updates.json",
);
try {
const file = fs.readFileSync(linuxUpdateJsonPath, "utf8");
JSON.parse(file);
} catch (error: unknown) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
"Error saving update notifications.",
"We encountered an error while saving the update notifications.",
);
logger.error("Error while JSON parsing updates.json: ");
logger.error(error);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -1,51 +0,0 @@
import {Notification, app, net} from "electron";
import getStream from "get-stream";
import * as semver from "semver";
import * as ConfigUtil from "../common/config-util";
import Logger from "../common/logger-util";
import * as LinuxUpdateUtil from "./linux-update-util";
import {fetchResponse} from "./request";
const logger = new Logger({
file: "linux-update-util.log",
});
export async function linuxUpdateNotification(
session: Electron.session,
): Promise<void> {
let url = "https://api.github.com/repos/zulip/zulip-desktop/releases";
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);
return;
}
const data = JSON.parse(await getStream(response));
const latestVersion = ConfigUtil.getConfigItem("betaUpdate", false)
? data[0].tag_name
: data.tag_name;
if (typeof latestVersion !== "string") {
throw new TypeError("Expected string for tag_name");
}
if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) {
new Notification({
title: "Zulip Update",
body: `A new version ${latestVersion} is available. Please update using your package manager.`,
}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}
}
} catch (error: unknown) {
logger.error("Linux update error.");
logger.error(error);
}
}

387
app/main/menu.js Normal file
View File

@@ -0,0 +1,387 @@
'use strict';
const os = require('os');
const path = require('path');
const { app, shell, BrowserWindow, Menu } = require('electron');
const fs = require('fs-extra');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
const appName = app.getName();
class AppMenu {
getHistorySubmenu() {
return [{
label: 'Back',
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('back');
}
}
}, {
label: 'Forward',
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('forward');
}
}
}];
}
getViewSubmenu() {
return [{
label: 'Reload',
accelerator: 'CommandOrControl+R',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('reload-viewer');
}
}
}, {
label: 'Hard Reload',
accelerator: 'CommandOrControl+Shift+R',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('hard-reload');
}
}
}, {
type: 'separator'
}, {
role: 'togglefullscreen'
}, {
label: 'Zoom In',
accelerator: 'CommandOrControl+=',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('zoomIn');
}
}
}, {
label: 'Zoom Out',
accelerator: 'CommandOrControl+-',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('zoomOut');
}
}
}, {
label: 'Actual Size',
accelerator: 'CommandOrControl+0',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('zoomActualSize');
}
}
}, {
type: 'separator'
}, {
label: 'Toggle Tray Icon',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.send('toggletray');
}
}
}, {
label: 'Toggle Sidebar',
accelerator: 'CommandOrControl+S',
click(item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue);
ConfigUtil.setConfigItem('showSidebar', newValue);
}
}
}, {
label: 'Toggle DevTools for Zulip App',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
}
}, {
label: 'Toggle DevTools for Active Tab',
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('tab-devtools');
}
}
}];
}
getHelpSubmenu() {
return [{
label: `${appName} Website`,
click() {
shell.openExternal('https://zulipchat.com/help/');
}
}, {
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
enabled: false
}, {
label: 'Report an Issue...',
click() {
const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. -->
-
${app.getName()} ${app.getVersion()}
Electron ${process.versions.electron}
${process.platform} ${process.arch} ${os.release()}`;
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
}
}];
}
getWindowSubmenu(tabs, activeTabIndex) {
const initialSubmenu = [{
role: 'minimize'
}, {
role: 'close'
}];
if (tabs.length > 0) {
const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
initialSubmenu.push({
type: 'separator'
});
for (let i = 0; i < tabs.length; i++) {
initialSubmenu.push({
label: tabs[i].webview.props.name,
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
checked: tabs[i].props.index === activeTabIndex,
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
}
},
type: 'radio'
});
}
}
return initialSubmenu;
}
getDarwinTpl(props) {
const { tabs, activeTabIndex } = props;
return [{
label: `${app.getName()}`,
submenu: [{
label: 'Zulip Desktop',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
}
}
}, {
type: 'separator'
}, {
label: 'Settings',
accelerator: 'Cmd+,',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-settings');
}
}
}, {
label: 'Keyboard Shortcuts',
accelerator: 'Cmd+K',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('shortcut');
}
}
}, {
type: 'separator'
}, {
label: 'Reset App Settings',
accelerator: 'Command+Shift+D',
click() {
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
accelerator: 'Cmd+L',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('log-out');
}
}
}, {
type: 'separator'
}, {
role: 'services',
submenu: []
}, {
type: 'separator'
}, {
role: 'hide'
}, {
role: 'hideothers'
}, {
role: 'unhide'
}, {
type: 'separator'
}, {
role: 'quit'
}]
}, {
label: 'Edit',
submenu: [{
role: 'undo'
}, {
role: 'redo'
}, {
type: 'separator'
}, {
role: 'cut'
}, {
role: 'copy'
}, {
role: 'paste'
}, {
role: 'pasteandmatchstyle'
}, {
role: 'delete'
}, {
role: 'selectall'
}]
}, {
label: 'View',
submenu: this.getViewSubmenu()
}, {
label: 'History',
submenu: this.getHistorySubmenu()
}, {
label: 'Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
}, {
role: 'help',
submenu: this.getHelpSubmenu()
}];
}
getOtherTpl(props) {
const { tabs, activeTabIndex } = props;
return [{
label: 'File',
submenu: [{
label: 'Zulip Desktop',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
}
}
}, {
type: 'separator'
}, {
label: 'Settings',
accelerator: 'Ctrl+,',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-settings');
}
}
}, {
type: 'separator'
}, {
label: 'Keyboard Shortcuts',
accelerator: 'Ctrl+K',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('shortcut');
}
}
}, {
type: 'separator'
}, {
label: 'Reset App Settings',
accelerator: 'Ctrl+Shift+D',
click() {
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
accelerator: 'Ctrl+L',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('log-out');
}
}
}, {
type: 'separator'
}, {
role: 'quit',
accelerator: 'Ctrl+Q'
}]
}, {
label: 'Edit',
submenu: [{
role: 'undo'
}, {
role: 'redo'
}, {
type: 'separator'
}, {
role: 'cut'
}, {
role: 'copy'
}, {
role: 'paste'
}, {
role: 'pasteandmatchstyle'
}, {
role: 'delete'
}, {
type: 'separator'
}, {
role: 'selectall'
}]
}, {
label: 'View',
submenu: this.getViewSubmenu()
}, {
label: 'History',
submenu: this.getHistorySubmenu()
}, {
label: 'Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
}, {
role: 'help',
submenu: this.getHelpSubmenu()
}];
}
static sendAction(action, ...params) {
const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') {
win.restore();
}
win.webContents.send(action, ...params);
}
static resetAppSettings() {
const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
fs.unlink(getAppPath, () => {
setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
});
}
setMenu(props) {
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);
}
}
module.exports = new AppMenu();

View File

@@ -1,723 +0,0 @@
import {BrowserWindow, Menu, app, shell} from "electron";
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 {appUpdater} from "./autoupdater";
import {send} from "./typed-ipc-main";
const appName = app.name;
function getHistorySubmenu(
enableMenu: boolean,
): Electron.MenuItemConstructorOptions[] {
return [
{
label: t.__("Back"),
accelerator: process.platform === "darwin" ? "Command+Left" : "Alt+Left",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("back");
}
},
},
{
label: t.__("Forward"),
accelerator:
process.platform === "darwin" ? "Command+Right" : "Alt+Right",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("forward");
}
},
},
];
}
function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
return [
{
label: t.__("Check for Updates"),
async click() {
await checkForUpdate();
},
},
{
label: t.__("Release Notes"),
async click() {
await shell.openExternal(
`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`,
);
},
},
{
type: "separator",
},
{
label: t.__("Download App Logs"),
click() {
const zip = new AdmZip();
const date = new Date();
const dateString = date.toLocaleDateString().replace(/\//g, "-");
// Create a zip file of all the logs and config data
zip.addLocalFolder(`${app.getPath("appData")}/${appName}/Logs`);
zip.addLocalFolder(`${app.getPath("appData")}/${appName}/config`);
// Put the log file in downloads folder
const logFilePath = `${app.getPath(
"downloads",
)}/Zulip-logs-${dateString}.zip`;
zip.writeZip(logFilePath);
// Open and select the log file
shell.showItemInFolder(logFilePath);
},
},
{
type: "separator",
},
{
label: t.__("Toggle DevTools for Zulip App"),
accelerator:
process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",
click(_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.openDevTools({mode: "undocked"});
}
},
},
{
label: t.__("Toggle DevTools for Active Tab"),
accelerator:
process.platform === "darwin" ? "Alt+Command+U" : "Ctrl+Shift+U",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("tab-devtools");
}
},
},
];
}
function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [
{
label: t.__("Reload"),
accelerator: "CommandOrControl+R",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("reload-current-viewer");
}
},
},
{
label: t.__("Hard Reload"),
accelerator: "CommandOrControl+Shift+R",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("hard-reload");
}
},
},
{
label: t.__("Hard Reload"),
visible: false,
accelerator: "F5",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("hard-reload");
}
},
},
{
type: "separator",
},
{
label: t.__("Toggle Full Screen"),
role: "togglefullscreen",
},
{
label: t.__("Zoom In"),
accelerator: "CommandOrControl+=",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomIn");
}
},
},
{
label: t.__("Zoom In"),
visible: false,
accelerator: "CommandOrControl+Plus",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomIn");
}
},
},
{
label: t.__("Zoom In"),
visible: false,
accelerator: "CommandOrControl+numadd",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomIn");
}
},
},
{
label: t.__("Zoom Out"),
accelerator: "CommandOrControl+-",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomOut");
}
},
},
{
label: t.__("Zoom Out"),
visible: false,
accelerator: "CommandOrControl+numsub",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomOut");
}
},
},
{
label: t.__("Actual Size"),
accelerator: "CommandOrControl+0",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomActualSize");
}
},
},
{
label: t.__("Actual Size"),
visible: false,
accelerator: "CommandOrControl+num0",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("zoomActualSize");
}
},
},
{
type: "separator",
},
{
label: t.__("Toggle Tray Icon"),
click(_item, focusedWindow) {
if (focusedWindow) {
send(focusedWindow.webContents, "toggletray");
}
},
},
{
label: t.__("Toggle Sidebar"),
accelerator: "CommandOrControl+Shift+S",
click(_item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
send(focusedWindow.webContents, "toggle-sidebar", newValue);
ConfigUtil.setConfigItem("showSidebar", newValue);
}
},
},
{
label: t.__("Auto hide Menu bar"),
checked: ConfigUtil.getConfigItem("autoHideMenubar", false),
visible: process.platform !== "darwin",
click(_item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false);
focusedWindow.autoHideMenuBar = newValue;
focusedWindow.setMenuBarVisibility(!newValue);
send(
focusedWindow.webContents,
"toggle-autohide-menubar",
newValue,
false,
);
ConfigUtil.setConfigItem("autoHideMenubar", newValue);
}
},
type: "checkbox",
},
];
}
function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
return [
{
label: `${appName + " Desktop"} v${app.getVersion()}`,
enabled: false,
},
{
label: t.__("About Zulip"),
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("open-about");
}
},
},
{
label: t.__("Help Center"),
click(focusedWindow) {
if (focusedWindow) {
sendAction("open-help");
}
},
},
{
label: t.__("Report an Issue"),
click() {
// The goal is to notify the main.html BrowserWindow
// which may not be the focused window.
for (const window of BrowserWindow.getAllWindows()) {
send(window.webContents, "open-feedback-modal");
}
},
},
];
}
function getWindowSubmenu(
tabs: TabData[],
activeTabIndex?: number,
): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [
{
label: t.__("Minimize"),
role: "minimize",
},
{
label: t.__("Close"),
role: "close",
},
];
if (tabs.length > 0) {
const ShortcutKey = process.platform === "darwin" ? "Cmd" : "Ctrl";
initialSubmenu.push({
type: "separator",
});
for (const tab of tabs) {
// Do not add functional tab settings to list of windows in menu bar
if (tab.role === "function" && tab.name === "Settings") {
continue;
}
initialSubmenu.push({
label: tab.name,
accelerator:
tab.role === "function" ? "" : `${ShortcutKey} + ${tab.index + 1}`,
checked: tab.index === activeTabIndex,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("switch-server-tab", tab.index);
}
},
type: "checkbox",
});
}
initialSubmenu.push(
{
type: "separator",
},
{
label: t.__("Switch to Next Organization"),
accelerator: "Ctrl+Tab",
enabled: tabs.length > 1,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction(
"switch-server-tab",
getNextServer(tabs, activeTabIndex!),
);
}
},
},
{
label: t.__("Switch to Previous Organization"),
accelerator: "Ctrl+Shift+Tab",
enabled: tabs.length > 1,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction(
"switch-server-tab",
getPreviousServer(tabs, activeTabIndex!),
);
}
},
},
);
}
return initialSubmenu;
}
function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props;
return [
{
label: app.name,
submenu: [
{
label: t.__("Add Organization"),
accelerator: "Cmd+Shift+N",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("new-server");
}
},
},
{
label: t.__("Toggle Do Not Disturb"),
accelerator: "Cmd+Shift+M",
click() {
const dndUtil = DNDUtil.toggle();
sendAction("toggle-dnd", dndUtil.dnd, dndUtil.newSettings);
},
},
{
label: t.__("Desktop Settings"),
accelerator: "Cmd+,",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("open-settings");
}
},
},
{
label: t.__("Keyboard Shortcuts"),
accelerator: "Cmd+Shift+K",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("show-keyboard-shortcuts");
}
},
},
{
type: "separator",
},
{
label: t.__("Copy Zulip URL"),
accelerator: "Cmd+Shift+C",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("copy-zulip-url");
}
},
},
{
label: t.__("Log Out of Organization"),
accelerator: "Cmd+L",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("log-out");
}
},
},
{
type: "separator",
},
{
label: t.__("Services"),
role: "services",
submenu: [],
},
{
type: "separator",
},
{
label: t.__("Hide"),
role: "hide",
},
{
label: t.__("Hide Others"),
role: "hideOthers",
},
{
label: t.__("Unhide"),
role: "unhide",
},
{
type: "separator",
},
{
label: t.__("Minimize"),
role: "minimize",
},
{
label: t.__("Close"),
role: "close",
},
{
label: t.__("Quit"),
role: "quit",
},
],
},
{
label: t.__("Edit"),
submenu: [
{
label: t.__("Undo"),
role: "undo",
},
{
label: t.__("Redo"),
role: "redo",
},
{
type: "separator",
},
{
label: t.__("Cut"),
role: "cut",
},
{
label: t.__("Copy"),
role: "copy",
},
{
label: t.__("Paste"),
role: "paste",
},
{
label: t.__("Paste and Match Style"),
role: "pasteAndMatchStyle",
},
{
label: t.__("Select All"),
role: "selectAll",
},
],
},
{
label: t.__("View"),
submenu: getViewSubmenu(),
},
{
label: t.__("History"),
submenu: getHistorySubmenu(enableMenu),
},
{
label: t.__("Window"),
submenu: getWindowSubmenu(tabs, activeTabIndex),
},
{
label: t.__("Tools"),
submenu: getToolsSubmenu(),
},
{
label: t.__("Help"),
role: "help",
submenu: getHelpSubmenu(),
},
];
}
function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props;
return [
{
label: t.__("File"),
submenu: [
{
label: t.__("Add Organization"),
accelerator: "Ctrl+Shift+N",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("new-server");
}
},
},
{
type: "separator",
},
{
label: t.__("Toggle Do Not Disturb"),
accelerator: "Ctrl+Shift+M",
click() {
const dndUtil = DNDUtil.toggle();
sendAction("toggle-dnd", dndUtil.dnd, dndUtil.newSettings);
},
},
{
label: t.__("Desktop Settings"),
accelerator: "Ctrl+,",
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("open-settings");
}
},
},
{
label: t.__("Keyboard Shortcuts"),
accelerator: "Ctrl+Shift+K",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("show-keyboard-shortcuts");
}
},
},
{
type: "separator",
},
{
label: t.__("Copy Zulip URL"),
accelerator: "Ctrl+Shift+C",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("copy-zulip-url");
}
},
},
{
label: t.__("Log Out of Organization"),
accelerator: "Ctrl+L",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction("log-out");
}
},
},
{
type: "separator",
},
{
label: t.__("Minimize"),
role: "minimize",
},
{
label: t.__("Close"),
role: "close",
},
{
label: t.__("Quit"),
role: "quit",
accelerator: "Ctrl+Q",
},
],
},
{
label: t.__("Edit"),
submenu: [
{
label: t.__("Undo"),
role: "undo",
},
{
label: t.__("Redo"),
role: "redo",
},
{
type: "separator",
},
{
label: t.__("Cut"),
role: "cut",
},
{
label: t.__("Copy"),
role: "copy",
},
{
label: t.__("Paste"),
role: "paste",
},
{
label: t.__("Paste and Match Style"),
role: "pasteAndMatchStyle",
},
{
type: "separator",
},
{
label: t.__("Select All"),
role: "selectAll",
},
],
},
{
label: t.__("View"),
submenu: getViewSubmenu(),
},
{
label: t.__("History"),
submenu: getHistorySubmenu(enableMenu),
},
{
label: t.__("Window"),
submenu: getWindowSubmenu(tabs, activeTabIndex),
},
{
label: t.__("Tools"),
submenu: getToolsSubmenu(),
},
{
label: t.__("Help"),
role: "help",
submenu: getHelpSubmenu(),
},
];
}
function sendAction<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void {
const win = BrowserWindow.getAllWindows()[0];
if (process.platform === "darwin") {
win.restore();
}
send(win.webContents, channel, ...args);
}
async function checkForUpdate(): Promise<void> {
await appUpdater(true);
}
function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex + 1) % tabs.length;
} while (tabs[activeTabIndex].role !== "server");
return activeTabIndex;
}
function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} while (tabs[activeTabIndex].role !== "server");
return activeTabIndex;
}
export function setMenu(props: MenuProps): void {
const tpl =
process.platform === "darwin" ? getDarwinTpl(props) : getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);
}

View File

@@ -1,87 +0,0 @@
import * as ConfigUtil from "../common/config-util";
export interface ProxyRule {
hostname?: string;
port?: number;
}
// TODO: Refactor to async function
export async function resolveSystemProxy(
mainWindow: Electron.BrowserWindow,
): Promise<void> {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = "www.example.com";
// Check HTTP Proxy
const httpProxy = (async () => {
const proxy = await ses.resolveProxy("http://" + resolveProxyUrl);
let httpString = "";
if (
proxy !== "DIRECT" &&
(proxy.includes("PROXY") || proxy.includes("HTTPS"))
) {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
httpString = "http=" + proxy.split("PROXY")[1] + ";";
}
return httpString;
})();
// Check HTTPS Proxy
const httpsProxy = (async () => {
const proxy = await ses.resolveProxy("https://" + resolveProxyUrl);
let httpsString = "";
if (
(proxy !== "DIRECT" || proxy.includes("HTTPS")) &&
(proxy.includes("PROXY") || proxy.includes("HTTPS"))
) {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
httpsString += "https=" + proxy.split("PROXY")[1] + ";";
}
return httpsString;
})();
// Check FTP Proxy
const ftpProxy = (async () => {
const proxy = await ses.resolveProxy("ftp://" + resolveProxyUrl);
let ftpString = "";
if (proxy !== "DIRECT" && proxy.includes("PROXY")) {
ftpString += "ftp=" + proxy.split("PROXY")[1] + ";";
}
return ftpString;
})();
// Check SOCKS Proxy
const socksProxy = (async () => {
const proxy = await ses.resolveProxy("socks4://" + resolveProxyUrl);
let socksString = "";
if (proxy !== "DIRECT") {
if (proxy.includes("SOCKS5")) {
socksString += "socks=" + proxy.split("SOCKS5")[1] + ";";
} else if (proxy.includes("SOCKS4")) {
socksString += "socks=" + proxy.split("SOCKS4")[1] + ";";
} else if (proxy.includes("PROXY")) {
socksString += "socks=" + proxy.split("PROXY")[1] + ";";
}
}
return socksString;
})();
const values = await Promise.all([
httpProxy,
httpsProxy,
ftpProxy,
socksProxy,
]);
const proxyString = values.join("");
ConfigUtil.setConfigItem("systemProxyRules", proxyString);
const useSystemProxy = ConfigUtil.getConfigItem("useSystemProxy", false);
if (useSystemProxy) {
ConfigUtil.setConfigItem("proxyRules", proxyString);
}
}

View File

@@ -1,133 +0,0 @@
import type {ClientRequest, IncomingMessage} from "electron";
import {app, net} from "electron";
import fs from "fs";
import path from "path";
import stream from "stream";
import util from "util";
import getStream from "get-stream";
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);
/* Request: domain-util */
const defaultIconUrl = "../renderer/img/icon.png";
const logger = new Logger({
file: "domain-util.log",
});
const generateFilePath = (url: string): string => {
const dir = `${app.getPath("userData")}/server-icons`;
const extension = path.extname(url).split("?")[0];
let hash = 5381;
let {length} = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
};
export const _getServerSettings = async (
domain: string,
session: Electron.session,
): Promise<ServerConf> => {
const response = await fetchResponse(
net.request({
url: domain + "/api/v1/server_settings",
session,
}),
);
if (response.statusCode !== 200) {
throw new Error(Messages.invalidZulipServerError(domain));
}
const {realm_name, realm_uri, realm_icon} = JSON.parse(
await getStream(response),
);
if (
typeof realm_name !== "string" ||
typeof realm_uri !== "string" ||
typeof realm_icon !== "string"
) {
throw new TypeError(Messages.noOrgsError(domain));
}
return {
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: realm_icon.startsWith("/") ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: realm_name,
};
};
export const _saveServerIcon = async (
url: string,
session: Electron.session,
): Promise<string> => {
try {
const response = await fetchResponse(net.request({url, session}));
if (response.statusCode !== 200) {
logger.log("Could not get server icon.");
return defaultIconUrl;
}
const filePath = generateFilePath(url);
await pipeline(response, fs.createWriteStream(filePath));
return filePath;
} catch (error: unknown) {
logger.log("Could not get server icon.");
logger.log(error);
logger.reportSentry(error);
return defaultIconUrl;
}
};
/* Request: reconnect-util */
export const _isOnline = async (
url: string,
session: Electron.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;
} catch (error: unknown) {
logger.log(error);
return false;
}
};

34
app/main/startup.js Normal file
View File

@@ -0,0 +1,34 @@
'use strict';
const { app } = require('electron');
const AutoLaunch = require('auto-launch');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const setAutoLaunch = AutoLaunchValue => {
// Don't run this in development
if (isDev) {
return;
}
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
path: appPath,
isHidden: false
});
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
}
};
module.exports = {
setAutoLaunch
};

View File

@@ -1,35 +0,0 @@
import {app} from "electron";
import AutoLaunch from "auto-launch";
import * as ConfigUtil from "../common/config-util";
export const setAutoLaunch = async (
AutoLaunchValue: boolean,
): Promise<void> => {
// Don't run this in development
if (!app.isPackaged) {
return;
}
const autoLaunchOption = ConfigUtil.getConfigItem(
"startAtLogin",
AutoLaunchValue,
);
// `setLoginItemSettings` doesn't support linux
if (process.platform === "linux") {
const ZulipAutoLauncher = new AutoLaunch({
name: "Zulip",
isHidden: false,
});
await (autoLaunchOption
? ZulipAutoLauncher.enable()
: ZulipAutoLauncher.disable());
} else {
app.setLoginItemSettings({
openAtLogin: autoLaunchOption,
openAsHidden: false,
});
}
};

View File

@@ -1,68 +0,0 @@
import type {IpcMainEvent, IpcMainInvokeEvent, WebContents} from "electron";
import {
ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports
} from "electron";
import type {MainCall, MainMessage, RendererMessage} from "../common/typed-ipc";
type MainListener<
Channel extends keyof MainMessage
> = MainMessage[Channel] extends (...args: infer Args) => infer Return
? (event: IpcMainEvent & {returnValue: Return}, ...args: Args) => void
: never;
type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends (
...args: infer Args
) => infer Return
? (event: IpcMainInvokeEvent, ...args: Args) => Return | Promise<Return>
: never;
export const ipcMain: {
on(
channel: "forward-message",
listener: <Channel extends keyof RendererMessage>(
event: IpcMainEvent,
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
) => void,
): void;
on<Channel extends keyof MainMessage>(
channel: Channel,
listener: MainListener<Channel>,
): void;
once<Channel extends keyof MainMessage>(
channel: Channel,
listener: MainListener<Channel>,
): void;
removeListener<Channel extends keyof MainMessage>(
channel: Channel,
listener: MainListener<Channel>,
): void;
removeAllListeners(channel?: keyof MainMessage): void;
handle<Channel extends keyof MainCall>(
channel: Channel,
handler: MainHandler<Channel>,
): void;
handleOnce<Channel extends keyof MainCall>(
channel: Channel,
handler: MainHandler<Channel>,
): void;
removeHandler(channel: keyof MainCall): void;
} = untypedIpcMain;
export function send<Channel extends keyof RendererMessage>(
contents: WebContents,
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void {
contents.send(channel, ...args);
}
export function sendToFrame<Channel extends keyof RendererMessage>(
contents: WebContents,
frameId: number | [number, number],
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void {
contents.sendToFrame(frameId, channel, ...args);
}

42
app/package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.4.0",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"email": "<svnitakash@gmail.com>",
"copyright": "©2017 Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "svnitakash@gmail.com"
},
"repository": {
"type": "git",
"url": "https://github.com/zulip/zulip-electron.git"
},
"bugs": {
"url": "https://github.com/zulip/zulip-electron/issues"
},
"main": "main/index.js",
"keywords": [
"Zulip",
"Group Chat app",
"electron-app",
"electron",
"Desktop app",
"InstantMessaging"
],
"dependencies": {
"electron-debug": "1.4.0",
"electron-is-dev": "0.3.0",
"electron-localshortcut": "2.0.2",
"electron-log": "2.2.7",
"electron-spellchecker": "1.2.0",
"electron-updater": "2.8.9",
"node-json-db": "0.7.3",
"request": "2.81.0",
"wurl": "2.5.0",
"electron-window-state": "4.1.1",
"auto-launch": "5.0.1"
}
}

View File

@@ -1,37 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/about.css" />
<title>Zulip - About</title>
</head>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/about.css">
</head>
<body>
<div class="about">
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">version ?.?.?</p>
<div class="maintenance-info">
<p class="detail maintainer">Maintained by Zulip</p>
<p class="detail license">Available under the Apache License</p>
<a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a>
</div>
</div>
<script>
<body>
<div class="about">
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">v?.?.?</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>
<script>
const {app} = require("electron").remote;
const version_tag = document.querySelector("#version");
version_tag.textContent = "v" + app.getVersion();
</script>
</body>
const app = require('electron').remote.app;
const version_tag = document.getElementById('version');
version_tag.innerHTML = 'version ' + app.getVersion();
function linkInBrowser(event) {
const shell = require('electron').shell;
const url = "https://github.com/zulip/zulip-electron/issues/new?body=Please describe your issue and steps to reproduce it."
shell.openExternal(url);
}
</script>
</body>
</html>

View File

@@ -1,66 +1,73 @@
body {
background: rgba(250, 250, 250, 1);
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: subpixel-antialiased;
background: #fafafa;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
}
.logo {
display: block;
margin: -40px auto;
display: block;
margin: 0 auto;
}
#version {
color: rgba(68, 67, 67, 1);
font-size: 1.3em;
padding-top: 40px;
color: #aaa;
font-size: 0.9em;
}
.about {
margin: 25vh auto;
height: 25vh;
text-align: center;
margin-top: 50px;
text-align: center;
}
.about p {
font-size: 20px;
color: rgba(0, 0, 0, 0.62);
font-size: 20px;
color: rgba(0, 0, 0, 0.62);
}
.about img {
width: 150px;
width: 150px;
}
.detail {
text-align: center;
text-align: center;
}
.detail.maintainer {
font-size: 1.2em;
font-weight: 500;
font-size: 1.2em;
font-weight: 500;
}
.detail.license {
font-size: 0.8em;
font-size: 0.8em;
}
.maintenance-info {
cursor: pointer;
position: absolute;
width: 100%;
left: 0;
color: rgba(68, 68, 68, 1);
position: absolute;
width: 100%;
bottom: 20px;
left: 0px;
color: #444;
}
.maintenance-info p {
margin: 0;
font-size: 1em;
width: 100%;
margin: 0;
font-size: 1em;
width: 100%;
}
p.detail a {
color: rgba(53, 95, 76, 1);
.maintenance-info .bug {
display: inline-block;
padding: 8px 15px;
margin-top: 30px;
text-decoration: none;
background-color: #52c2af;
color: #fff;
border-radius: 4px;
transition: background-color 0.2s ease;
}
p.detail a:hover {
text-decoration: underline;
.maintenance-info .bug:hover {
background-color: #32a692;
}

View File

@@ -1,19 +0,0 @@
:host {
--button-color: rgb(69, 166, 149);
}
button {
background-color: var(--button-color);
border-color: var(--button-color);
}
button:hover,
button:focus {
border-color: var(--button-color);
color: var(--button-color);
}
button:active {
background-color: rgb(241, 241, 241);
color: var(--button-color);
}

View File

@@ -4,489 +4,318 @@
html,
body {
height: 100%;
margin: 0;
cursor: default;
user-select: none;
height: 100%;
margin: 0;
cursor: default;
user-select: none;
}
#content {
display: flex;
height: 100%;
display: flex;
height: 100%;
background: #eee url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
}
.toggle-sidebar {
background: rgba(34, 44, 49, 1);
width: 54px;
padding: 27px 0 20px 0;
justify-content: space-between;
display: flex;
flex-direction: column;
-webkit-app-region: drag;
overflow: hidden;
transition: all 0.5s ease;
z-index: 2;
}
.toggle-sidebar div {
transition: all 0.5s ease-out;
}
.sidebar-hide {
width: 0;
transition: all 0.8s ease;
}
.sidebar-hide div {
transform: translateX(-100%);
transition: all 0.6s ease-out;
}
#view-controls-container {
height: calc(100% - 208px);
overflow-y: hidden;
}
#view-controls-container::-webkit-scrollbar {
width: 4px;
}
#view-controls-container::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
#view-controls-container::-webkit-scrollbar-thumb {
background-color: rgba(169, 169, 169, 1);
outline: 1px solid rgba(169, 169, 169, 1);
}
#view-controls-container:hover {
overflow-y: overlay;
#sidebar {
background: #222c31;
width: 54px;
padding: 27px 0 20px 0;
justify-content: space-between;
display: flex;
flex-direction: column;
-webkit-app-region: drag;
overflow: hidden;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: local("Material Icons"), local("MaterialIcons-Regular"),
url(../fonts/MaterialIcons-Regular.ttf) format("truetype");
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
}
/*******************
* Left Sidebar *
*******************/
* Left Sidebar *
*******************/
#tabs-container {
display: flex;
align-items: center;
flex-direction: column;
display: flex;
align-items: center;
flex-direction: column;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
}
#actions-container {
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
}
.action-button {
display: flex;
flex-direction: column;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
}
.action-button:hover {
cursor: pointer;
cursor: pointer;
}
.action-button i {
color: rgba(108, 133, 146, 1);
font-size: 28px;
color: #6c8592;
font-size: 28px;
}
.action-button:hover i {
color: rgba(152, 169, 179, 1);
}
.action-button.active {
/* background-color: rgba(255, 255, 255, 0.25); */
background-color: rgba(239, 239, 239, 1);
opacity: 0.9;
padding-right: 14px;
}
.action-button.active i {
color: rgba(28, 38, 43, 1);
}
.action-button.disable {
opacity: 0.6;
}
.action-button.disable:hover {
cursor: not-allowed;
}
.action-button.disable:hover i {
color: rgba(108, 133, 146, 1);
}
.tab {
position: relative;
margin: 2px 0;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
color: #98a9b3;
}
.tab:first-child {
margin-top: 9px;
margin-top: 8px;
}
.tab {
position: relative;
margin: 2px 0;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.tab .server-icons {
width: 35px;
vertical-align: top;
border-radius: 4px;
border-radius: 50%;
width: 30px;
padding: 3px;
height: 30px;
vertical-align: top;
}
.tab .server-tab {
width: 100%;
height: 35px;
position: relative;
margin-top: 5px;
z-index: 11;
line-height: 31px;
color: rgba(238, 238, 238, 1);
text-align: center;
overflow: hidden;
opacity: 0.6;
padding: 6px 0;
}
.server-tab .alt-icon {
font-family: Verdana, sans-serif;
font-weight: 600;
font-size: 22px;
border: 2px solid rgba(34, 44, 49, 1);
margin-left: 17%;
width: 35px;
border-radius: 4px;
width: 100%;
height: 35px;
position: relative;
margin: 5px 0 2px 0;
z-index: 11;
line-height: 31px;
color: #eee;
text-align: center;
overflow: hidden;
opacity: 0.6;
padding: 2px 0;
}
.tab .server-tab:hover {
opacity: 0.8;
}
.tab.active .server-tab {
opacity: 1;
background-color: rgba(100, 132, 120, 1);
opacity: 0.8;
}
.tab.functional-tab {
height: 46px;
padding: 0;
height: 46px;
padding: 0;
}
.tab.functional-tab.active .server-tab {
padding: 2px 0;
height: 40px;
background-color: rgba(255, 255, 255, 0.25);
padding: 2px 0;
height: 40px;
background-color: rgba(255, 255, 255, 0.25);
}
.tab.functional-tab .server-tab i {
font-size: 28px;
line-height: 36px;
font-size: 28px;
line-height: 36px;
}
.tab.active .server-tab {
opacity: 1;
background-color: #648478;
}
.tab .server-tab-badge.active {
border-radius: 9px;
min-width: 11px;
padding: 0 3px;
height: 17px;
background-color: rgba(244, 67, 54, 1);
font-size: 10px;
font-family: sans-serif;
position: absolute;
z-index: 15;
top: 6px;
float: right;
color: rgba(255, 255, 255, 1);
text-align: center;
line-height: 17px;
display: block;
right: 0;
border-radius: 9px;
min-width: 11px;
padding: 0 3px;
height: 17px;
background-color: #f44336;
font-size: 10px;
font-family: sans-serif;
position: absolute;
right: 5px;
z-index: 15;
top: 6px;
float: right;
color: #fff;
text-align: center;
line-height: 17px;
display: block;
right: 0;
}
.tab .server-tab-badge {
display: none;
display: none;
}
.tab .server-tab-badge.close-button {
width: 16px;
padding: 0;
width: 16px;
padding: 0;
}
.tab .server-tab-badge.close-button i {
font-size: 13px;
line-height: 17px;
font-size: 13px;
line-height: 17px;
}
#add-tab {
display: flex;
align-items: center;
flex-direction: column;
display: flex;
align-items: center;
flex-direction: column;
}
.tab .server-tab-shortcut {
color: rgba(100, 132, 120, 1);
font-size: 12px;
text-align: center;
font-family: sans-serif;
margin-bottom: 5px;
color: #648478;
font-size: 12px;
text-align: center;
font-family: sans-serif;
margin-bottom: 5px;
}
.refresh {
animation: rotate-loader 1s linear infinite;
}
@keyframes rotate-loader {
from {
transform: rotate(0);
}
to {
transform: rotate(-360deg);
}
}
/*******************
* Webview Area *
*******************/
* Webview Area *
*******************/
#webviews-container {
display: flex;
height: 100%;
width: 100%;
}
/* Pseudo element for loading indicator */
#webviews-container::before {
content: "";
position: absolute;
z-index: 1;
background: rgba(255, 255, 255, 1) url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
width: 100%;
height: 100%;
}
/* When the active webview is loaded */
#webviews-container.loaded::before {
opacity: 0;
z-index: -1;
visibility: hidden;
display: flex;
height: 100%;
width: 100%;
}
webview {
/* transition: opacity 0.3s ease-in; */
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
opacity: 1;
transition: opacity 0.3s ease-in;
flex-grow: 1;
}
webview.onload {
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
}
webview.active {
opacity: 1;
z-index: 1;
visibility: visible;
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
}
webview.disabled {
opacity: 0;
flex: 0 1;
height: 0;
width: 0;
opacity: 0;
transition: opacity 0.3s ease-out;
}
webview.focus {
outline: 0 solid transparent;
webview:focus {
outline: 0px solid transparent;
}
/* Tooltip styling */
#loading-tooltip,
#dnd-tooltip,
#back-tooltip,
#reload-tooltip,
#setting-tooltip {
font-family: sans-serif;
background: rgba(34, 44, 49, 1);
margin-left: 48px;
padding: 6px 8px;
position: absolute;
margin-top: 0;
z-index: 1000;
color: rgba(255, 255, 255, 1);
border-radius: 4px;
text-align: center;
width: 55px;
font-size: 14px;
font-family: sans-serif;
background: #222c31;
margin-left: 68px;
padding: 6px 8px;
position: absolute;
margin-top: 0px;
z-index: 1000;
color: white;
border-radius: 4px;
text-align: center;
width: 55px;
font-size: 14px;
}
#loading-tooltip::after,
#dnd-tooltip::after,
#back-tooltip::after,
#reload-tooltip::after,
#setting-tooltip::after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1);
position: absolute;
top: 7px;
right: 68px;
}
#add-server-tooltip,
.server-tooltip {
font-family: "arial", sans-serif;
background: rgba(34, 44, 49, 1);
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 11px;
z-index: 5000 !important;
color: rgba(255, 255, 255, 1);
border-radius: 4px;
text-align: center;
width: max-content;
font-size: 14px;
}
#add-server-tooltip::after,
.server-tooltip::after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1);
position: absolute;
top: 10px;
left: -5px;
#reload-tooltip:after,
#setting-tooltip:after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid #222c31;
position: absolute;
top: 7px;
right: 68px;
}
#collapse-button {
bottom: 30px;
left: 20px;
position: absolute;
width: 24px;
height: 24px;
background: rgba(34, 44, 49, 1);
border-radius: 20px;
cursor: pointer;
box-shadow: rgba(153, 153, 153, 1) 1px 1px;
bottom: 30px;
left: 20px;
position: absolute;
width: 24px;
height: 24px;
background: #222c31;
border-radius: 20px;
cursor: pointer;
box-shadow: #999 1px 1px;
}
#collapse-button i {
color: rgba(239, 239, 239, 1);
color: #efefef;
}
#main-container {
display: flex;
height: 100%;
width: 100%;
position: relative;
flex-grow: 1;
flex-basis: 0;
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.hidden {
display: none !important;
display: none !important;
}
/* Full screen Popup container */
.popup .popuptext {
visibility: hidden;
background-color: rgba(85, 85, 85, 1);
color: rgba(255, 255, 255, 1);
text-align: center;
border-radius: 6px;
padding: 9px 0;
position: absolute;
z-index: 1000;
font-family: arial, sans-serif;
width: 240px;
top: 15px;
height: 20px;
left: 43%;
visibility: hidden;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 9px 0;
position: absolute;
z-index: 1000;
font-family: arial;
width: 240px;
top: 15px;
height: 20px;
left: 43%;
}
.popup .show {
visibility: visible;
animation: cssAnimation 0s ease-in 5s forwards;
animation-fill-mode: forwards;
visibility: visible;
animation: cssAnimation 0s ease-in 5s forwards;
animation-fill-mode: forwards;
}
@keyframes cssAnimation {
from {
opacity: 0;
}
to {
width: 0;
height: 0;
overflow: hidden;
opacity: 1;
}
}
send-feedback {
width: 60%;
height: 85%;
}
#feedback-modal {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(68, 67, 67, 0.81);
align-items: center;
justify-content: center;
z-index: 2;
transition: all 1s ease-out;
}
#feedback-modal.show {
display: flex;
from {
opacity: 0;
}
to {
width: 0;
height: 0;
overflow: hidden;
opacity: 1;
}
}

View File

@@ -1,59 +1,42 @@
html,
body {
margin: 0;
cursor: default;
font-size: 14px;
color: rgba(51, 51, 51, 1);
background: rgba(255, 255, 255, 1);
user-select: none;
html, body {
margin: 0;
cursor: default;
font-size: 14px;
color: #333;
background: #fff;
user-select:none;
}
#content {
display: flex;
flex-direction: column;
font-family: "Trebuchet MS", Helvetica, sans-serif;
margin: 100px 200px;
text-align: center;
display: flex;
flex-direction: column;
font-family: "Trebuchet MS", Helvetica, sans-serif;
margin: 100px 200px;
text-align: center;
}
#title {
text-align: left;
font-size: 24px;
font-weight: bold;
margin: 20px 0;
}
#subtitle {
font-size: 20px;
text-align: left;
margin: 12px 0;
font-size: 24px;
font-weight: bold;
margin: 20px 0;
}
#description {
text-align: left;
font-size: 16px;
list-style-position: inside;
font-size: 16px;
}
#reconnect {
float: left;
font-size: 16px;
background: #009688;
color: #fff;
width: 84px;
height: 32px;
border-radius: 5px;
line-height: 32px;
margin: 20px auto 0;
cursor: pointer;
}
#settings {
margin-left: 116px;
}
.button {
font-size: 16px;
background: rgba(0, 150, 136, 1);
color: rgba(255, 255, 255, 1);
width: 96px;
height: 32px;
border-radius: 5px;
line-height: 32px;
cursor: pointer;
}
.button:hover {
opacity: 0.8;
}
#reconnect:hover {
opacity: 0.8;
}

View File

@@ -1,74 +1,65 @@
html,
body {
height: 100%;
margin: 0;
cursor: default;
user-select: none;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
background: rgba(239, 239, 239, 1);
letter-spacing: -0.08px;
line-height: 18px;
color: rgba(139, 142, 143, 1);
height: 100%;
margin: 0;
cursor: default;
user-select: none;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
color: #333;
background: #efefef;
}
kbd {
display: inline-block;
border: 1px solid rgba(204, 204, 204, 1);
border-radius: 4px;
font-size: 15px;
font-family: Courier New, Courier, monospace;
font-weight: bold;
white-space: nowrap;
background-color: rgba(247, 247, 247, 1);
color: rgba(51, 51, 51, 1);
margin: 0 0.1em;
padding: 0.3em 0.8em;
text-shadow: 0 1px 0 rgba(255, 255, 255, 1);
line-height: 1.4;
padding: 0.1em 0.6em;
border: 1px solid #ccc;
font-size: 12px;
font-family: Arial,Helvetica,sans-serif;
background-color: #f7f7f7;
color: #333;
-moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
-webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
display: inline-block;
margin: 0 0.1em;
text-shadow: 0 1px 0 #fff;
line-height: 1.4;
white-space: nowrap;
}
table,
th,
td {
border-collapse: collapse;
color: rgba(56, 52, 48, 1);
table, th, td {
border: 1px solid #ddd;
border-collapse: collapse;
}
table {
width: 100%;
margin-top: 18px;
margin-bottom: 18px;
}
table { width: 85%; }
td {
padding: 5px;
}
table tr:nth-child(even) { background-color: #f2f2f2; }
td { padding: 5px; }
td:nth-child(odd) {
text-align: right;
width: 50%;
text-align: right;
width: 50%;
}
@font-face {
font-family: "Material Icons";
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local("Material Icons"), local("MaterialIcons-Regular"),
url(../fonts/MaterialIcons-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Montserrat";
src: url(../fonts/Montserrat-Regular.ttf) format("truetype");
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: "Material Icons";
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
@@ -78,692 +69,246 @@ td:nth-child(odd) {
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
}
#content {
display: flex;
height: 100%;
font-family: "Montserrat", sans-serif;
display: flex;
height: 100%;
font-family: sans-serif;
}
#sidebar {
width: 150px;
min-width: 100px;
padding: 30px 30px 30px 35px;
display: flex;
flex-direction: column;
font-size: 16px;
background: rgba(242, 242, 242, 1);
width: 80px;
padding: 30px;
display: flex;
flex-direction: column;
font-size: 16px;
}
#nav-container {
padding: 20px 0;
padding: 20px 0;
}
.nav {
padding: 7px 0;
color: rgba(153, 153, 153, 1);
cursor: pointer;
padding: 5px 0;
color: #999;
cursor: pointer;
}
.nav.active {
color: rgba(78, 191, 172, 1);
cursor: default;
position: relative;
color: #464e5a;
cursor: default;
position: relative;
}
.nav.active::before {
background: rgba(70, 78, 90, 1);
width: 3px;
height: 18px;
position: absolute;
left: -8px;
content: "";
}
/* We don't want to show this in nav item since we have the + button for adding an Organization */
#nav-AddServer {
display: none;
background: #464e5a;
width: 3px;
height: 16px;
position: absolute;
left: -8px;
content: '';
}
#settings-header {
font-size: 22px;
color: rgba(34, 44, 49, 1);
font-weight: bold;
text-transform: uppercase;
font-size: 22px;
color: #5c6166;
}
#settings-container {
width: 100%;
display: flex;
padding: 30px;
overflow-y: scroll;
width: 100%;
display: flex;
padding: 30px;
overflow-y: scroll;
}
#new-server-container {
padding-left: 42px;
padding-top: 25px;
margin-right: 16px;
opacity: 1;
transition: opacity 0.3s;
}
.title {
font-weight: 500;
color: rgba(34, 44, 49, 1);
}
.page-title {
color: rgba(34, 44, 49, 1);
font-size: 15px;
font-weight: bold;
padding: 4px 0 6px 0;
}
.add-server-info-row {
display: flex;
margin: 8px 0 0 0;
}
.add-server-info-right {
flex-grow: 1;
margin-top: 10px;
padding: 4px 0 6px 0;
font-weight: bold;
color: #1e1e1e;
}
.sub-title {
padding: 4px 0 6px 0;
font-weight: bold;
color: rgba(97, 97, 97, 1);
padding: 4px 0 6px 0;
font-weight: bold;
color: #616161;
}
img.server-info-icon {
width: 36px;
height: 36px;
padding: 4px 4px 4px 10px;
cursor: pointer;
vertical-align: middle;
width: 36px;
height: 36px;
padding: 4px;
}
.server-info-left {
margin: 4px 20px 0 0;
min-width: 40%;
margin: 10px 20px 0 0;
}
.server-info-right {
margin-top: 4px;
width: 55%;
display: flex;
align-items: baseline;
justify-content: space-between;
flex-grow: 1;
flex-grow: 1;
margin-right: 10px;
}
.server-info-row {
display: inline-block;
margin: 5px 0 0 0;
}
.server-info-left .server-info-row {
display: inline-flex;
align-items: inherit;
vertical-align: -2px;
position: relative;
}
.server-url {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
margin: 8px 0 0 0;
}
.server-info-alias {
font-weight: bold;
cursor: pointer;
}
.server-url-info {
margin-right: 10px;
font-weight: bold;
cursor: pointer;
}
.setting-input-key {
font-size: 14px;
height: 27px;
line-height: 27px;
font-weight: bold;
background: transparent;
flex-wrap: nowrap;
margin-right: 10px;
font-size: 14px;
height: 27px;
line-height: 27px;
font-weight: bold;
background: transparent;
flex-wrap: nowrap;
margin-right: 10px;
}
.setting-input-value {
flex-grow: 1;
font-size: 14px;
border-radius: 4px;
padding: 13px;
border: rgba(237, 237, 237, 1) 2px solid;
outline-width: 0;
background: transparent;
max-width: 450px;
flex-grow: 1;
font-size: 14px;
height: 24px;
border: none;
border-bottom: #ededed 1px solid;
outline-width: 0;
background: transparent;
max-width: 500px;
}
.setting-input-value:focus {
border: rgba(78, 191, 172, 1) 2px solid;
border-bottom: #7cb980 1px solid;
}
.invalid-input-value:focus {
border: rgba(239, 83, 80, 1) 2px solid;
}
.manual-proxy-block {
width: 96%;
.setting-block {
width: 100%;
}
.actions-container {
display: flex;
font-size: 14px;
color: rgba(35, 93, 58, 1);
vertical-align: middle;
margin: 10px 0;
flex-wrap: wrap;
display: flex;
font-size: 14px;
color: #235d3a;
vertical-align: middle;
margin: 10px 0;
flex-wrap: wrap;
}
.action {
display: flex;
align-items: center;
padding: 0 10px;
margin-right: 10px;
display: flex;
align-items: center;
padding: 0 10px;
border-radius: 2px;
margin-right: 10px;
}
.action i {
margin-right: 5px;
font-size: 18px;
line-height: 26px;
margin-right: 5px;
font-size: 18px;
line-height: 27px;
}
.settings-pane {
flex-grow: 1;
min-width: 550px;
flex-grow: 1;
}
.action:hover {
cursor: pointer;
}
.action.disabled {
color: rgba(153, 153, 153, 1);
cursor: pointer;
}
.action.disabled:hover {
cursor: default;
cursor: default;
}
.action.disabled {
color: #999;
}
.settings-card {
display: flex;
flex-wrap: wrap;
padding: 12px 30px;
margin: 10px 0 20px 0;
background: rgba(255, 255, 255, 1);
width: 80%;
transition: all 0.2s;
}
.settings-card:hover {
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 0 0 rgba(0, 0, 0, 0.12);
display: flex;
flex-wrap: wrap;
padding: 12px 30px;
margin: 10px 0 20px 0;
background: #fff;
border-radius: 2px;
width: 540px;
box-shadow: 1px 2px 4px #bcbcbc;
}
.hidden {
display: none;
margin: 5px !important;
display: none;
margin: 5px !important;
}
.red {
color: rgb(240, 148, 148);
background: rgba(255, 255, 255, 1);
border-radius: 4px;
display: inline-block;
border: 2px solid rgb(240, 148, 148);
padding: 10px;
width: 100px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease;
text-decoration: none;
text-align: center;
}
.red:hover {
background-color: rgb(240, 148, 148);
color: rgba(255, 255, 255, 1);
color: #ef5350;
background: #ffebee;
border: 1px solid #ef5350;
}
.green {
color: rgba(255, 255, 255, 1);
background: rgba(78, 191, 172, 1);
border-radius: 4px;
display: inline-block;
border: none;
padding: 10px;
width: 100px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease;
text-decoration: none;
text-align: center;
}
.green:hover {
background-color: rgba(60, 159, 141, 1);
color: rgba(255, 255, 255, 1);
}
.w-150 {
width: 150px;
}
.w-250 {
width: 250px;
color: #388E3C;
background: #E8F5E9;
border: 1px solid #388E3C;
}
.grey {
color: rgba(158, 158, 158, 1);
background: rgba(250, 250, 250, 1);
border: 1px solid rgba(158, 158, 158, 1);
color: #9E9E9E;
background: #FAFAFA;
border: 1px solid #9E9E9E;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 6px;
}
#note {
font-size: 10px;
display: flex;
justify-content: space-between;
width: 100%;
margin: 6px 0;
}
.code {
font-family: Courier New, Courier, monospace;
font-family: Courier New, Courier, monospace;
}
i.open-tab-button {
padding-left: 5px;
font-size: 19px;
cursor: pointer;
padding: 0 5px;
font-size: 18px;
cursor: pointer;
}
#css-delete-action {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
.reset-data-button {
display: inline-block;
width: 120px;
cursor: pointer;
font-size: 11px;
transition: background-color 0.2s ease;
text-decoration: none;
/*
.action class will add extra margin to right which
we don't want for a button; the extra margin is intended for radio buttons
*/
margin-right: 0;
}
.selected-css-path,
.download-folder-path {
background: rgba(238, 238, 238, 1);
padding: 5px 10px;
margin-right: 10px;
display: flex;
width: 90%;
justify-content: space-between;
.reset-data-button:hover {
background-color: #32a692;
color: #fff;
}
#remove-custom-css {
align-items: flex-end;
#open-shortcuts-url {
color: #08c;
cursor: pointer;
text-decoration: none;
}
#server-info-container {
/* min-height: calc(100% - 260px); */
}
#new-org-button {
margin: 30px 0 30px 0;
}
#create-organization-container {
font-size: 1.15em;
}
#create-organization-container i {
position: relative;
top: 3px;
}
#open-create-org-link {
cursor: pointer;
text-decoration: none;
}
.toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.toggle + label {
display: block;
position: relative;
cursor: pointer;
outline: none;
user-select: none;
}
.disallowed:hover {
background-color: rgba(241, 241, 241, 1);
cursor: not-allowed;
}
input.toggle-round + label {
padding: 2px;
width: 50px;
height: 25px;
background-color: rgba(221, 221, 221, 1);
border-radius: 25px;
}
input.toggle-round + label::before,
input.toggle-round + label::after {
display: block;
position: absolute;
top: 2px;
left: 2px;
bottom: 2px;
content: "";
}
input.toggle-round + label::before {
background-color: rgba(241, 241, 241, 1);
border-radius: 25px;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
input.toggle-round + label::after {
width: 25px;
height: 25px;
background-color: rgba(255, 255, 255, 1);
border-radius: 100%;
}
input.toggle-round:checked + label::before {
background-color: rgba(78, 191, 172, 1);
top: 0;
right: 0;
left: 0;
bottom: 0;
}
input.toggle-round:checked + label::after {
margin-left: 25px;
}
/* Add new server modal */
.add-server-modal {
display: block;
position: fixed;
z-index: 1;
padding-top: 15vh;
left: 0;
top: 0;
margin: auto;
width: 100%;
height: 100%;
/* background: rgba(61, 64, 67, 15); */
background: linear-gradient(35deg, rgba(0, 59, 82, 1), rgba(69, 181, 155, 1));
overflow: auto;
}
/* Modal Content */
.modal-container {
background-color: rgba(244, 247, 248, 1);
margin: auto;
padding: 57px;
border: rgba(218, 225, 227, 1) 1px solid;
width: 550px;
height: 370px;
border-radius: 4px;
}
.add-server-modal .page-title {
text-align: center;
font-size: 1.6rem;
}
.divider {
margin-bottom: 30px;
margin-top: 30px;
color: rgba(125, 135, 138, 1);
}
.divider hr {
margin-left: 8px;
margin-right: 8px;
width: 44%;
}
.left {
float: left;
}
.right {
float: right;
}
.server-center {
width: 100%;
text-align: center;
align-items: center;
padding-top: 13px;
margin-left: -5px;
}
.server-center button {
font-weight: bold;
font-size: 1.1rem;
margin: auto;
align-items: center;
text-align: center;
color: rgba(255, 255, 255, 1);
background: rgba(78, 191, 172, 1);
border-color: none;
border: none;
width: 98%;
height: 46px;
border-radius: 4px;
cursor: pointer;
}
.server-center button:hover {
background: rgba(50, 149, 136, 1);
}
.server-center button:focus {
background: rgba(50, 149, 136, 1);
}
.certificates-card {
width: 80%;
}
.certificate-input {
width: 100%;
margin-top: 10px;
display: inline-flex;
}
.certificate-input div {
align-self: center;
}
.certificate-input .setting-input-value {
margin-left: 10px;
max-width: 100%;
}
.tip {
background: none;
padding: 0;
}
.tip:hover {
box-shadow: none;
}
.md-14 {
font-size: 14px;
vertical-align: middle;
padding-right: 6px;
}
#open-hotkeys-link {
text-decoration: underline;
cursor: pointer;
}
.server-network-option {
font-weight: bold;
font-size: 1.1rem;
margin-top: 10px;
padding-top: 15px;
align-items: center;
text-align: center;
color: rgb(78, 191, 172);
width: 98%;
height: 46px;
cursor: pointer;
}
i.open-network-button {
font-size: 16px;
cursor: pointer;
padding-left: 5px;
position: absolute;
}
/* responsive grid */
@media (min-width: 500px) and (max-width: 720px) {
#new-server-container {
padding-left: 0;
width: 60vw;
padding-right: 4vh;
}
.page-title {
width: 60vw;
}
}
@media (max-width: 500px) {
#new-server-container {
padding-left: 0;
width: 54%;
}
.page-title {
width: 54%;
}
}
@media (max-width: 650px) {
.selected-css-path,
.download-folder-path {
margin-right: 15px;
}
#css-delete-action {
margin-left: 10px;
}
#css-delete-action span {
display: none;
}
}
@media (max-width: 720px) {
.modal-container {
width: 60vw;
padding: 40px;
min-width: 300px;
}
.server-center button {
margin-right: -12px;
width: 100%;
}
.divider {
margin-right: -8px;
}
.divider hr {
margin-left: 6px;
margin-right: 6px;
width: 43%;
}
}
@media (max-width: 600px) {
.divider {
margin-left: 4%;
}
.divider hr {
margin-left: 2px;
margin-right: 2px;
width: 40%;
}
}
@media (max-width: 900px) {
.settings-card {
flex-direction: column;
align-items: center;
}
.server-info-right {
flex-direction: column;
align-items: center;
}
.action {
margin-top: 10px;
}
}
.lang-menu {
font-size: 13px;
font-weight: bold;
background: rgba(78, 191, 172, 1);
width: 100px;
height: 38px;
color: rgba(255, 255, 255, 1);
border-color: rgba(0, 0, 0, 0);
}
.tagify__input {
min-width: 130px !important;
}
.tagify__input::before {
top: 0;
bottom: 0;
}
#open-shortcuts-url:hover {
color: #005580;;
text-decoration: underline;
}

View File

@@ -1,9 +1,9 @@
/* Override css rules */
.portico-wrap > .header {
display: none;
.portico-wrap>.header {
display: none;
}
.portico-container > .footer {
display: none;
}
.portico-container>.footer {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,82 +0,0 @@
import crypto from "crypto";
import {clipboard} from "electron";
// This helper is exposed via electron_bridge for use in the social
// login flow.
//
// It consists of a key and a promised token. The in-app page sends
// the key to the server, and opens the users browser to a page where
// they can log in and get a token encrypted to that key. When the
// user copies the encrypted token from their browser to the
// clipboard, we decrypt it and resolve the promise. The in-app page
// then uses the decrypted token to log the user in within the app.
//
// The encryption is authenticated (AES-GCM) to guarantee that we
// dont leak anything from the users clipboard other than the token
// intended for us.
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
version: number;
key: Uint8Array;
pasted: Promise<string>;
constructor(_: number) {
// At this time, the only version is 1.
this.version = 1;
this.key = crypto.randomBytes(32);
this.pasted = new Promise((resolve) => {
let interval: NodeJS.Timeout | null = null;
const startPolling = () => {
if (interval === null) {
interval = setInterval(poll, 1000);
}
poll();
};
const stopPolling = () => {
if (interval !== null) {
clearInterval(interval);
interval = null;
}
};
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 hasnt been copied yet; try
// again next time.
return;
}
window.removeEventListener("focus", startPolling);
window.removeEventListener("blur", stopPolling);
stopPolling();
resolve(plaintext);
};
window.addEventListener("focus", startPolling);
window.addEventListener("blur", stopPolling);
if (document.hasFocus()) {
startPolling();
}
});
}
}

View File

@@ -0,0 +1,11 @@
'use strict';
class BaseComponent {
generateNodeFromTemplate(template) {
const wrapper = document.createElement('div');
wrapper.innerHTML = template;
return wrapper.firstElementChild;
}
}
module.exports = BaseComponent;

View File

@@ -1,12 +0,0 @@
import type {HTML} from "../../../common/html";
export function generateNodeFromHTML(html: HTML): Element {
const wrapper = document.createElement("div");
wrapper.innerHTML = html.html;
if (wrapper.firstElementChild === null) {
throw new Error("No element found in HTML");
}
return wrapper.firstElementChild;
}

View File

@@ -1,143 +0,0 @@
import type {ContextMenuParams} from "electron";
import {remote} from "electron";
import * as t from "../../../common/translation-util";
const {clipboard, Menu} = remote;
export const contextMenu = (
webContents: Electron.WebContents,
event: Event,
props: ContextMenuParams,
) => {
const isText = props.selectionText !== "";
const isLink = props.linkURL !== "";
const linkURL = isLink ? new URL(props.linkURL) : undefined;
const makeSuggestion = (suggestion: string) => ({
label: suggestion,
visible: true,
async click() {
await webContents.insertText(suggestion);
},
});
let menuTemplate: Electron.MenuItemConstructorOptions[] = [
{
label: t.__("Add to Dictionary"),
visible: props.isEditable && isText && props.misspelledWord.length > 0,
click(_item) {
webContents.session.addWordToSpellCheckerDictionary(
props.misspelledWord,
);
},
},
{
type: "separator",
visible: props.isEditable && isText && props.misspelledWord.length > 0,
},
{
label: `${t.__("Look Up")} "${props.selectionText}"`,
visible: process.platform === "darwin" && isText,
click(_item) {
webContents.showDefinitionForSelection();
},
},
{
type: "separator",
visible: process.platform === "darwin" && isText,
},
{
label: t.__("Cut"),
visible: isText,
enabled: props.isEditable,
accelerator: "CommandOrControl+X",
click(_item) {
webContents.cut();
},
},
{
label: t.__("Copy"),
accelerator: "CommandOrControl+C",
enabled: props.editFlags.canCopy,
click(_item) {
webContents.copy();
},
},
{
label: t.__("Paste"), // Bug: Paste replaces text
accelerator: "CommandOrControl+V",
enabled: props.isEditable,
click() {
webContents.paste();
},
},
{
type: "separator",
},
{
label:
linkURL?.protocol === "mailto:"
? t.__("Copy Email Address")
: t.__("Copy Link"),
visible: isLink,
click(_item) {
clipboard.write({
bookmark: props.linkText,
text:
linkURL?.protocol === "mailto:" ? linkURL.pathname : props.linkURL,
});
},
},
{
label: t.__("Copy Image"),
visible: props.mediaType === "image",
click(_item) {
webContents.copyImageAt(props.x, props.y);
},
},
{
label: t.__("Copy Image URL"),
visible: props.mediaType === "image",
click(_item) {
clipboard.write({
bookmark: props.srcURL,
text: props.srcURL,
});
},
},
{
type: "separator",
visible: isLink || props.mediaType === "image",
},
{
label: t.__("Services"),
visible: process.platform === "darwin",
role: "services",
},
];
if (props.misspelledWord) {
if (props.dictionarySuggestions.length > 0) {
const suggestions: Electron.MenuItemConstructorOptions[] = props.dictionarySuggestions.map(
(suggestion: string) => makeSuggestion(suggestion),
);
menuTemplate = [...suggestions, ...menuTemplate];
} else {
menuTemplate.unshift({
label: t.__("No Suggestion Found"),
enabled: false,
});
}
}
// 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.
// https://github.com/electron/electron/issues/5869
// https://github.com/electron/electron/issues/6906
const filteredMenuTemplate = menuTemplate.filter(
(menuItem) => menuItem.visible ?? true,
);
const menu = Menu.buildFromTemplate(filteredMenuTemplate);
menu.popup();
};

View File

@@ -0,0 +1,43 @@
'use strict';
const Tab = require(__dirname + '/../components/tab.js');
class FunctionalTab extends Tab {
template() {
return `<div class="tab functional-tab">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
</div>
</div>`;
}
init() {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
this.registerListeners();
}
registerListeners() {
super.registerListeners();
this.$el.addEventListener('mouseover', () => {
this.$closeButton.classList.add('active');
});
this.$el.addEventListener('mouseout', () => {
this.$closeButton.classList.remove('active');
});
this.$closeButton.addEventListener('click', e => {
this.props.onDestroy();
e.stopPropagation();
});
}
}
module.exports = FunctionalTab;

View File

@@ -1,52 +0,0 @@
import type {HTML} from "../../../common/html";
import {html} from "../../../common/html";
import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
export default class FunctionalTab extends Tab {
$el: Element;
$closeButton?: Element;
constructor(props: TabProps) {
super(props);
this.$el = generateNodeFromHTML(this.templateHTML());
if (this.props.name !== "Settings") {
this.props.$root.append(this.$el);
this.$closeButton = this.$el.querySelector(".server-tab-badge")!;
this.registerListeners();
}
}
templateHTML(): HTML {
return html`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
</div>
</div>
`;
}
registerListeners(): void {
super.registerListeners();
this.$el.addEventListener("mouseover", () => {
this.$closeButton?.classList.add("active");
});
this.$el.addEventListener("mouseout", () => {
this.$closeButton?.classList.remove("active");
});
this.$closeButton?.addEventListener("click", (event: Event) => {
this.props.onDestroy?.();
event.stopPropagation();
});
}
}

View File

@@ -1,72 +0,0 @@
import {remote} from "electron";
import * as ConfigUtil from "../../../common/config-util";
import {ipcRenderer} from "../typed-ipc-renderer";
import * as LinkUtil from "../utils/link-util";
import type WebView from "./webview";
const {shell, app} = remote;
const dingSound = new Audio("../resources/sounds/ding.ogg");
export default function handleExternalLink(
this: WebView,
event: Electron.NewWindowEvent,
): void {
event.preventDefault();
const url = new URL(event.url);
const downloadPath = ConfigUtil.getConfigItem(
"downloadsPath",
`${app.getPath("downloads")}`,
);
if (LinkUtil.isUploadsUrl(this.props.url, url)) {
ipcRenderer.send("downloadFile", url.href, downloadPath);
ipcRenderer.once(
"downloadFileCompleted",
async (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification("Download Complete", {
body: `Click to show ${fileName} in folder`,
silent: true, // We'll play our own sound - ding.ogg
});
downloadNotification.addEventListener("click", () => {
// Reveal file in download folder
shell.showItemInFolder(filePath);
});
ipcRenderer.removeAllListeners("downloadFileFailed");
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem("silent", false)) {
await dingSound.play();
}
},
);
ipcRenderer.once("downloadFileFailed", (_event: Event, state: string) => {
// Automatic download failed, so show save dialog prompt and download
// through webview
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
// prompts right after each other)
// Check that the download is not cancelled by user
if (state !== "cancelled") {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
// doesn't work
// eslint-disable-next-line no-new
new Notification("Download Complete", {
body: "Download failed",
});
} else {
this.$el!.downloadURL(url.href);
}
}
ipcRenderer.removeAllListeners("downloadFileCompleted");
});
} else {
(async () => LinkUtil.openBrowser(url))();
}
}

View File

@@ -0,0 +1,58 @@
'use strict';
const Tab = require(__dirname + '/../components/tab.js');
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const {ipcRenderer} = require('electron');
class ServerTab extends Tab {
template() {
return `<div class="tab">
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>`;
}
init() {
super.init();
this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0];
}
updateBadge(count) {
if (count > 0) {
const formattedCount = count > 999 ? '1K+' : count;
this.$badge.innerHTML = formattedCount;
this.$badge.classList.add('active');
} else {
this.$badge.classList.remove('active');
}
}
generateShortcutText() {
// Only provide shortcuts for server [0..10]
if (this.props.index >= 10) {
return '';
}
const shownIndex = this.props.index + 1;
let shortcutText = '';
if (SystemUtil.getOS() === 'Mac') {
shortcutText = `${shownIndex}`;
} else {
shortcutText = `Ctrl+${shownIndex}`;
}
ipcRenderer.send('register-server-tab-shortcut', shownIndex);
return shortcutText;
}
}
module.exports = ServerTab;

View File

@@ -1,66 +0,0 @@
import type {HTML} from "../../../common/html";
import {html} from "../../../common/html";
import {ipcRenderer} from "../typed-ipc-renderer";
import * as SystemUtil from "../utils/system-util";
import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
export default class ServerTab extends Tab {
$el: Element;
$badge: Element;
constructor(props: TabProps) {
super(props);
this.$el = generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.registerListeners();
this.$badge = this.$el.querySelector(".server-tab-badge")!;
}
templateHTML(): HTML {
return html`
<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">
${this.props.name}
</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src="${this.props.icon}" />
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>
`;
}
updateBadge(count: number): void {
if (count > 0) {
const formattedCount = count > 999 ? "1K+" : count.toString();
this.$badge.textContent = formattedCount;
this.$badge.classList.add("active");
} else {
this.$badge.classList.remove("active");
}
}
generateShortcutText(): string {
// Only provide shortcuts for server [0..9]
if (this.props.index >= 9) {
return "";
}
const shownIndex = this.props.index + 1;
let shortcutText = "";
shortcutText =
SystemUtil.getOS() === "Mac" ? `${shownIndex}` : `Ctrl+${shownIndex}`;
// Array index == Shown index - 1
ipcRenderer.send("switch-server-tab", shownIndex - 1);
return shortcutText;
}
}

View File

@@ -0,0 +1,46 @@
'use strict';
const BaseComponent = require(__dirname + '/../components/base.js');
class Tab extends BaseComponent {
constructor(props) {
super();
this.props = props;
this.webview = this.props.webview;
this.init();
}
init() {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.registerListeners();
}
registerListeners() {
this.$el.addEventListener('click', this.props.onClick);
}
isLoading() {
return this.webview.isLoading;
}
activate() {
this.$el.classList.add('active');
this.webview.load();
}
deactivate() {
this.$el.classList.remove('active');
this.webview.hide();
}
destroy() {
this.$el.parentNode.removeChild(this.$el);
this.webview.$el.parentNode.removeChild(this.webview.$el);
}
}
module.exports = Tab;

View File

@@ -1,58 +0,0 @@
import type WebView from "./webview";
export interface TabProps {
role: string;
icon?: string;
name: string;
$root: Element;
onClick: () => void;
index: number;
tabIndex: number;
onHover?: () => void;
onHoverOut?: () => void;
webview: WebView;
materialIcon?: string;
onDestroy?: () => void;
}
export default abstract class Tab {
props: TabProps;
webview: WebView;
abstract $el: Element;
constructor(props: TabProps) {
this.props = props;
this.webview = this.props.webview;
}
registerListeners(): void {
this.$el.addEventListener("click", this.props.onClick);
if (this.props.onHover !== undefined) {
this.$el.addEventListener("mouseover", this.props.onHover);
}
if (this.props.onHoverOut !== undefined) {
this.$el.addEventListener("mouseout", this.props.onHoverOut);
}
}
showNetworkError(): void {
this.webview.forceLoad();
}
activate(): void {
this.$el.classList.add("active");
this.webview.load();
}
deactivate(): void {
this.$el.classList.remove("active");
this.webview.hide();
}
destroy(): void {
this.$el.remove();
this.webview.$el!.remove();
}
}

View File

@@ -0,0 +1,183 @@
'use strict';
const path = require('path');
const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const LinkUtil = require(__dirname + '/../utils/link-util.js');
const {shell} = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js');
class WebView extends BaseComponent {
constructor(props) {
super();
this.props = props;
this.zoomFactor = 1.0;
this.loading = false;
this.badgeCount = 0;
}
template() {
return `<webview
class="disabled"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
webpreferences="allowRunningInsecureContent, javascript=yes">
</webview>`;
}
init() {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.registerListeners();
}
registerListeners() {
this.$el.addEventListener('new-window', event => {
const {url} = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
event.preventDefault();
this.$el.loadURL(url);
} else {
event.preventDefault();
shell.openExternal(url);
}
});
this.$el.addEventListener('page-title-updated', event => {
const {title} = event;
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
});
this.$el.addEventListener('dom-ready', () => {
if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
this.show();
});
this.$el.addEventListener('did-fail-load', event => {
const {errorDescription} = event;
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
if (hasConnectivityErr) {
console.error('error', errorDescription);
this.props.onNetworkError();
}
});
this.$el.addEventListener('did-start-loading', () => {
let userAgent = SystemUtil.getUserAgent();
if (!userAgent) {
SystemUtil.setUserAgent(this.$el.getUserAgent());
userAgent = SystemUtil.getUserAgent();
}
this.$el.setUserAgent(userAgent);
});
}
getBadgeCount(title) {
const messageCountInTitle = (/\(([0-9]+)\)/).exec(title);
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
}
show() {
// Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) {
return;
}
this.$el.classList.remove('disabled');
setTimeout(() => {
if (this.props.role === 'server') {
this.$el.classList.remove('onload');
}
}, 1000);
this.focus();
this.loading = false;
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
}
focus() {
// focus Webview and it's contents when Window regain focus.
const webContents = this.$el.getWebContents();
if (webContents && !webContents.isFocused()) {
this.$el.focus();
webContents.focus();
}
}
hide() {
this.$el.classList.add('disabled');
}
load() {
if (this.$el) {
this.show();
} else {
this.init();
}
}
zoomIn() {
this.zoomFactor += 0.1;
this.$el.setZoomFactor(this.zoomFactor);
}
zoomOut() {
this.zoomFactor -= 0.1;
this.$el.setZoomFactor(this.zoomFactor);
}
zoomActualSize() {
this.zoomFactor = 1.0;
this.$el.setZoomFactor(this.zoomFactor);
}
logOut() {
this.$el.executeJavaScript('logout()');
}
showShortcut() {
this.$el.executeJavaScript('shortcut()');
}
openDevTools() {
this.$el.openDevTools();
}
back() {
if (this.$el.canGoBack()) {
this.$el.goBack();
}
}
forward() {
if (this.$el.canGoForward()) {
this.$el.goForward();
}
}
reload() {
this.hide();
this.$el.reload();
}
send(...param) {
this.$el.send(...param);
}
}
module.exports = WebView;

View File

@@ -1,345 +0,0 @@
import {remote} from "electron";
import fs from "fs";
import path from "path";
import * as ConfigUtil from "../../../common/config-util";
import {HTML, html} from "../../../common/html";
import type {RendererMessage} from "../../../common/typed-ipc";
import {ipcRenderer} from "../typed-ipc-renderer";
import * as SystemUtil from "../utils/system-util";
import {generateNodeFromHTML} from "./base";
import {contextMenu} from "./context-menu";
import handleExternalLink from "./handle-external-link";
const {app, dialog} = remote;
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
interface WebViewProps {
$root: Element;
index: number;
tabIndex: number;
url: string;
role: string;
name: string;
isActive: () => boolean;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
onTitleChange: () => void;
hasPermission?: (origin: string, permission: string) => boolean;
}
export default class WebView {
props: WebViewProps;
zoomFactor: number;
badgeCount: number;
loading: boolean;
customCSS: string | false | null;
$webviewsContainer: DOMTokenList;
$el?: Electron.WebviewTag;
domReady?: Promise<void>;
constructor(props: WebViewProps) {
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;
}
templateHTML(): HTML {
return html`
<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${new HTML({html: this.props.nodeIntegration ? "nodeIntegration" : ""})}
${new HTML({html: this.props.preload ? 'preload="js/preload.js"' : ""})}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
contextIsolation=${!this.props.nodeIntegration},
spellcheck=${Boolean(
ConfigUtil.getConfigItem("enableSpellchecker", true),
)},
worldSafeExecuteJavaScript=true
"
>
</webview>
`;
}
init(): void {
this.$el = generateNodeFromHTML(this.templateHTML()) as Electron.WebviewTag;
this.domReady = new Promise((resolve) => {
this.$el!.addEventListener(
"dom-ready",
() => {
resolve();
},
true,
);
});
this.props.$root.append(this.$el);
this.registerListeners();
}
registerListeners(): void {
this.$el!.addEventListener("new-window", (event) => {
handleExternalLink.call(this, event);
});
if (shouldSilentWebview) {
this.$el!.addEventListener("dom-ready", () => {
this.$el!.setAudioMuted(true);
});
}
this.$el!.addEventListener("page-title-updated", (event) => {
const {title} = event;
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
});
this.$el!.addEventListener("did-navigate-in-page", (event) => {
const isSettingPage = event.url.includes("renderer/preference.html");
if (isSettingPage) {
return;
}
this.canGoBackButton();
});
this.$el!.addEventListener("did-navigate", () => {
this.canGoBackButton();
});
this.$el!.addEventListener("page-favicon-updated", (event) => {
const {favicons} = event;
// 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();
}
}
});
this.$el!.addEventListener("dom-ready", () => {
const webContents = remote.webContents.fromId(
this.$el!.getWebContentsId(),
);
webContents.addListener("context-menu", (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});
if (this.props.role === "server") {
this.$el!.classList.add("onload");
}
this.loading = false;
this.props.switchLoading(false, this.props.url);
this.show();
// Refocus text boxes after reload
// Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
this.$el!.blur();
this.$el!.focus();
});
this.$el!.addEventListener("did-fail-load", (event) => {
const {errorDescription} = event;
const hasConnectivityError = SystemUtil.connectivityERR.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", () => {
const isSettingPage = this.props.url.includes("renderer/preference.html");
if (!isSettingPage) {
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;
}
async showNotificationSettings(): Promise<void> {
await 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
if (this.loading) {
this.$webviewsContainer.remove("loaded");
} else {
this.$webviewsContainer.add("loaded");
}
this.$el!.classList.remove("disabled");
this.$el!.classList.add("active");
setTimeout(() => {
if (this.props.role === "server") {
this.$el!.classList.remove("onload");
}
}, 1000);
this.focus();
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
(async () =>
this.$el!.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.$el!.insertCSS(
fs.readFileSync(path.resolve(__dirname, customCSS), "utf8"),
))();
}
}
focus(): void {
// Focus Webview and it's contents when Window regain focus.
const webContents = remote.webContents.fromId(this.$el!.getWebContentsId());
// HACK: webContents.isFocused() seems to be true even without the element
// being in focus. So, we check against `document.activeElement`.
if (webContents && this.$el !== document.activeElement) {
// HACK: Looks like blur needs to be called on the previously focused
// element to transfer focus correctly, in Electron v3.0.10
// See https://github.com/electron/electron/issues/15718
(document.activeElement as HTMLElement).blur();
this.$el!.focus();
webContents.focus();
}
}
hide(): void {
this.$el!.classList.add("disabled");
this.$el!.classList.remove("active");
}
load(): void {
if (this.$el) {
this.show();
} else {
this.init();
}
}
zoomIn(): void {
this.zoomFactor += 0.1;
this.$el!.setZoomFactor(this.zoomFactor);
}
zoomOut(): void {
this.zoomFactor -= 0.1;
this.$el!.setZoomFactor(this.zoomFactor);
}
zoomActualSize(): void {
this.zoomFactor = 1;
this.$el!.setZoomFactor(this.zoomFactor);
}
async logOut(): Promise<void> {
await this.send("logout");
}
async showKeyboardShortcuts(): Promise<void> {
await this.send("show-keyboard-shortcuts");
}
openDevTools(): void {
this.$el!.openDevTools();
}
back(): void {
if (this.$el!.canGoBack()) {
this.$el!.goBack();
this.focus();
}
}
canGoBackButton(): void {
const $backButton = document.querySelector(
"#actions-container #back-action",
)!;
if (this.$el!.canGoBack()) {
$backButton.classList.remove("disable");
} else {
$backButton.classList.add("disable");
}
}
forward(): void {
if (this.$el!.canGoForward()) {
this.$el!.goForward();
}
}
reload(): void {
this.hide();
// Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove("loaded");
this.loading = true;
this.props.switchLoading(true, this.props.url);
this.$el!.reload();
}
forceLoad(): void {
this.init();
}
async send<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): Promise<void> {
await this.domReady;
ipcRenderer.sendTo(this.$el!.getWebContentsId(), channel, ...args);
}
}

View File

@@ -1,102 +0,0 @@
import {remote} from "electron";
import {EventEmitter} from "events";
import {ClipboardDecrypterImpl} from "./clipboard-decrypter";
import type {NotificationData} from "./notification";
import {newNotification} from "./notification";
import {ipcRenderer} from "./typed-ipc-renderer";
type ListenerType = (...args: any[]) => void;
let notificationReplySupported = false;
// Indicates if the user is idle or not
let idle = false;
// Indicates the time at which user was last active
let lastActive = Date.now();
export const bridgeEvents = new EventEmitter();
const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...args),
on_event: (eventName: string, listener: ListenerType): void => {
bridgeEvents.on(eventName, listener);
},
new_notification: (
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
): NotificationData => newNotification(title, options, dispatch),
get_idle_on_system: (): boolean => idle,
get_last_active_on_system: (): number => lastActive,
get_send_notification_reply_message_supported: (): boolean =>
notificationReplySupported,
set_send_notification_reply_message_supported: (value: boolean): void => {
notificationReplySupported = value;
},
decrypt_clipboard: (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version),
};
bridgeEvents.on("total_unread_count", (unreadCount: unknown) => {
if (typeof unreadCount !== "number") {
throw new TypeError("Expected string for unreadCount");
}
ipcRenderer.send("unread-count", unreadCount);
});
bridgeEvents.on("realm_name", (realmName: unknown) => {
if (typeof realmName !== "string") {
throw new TypeError("Expected string for realmName");
}
const serverURL = location.origin;
ipcRenderer.send("realm-name-changed", serverURL, realmName);
});
bridgeEvents.on("realm_icon_url", (iconURL: unknown) => {
if (typeof iconURL !== "string") {
throw new TypeError("Expected string for iconURL");
}
const serverURL = location.origin;
ipcRenderer.send(
"realm-icon-changed",
serverURL,
iconURL.includes("http") ? iconURL : `${serverURL}${iconURL}`,
);
});
// Set user as active and update the time of last activity
ipcRenderer.on("set-active", () => {
if (!remote.app.isPackaged) {
console.log("active");
}
idle = false;
lastActive = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on("set-idle", () => {
if (!remote.app.isPackaged) {
console.log("idle");
}
idle = true;
});
// 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
// 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;

View File

@@ -1,55 +0,0 @@
import {remote} from "electron";
import fs from "fs";
import path from "path";
import SendFeedback from "@electron-elements/send-feedback";
const {app} = remote;
customElements.define("send-feedback", SendFeedback);
export const sendFeedback: SendFeedback = document.querySelector(
"send-feedback",
)!;
export const feedbackHolder = sendFeedback.parentElement!;
// Make the button color match zulip app's theme
sendFeedback.customStylesheet = "css/feedback.css";
// Customize the fields of custom elements
sendFeedback.title = "Report Issue";
sendFeedback.titleLabel = "Issue title:";
sendFeedback.titlePlaceholder = "Enter issue title";
sendFeedback.textareaLabel = "Describe the issue:";
sendFeedback.textareaPlaceholder =
"Succinctly describe your issue and steps to reproduce it...";
sendFeedback.buttonLabel = "Report Issue";
sendFeedback.loaderSuccessText = "";
sendFeedback.useReporter("emailReporter", {
email: "support@zulip.com",
});
feedbackHolder.addEventListener("click", (event: Event) => {
// Only remove the class if the grey out faded
// part is clicked and not the feedback element itself
if (event.target === event.currentTarget) {
feedbackHolder.classList.remove("show");
}
});
sendFeedback.addEventListener("feedback-submitted", () => {
setTimeout(() => {
feedbackHolder.classList.remove("show");
}, 1000);
});
sendFeedback.addEventListener("feedback-cancelled", () => {
feedbackHolder.classList.remove("show");
});
const dataDir = app.getPath("userData");
const logsDir = path.join(dataDir, "/Logs");
sendFeedback.logs.push(
...fs.readdirSync(logsDir).map((file) => path.join(logsDir, file)),
);

View File

@@ -1,112 +0,0 @@
"use strict";
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;
};
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);
},
};
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);
}
},
};
}
const NativeNotification = Notification;
class InjectedNotification extends EventTarget {
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)),
),
);
}
static get maxActions(): number {
return NativeNotification.maxActions;
}
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;
}
}
Object.defineProperties(InjectedNotification.prototype, {
onclick: attributeListener("click"),
onclose: attributeListener("close"),
onerror: attributeListener("error"),
onshow: attributeListener("show"),
});
window.Notification = InjectedNotification as any;
})();

371
app/renderer/js/main.js Normal file
View File

@@ -0,0 +1,371 @@
'use strict';
require(__dirname + '/js/tray.js');
const { ipcRenderer, remote } = require('electron');
const { session } = remote;
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
const WebView = require(__dirname + '/js/components/webview.js');
const ServerTab = require(__dirname + '/js/components/server-tab.js');
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
class ServerManagerView {
constructor() {
this.$addServerButton = document.getElementById('add-tab');
this.$tabsContainer = document.getElementById('tabs-container');
const $actionsContainer = document.getElementById('actions-container');
this.$reloadButton = $actionsContainer.querySelector('#reload-action');
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container');
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
this.$sidebar = document.getElementById('sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup');
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.activeTabIndex = -1;
this.tabs = [];
this.functionalTabs = {};
}
init() {
this.loadProxy().then(() => {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
});
}
loadProxy() {
return new Promise(resolve => {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve);
} else {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
}, resolve);
}
});
}
initSidebar() {
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
this.toggleSidebar(showSidebar);
}
initTabs() {
const servers = DomainUtil.getDomains();
if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) {
this.initServer(servers[i], i);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
}
this.activateTab(0);
} else {
this.openSettings('Servers');
}
ipcRenderer.send('local-shortcuts', true);
}
initServer(server, index) {
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
$root: this.$tabsContainer,
onClick: this.activateTab.bind(this, index),
index,
webview: new WebView({
$root: this.$webviewsContainer,
index,
url: server.url,
name: server.alias,
isActive: () => {
return index === this.activeTabIndex;
},
onNetworkError: this.openNetworkTroubleshooting.bind(this),
onTitleChange: this.updateBadge.bind(this),
nodeIntegration: false,
preload: true
})
}));
}
initActions() {
this.$reloadButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.reload();
});
this.$addServerButton.addEventListener('click', () => {
this.openSettings('Servers');
});
this.$settingsButton.addEventListener('click', () => {
this.openSettings('General');
});
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
}
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
});
SidebarButton.addEventListener('mouseout', () => {
SidebarTooltip.style.display = 'none';
});
}
openFunctionalTab(tabProps) {
if (this.functionalTabs[tabProps.name] !== undefined) {
this.activateTab(this.functionalTabs[tabProps.name]);
return;
}
this.functionalTabs[tabProps.name] = this.tabs.length;
this.tabs.push(new FunctionalTab({
role: 'function',
materialIcon: tabProps.materialIcon,
$root: this.$tabsContainer,
index: this.functionalTabs[tabProps.name],
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
webview: new WebView({
$root: this.$webviewsContainer,
index: this.functionalTabs[tabProps.name],
url: tabProps.url,
name: tabProps.name,
isActive: () => {
return this.functionalTabs[tabProps.name] === this.activeTabIndex;
},
onNetworkError: this.openNetworkTroubleshooting.bind(this),
onTitleChange: this.updateBadge.bind(this),
nodeIntegration: true,
preload: false
})
}));
this.activateTab(this.functionalTabs[tabProps.name]);
}
openSettings(nav = 'General') {
this.openFunctionalTab({
name: 'Settings',
materialIcon: 'settings',
url: `file://${__dirname}/preference.html#${nav}`
});
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
}
openAbout() {
this.openFunctionalTab({
name: 'About',
materialIcon: 'sentiment_very_satisfied',
url: `file://${__dirname}/about.html`
});
}
openNetworkTroubleshooting() {
this.openFunctionalTab({
name: 'Network Troubleshooting',
materialIcon: 'network_check',
url: `file://${__dirname}/network.html`
});
}
activateTab(index, hideOldTab = true) {
if (this.tabs[index].webview.loading) {
return;
}
if (this.activeTabIndex !== -1) {
if (this.activeTabIndex === index) {
return;
} else if (hideOldTab) {
this.tabs[this.activeTabIndex].deactivate();
}
}
this.activeTabIndex = index;
this.tabs[index].activate();
ipcRenderer.send('update-menu', {
tabs: this.tabs,
activeTabIndex: this.activeTabIndex
});
}
destroyTab(name, index) {
if (this.tabs[index].webview.loading) {
return;
}
this.tabs[index].destroy();
delete this.tabs[index];
delete this.functionalTabs[name];
// Issue #188: If the functional tab was not focused, do not activate another tab.
if (this.activeTabIndex === index) {
this.activateTab(0, false);
}
}
destroyView() {
// Clear global variables
this.activeTabIndex = -1;
this.tabs = [];
this.functionalTabs = {};
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
// Destroy shortcuts
ipcRenderer.send('local-shortcuts', false);
}
reloadView() {
this.destroyView();
this.initTabs();
}
updateBadge() {
let messageCountAll = 0;
for (let i = 0; i < this.tabs.length; i++) {
if (this.tabs[i] && this.tabs[i].updateBadge) {
const count = this.tabs[i].webview.badgeCount;
messageCountAll += count;
this.tabs[i].updateBadge(count);
}
}
ipcRenderer.send('update-badge', messageCountAll);
}
toggleSidebar(show) {
if (show) {
this.$sidebar.classList.remove('hidden');
} else {
this.$sidebar.classList.add('hidden');
}
}
registerIpcs() {
const webviewListeners = {
'webview-reload': 'reload',
back: 'back',
focus: 'focus',
forward: 'forward',
zoomIn: 'zoomIn',
zoomOut: 'zoomOut',
zoomActualSize: 'zoomActualSize',
'log-out': 'logOut',
shortcut: 'showShortcut',
'tab-devtools': 'openDevTools'
};
for (const key in webviewListeners) {
ipcRenderer.on(key, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) {
activeWebview[webviewListeners[key]]();
}
});
}
ipcRenderer.on('open-settings', (event, settingNav) => {
this.openSettings(settingNav);
});
ipcRenderer.on('open-about', this.openAbout.bind(this));
ipcRenderer.on('reload-viewer', this.reloadView.bind(this));
ipcRenderer.on('hard-reload', () => {
ipcRenderer.send('reload-full-app');
});
ipcRenderer.on('clear-app-data', () => {
ipcRenderer.send('clear-app-settings');
});
ipcRenderer.on('switch-server-tab', (event, index) => {
this.activateTab(index);
});
ipcRenderer.on('reload-proxy', (event, showAlert) => {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
}
});
});
ipcRenderer.on('toggle-sidebar', (event, show) => {
this.toggleSidebar(show);
});
ipcRenderer.on('enter-fullscreen', () => {
this.$fullscreenPopup.classList.add('show');
this.$fullscreenPopup.classList.remove('hidden');
});
ipcRenderer.on('leave-fullscreen', () => {
this.$fullscreenPopup.classList.remove('show');
});
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => {
// Create a canvas from unread messagecounts
function createOverlayIcon(messageCount) {
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.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
});
}
}
window.onload = () => {
const serverManagerView = new ServerManagerView();
serverManagerView.init();
window.addEventListener('online', () => {
serverManagerView.reloadView();
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
'use strict';
const { remote } = require('electron');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const app = remote.app;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId('org.zulip.zulip-electron');
const NativeNotification = window.Notification;
class baseNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = ConfigUtil.getConfigItem('silent') || false;
// calling super without passing arguments will fail to constuct Notification
// and will result in no notification. It's a hack (not an ideal way) for deleting the window notification
ConfigUtil.getConfigItem('showNotification') ? super(title, opts) : super(); // eslint-disable-line no-unused-expressions
}
}
window.Notification = baseNotification;

View File

@@ -1,30 +0,0 @@
import * as ConfigUtil from "../../../common/config-util";
import {ipcRenderer} from "../typed-ipc-renderer";
import {focusCurrentServer} from "./helpers";
const NativeNotification = window.Notification;
export default class BaseNotification extends NativeNotification {
constructor(title: string, options: NotificationOptions) {
options.silent = true;
super(title, options);
this.addEventListener("click", () => {
// Focus to the server who sent the
// notification if not focused already
focusCurrentServer();
ipcRenderer.send("focus-app");
});
}
static async requestPermission(): Promise<NotificationPermission> {
return this.permission;
}
// Override default Notification permission
static get permission(): NotificationPermission {
return ConfigUtil.getConfigItem("showNotification", true)
? "granted"
: "denied";
}
}

View File

@@ -1,20 +0,0 @@
import {remote} from "electron";
import {ipcRenderer} from "../typed-ipc-renderer";
// Do not change this
export const appId = "org.zulip.zulip-electron";
const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// This function will focus the server that sent
// the notification. Main function implemented in main.js
export function focusCurrentServer(): void {
ipcRenderer.sendTo(
currentWindow.webContents.id,
"focus-webview-with-id",
webContentsId,
);
}

View File

@@ -1,65 +0,0 @@
import {remote} from "electron";
import DefaultNotification from "./default-notification";
import {appId} from "./helpers";
const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
export interface NotificationData {
close: () => void;
title: string;
dir: NotificationDirection;
lang: string;
body: string;
tag: string;
image: string;
icon: string;
badge: string;
vibrate: readonly number[];
timestamp: number;
renotify: boolean;
silent: boolean;
requireInteraction: boolean;
data: unknown;
actions: readonly NotificationAction[];
}
export function newNotification(
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
): NotificationData {
const notification = new DefaultNotification(title, options);
for (const type of ["click", "close", "error", "show"]) {
notification.addEventListener(type, (ev: Event) => {
if (!dispatch(type, ev)) {
ev.preventDefault();
}
});
}
return {
close: () => {
notification.close();
},
title: notification.title,
dir: notification.dir,
lang: notification.lang,
body: notification.body,
tag: notification.tag,
image: notification.image,
icon: notification.icon,
badge: notification.badge,
vibrate: notification.vibrate,
timestamp: notification.timestamp,
renotify: notification.renotify,
silent: notification.silent,
requireInteraction: notification.requireInteraction,
data: notification.data,
actions: notification.actions,
};
}

View File

@@ -0,0 +1,20 @@
'use strict';
const {ipcRenderer} = require('electron');
class NetworkTroubleshootingView {
constructor() {
this.$reconnectButton = document.getElementById('reconnect');
}
init() {
this.$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
}
window.onload = () => {
const networkTroubleshootingView = new NetworkTroubleshootingView();
networkTroubleshootingView.init();
};

View File

@@ -1,13 +0,0 @@
import {ipcRenderer} from "../typed-ipc-renderer";
export function init(
$reconnectButton: Element,
$settingsButton: Element,
): void {
$reconnectButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "reload-viewer");
});
$settingsButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-settings");
});
}

View File

@@ -0,0 +1,63 @@
'use strict';
const electron = require('electron');
const { app } = require('electron');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
let instance = null;
class BadgeSettings {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount, mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow);
}
}
hideBadgeCount(mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount, mainWindow) {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else {
this.hideBadgeCount(mainWindow);
}
}
updateOverlayIcon(messageCount, mainWindow) {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
updateTaskbarIcon(data, text, mainWindow) {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}
}
module.exports = new BadgeSettings();

View File

@@ -0,0 +1,40 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
class BaseSection extends BaseComponent {
generateSettingOption(props) {
const {$element, value, clickHandler} = props;
$element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
$element.appendChild($optionControl);
$optionControl.addEventListener('click', clickHandler);
}
generateOptionTemplate(settingOption) {
if (settingOption) {
return `
<div class="action green">
<span>On</span>
</div>
`;
} else {
return `
<div class="action red">
<span>Off</span>
</div>
`;
}
}
reloadApp() {
ipcRenderer.send('forward-message', 'reload-viewer');
}
}
module.exports = BaseSection;

View File

@@ -1,83 +0,0 @@
import type {HTML} from "../../../../common/html";
import {html} from "../../../../common/html";
import {generateNodeFromHTML} from "../../components/base";
import {ipcRenderer} from "../../typed-ipc-renderer";
interface BaseSectionProps {
$element: HTMLElement;
disabled?: boolean;
value: boolean;
clickHandler: () => void;
}
export function generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props;
$element.textContent = "";
const $optionControl = generateNodeFromHTML(
generateOptionHTML(value, disabled),
);
$element.append($optionControl);
if (!disabled) {
$optionControl.addEventListener("click", clickHandler);
}
}
export function generateOptionHTML(
settingOption: boolean,
disabled?: boolean,
): HTML {
const labelHTML = disabled
? html`<label
class="disallowed"
title="Setting locked by system administrator."
></label>`
: html`<label></label>`;
if (settingOption) {
return html`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled />
${labelHTML}
</div>
</div>
`;
}
return html`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" />
${labelHTML}
</div>
</div>
`;
}
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
export function generateSelectHTML(
options: Record<string, string>,
className?: string,
idName?: string,
): HTML {
const optionsHTML = html``.join(
Object.keys(options).map(
(key) => html`
<option name="${key}" value="${key}">${options[key]}</option>
`,
),
);
return html`
<select class="${className}" id="${idName}">
${optionsHTML}
</select>
`;
}
export function reloadApp(): void {
ipcRenderer.send("forward-message", "reload-viewer");
}

View File

@@ -1,66 +0,0 @@
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 {reloadApp} from "./base-section";
import {initFindAccounts} from "./find-accounts";
import {initServerInfoForm} from "./server-info-form";
interface ConnectedOrgSectionProps {
$root: Element;
}
export function initConnectedOrgSection(props: ConnectedOrgSectionProps): void {
props.$root.textContent = "";
const servers = DomainUtil.getDomains();
props.$root.innerHTML = html`
<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.")}
</div>
<div id="server-info-container"></div>
<div id="new-org-button">
<button class="green sea w-250">
${t.__("Connect to another organization")}
</button>
</div>
<div class="page-title">${t.__("Find accounts by email")}</div>
<div id="find-accounts-container"></div>
</div>
`.html;
const $serverInfoContainer = document.querySelector(
"#server-info-container",
)!;
const $existingServers = document.querySelector("#existing-servers")!;
const $newOrgButton: HTMLButtonElement = document.querySelector(
"#new-org-button",
)!;
const $findAccountsContainer = document.querySelector(
"#find-accounts-container",
)!;
const noServerText = t.__("All the connected orgnizations will appear here");
// Show noServerText if no servers are there otherwise hide it
$existingServers.textContent = servers.length === 0 ? noServerText : "";
for (const [i, server] of servers.entries()) {
initServerInfoForm({
$root: $serverInfoContainer,
server,
index: i,
onChange: reloadApp,
});
}
$newOrgButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-org-tab");
});
initFindAccounts({
$root: $findAccountsContainer,
});
}

View File

@@ -1,67 +0,0 @@
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {generateNodeFromHTML} from "../../components/base";
import * as LinkUtil from "../../utils/link-util";
interface FindAccountsProps {
$root: Element;
}
async function findAccounts(url: string): Promise<void> {
if (!url) {
return;
}
if (!url.startsWith("http")) {
url = "https://" + url;
}
await LinkUtil.openBrowser(new URL("/accounts/find", url));
}
export function initFindAccounts(props: FindAccountsProps): void {
const $findAccounts = generateNodeFromHTML(html`
<div class="settings-card certificate-card">
<div class="certificate-input">
<div>${t.__("Organization URL")}</div>
<input class="setting-input-value" value="zulipchat.com" />
</div>
<div class="certificate-input">
<button class="green w-150" id="find-accounts-button">
${t.__("Find accounts")}
</button>
</div>
</div>
`);
props.$root.append($findAccounts);
const $findAccountsButton = $findAccounts.querySelector(
"#find-accounts-button",
)!;
const $serverUrlField: HTMLInputElement = $findAccounts.querySelector(
"input.setting-input-value",
)!;
$findAccountsButton.addEventListener("click", async () => {
await findAccounts($serverUrlField.value);
});
$serverUrlField.addEventListener("click", () => {
if ($serverUrlField.value === "zulipchat.com") {
$serverUrlField.setSelectionRange(0, 0);
}
});
$serverUrlField.addEventListener("keypress", async (event) => {
if (event.key === "Enter") {
await findAccounts($serverUrlField.value);
}
});
$serverUrlField.addEventListener("input", () => {
if ($serverUrlField.value) {
$serverUrlField.classList.remove("invalid-input-value");
} else {
$serverUrlField.classList.add("invalid-input-value");
}
});
}

View File

@@ -0,0 +1,217 @@
'use strict';
const path = require('path');
const { ipcRenderer } = require('electron');
const { app, dialog } = require('electron').remote;
const fs = require('fs-extra');
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class GeneralSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane">
<div class="title">Appearance</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="tray-option">
<div class="setting-description">Show app icon in system tray</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">CmdOrCtrl+S</span>)</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">Show app unread badge</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Desktop Notification</div>
<div class="settings-card">
<div class="setting-row" id="show-notification-option">
<div class="setting-description">Show Desktop Notifications</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="silent-option">
<div class="setting-description">Mute all sounds from Zulip</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">App Updates</div>
<div class="settings-card">
<div class="setting-row" id="betaupdate-option">
<div class="setting-description">Get beta updates</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="autoupdate-option">
<div class="setting-description">Automatically install new updates</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Functionality</div>
<div class="settings-card">
<div class="setting-row" id="startAtLogin-option">
<div class="setting-description">Start app at login</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Reset Application Data</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences
</div>
<button class="reset-data-button green">Reset App Data</button>
</div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.updateTrayOption();
this.updateBadgeOption();
this.updateUpdateOption();
this.updateAutoUpdateOption();
this.updateSilentOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
this.showDesktopNotification();
}
updateTrayOption() {
this.generateSettingOption({
$element: document.querySelector('#tray-option .setting-control'),
value: ConfigUtil.getConfigItem('trayIcon', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('trayIcon');
ConfigUtil.setConfigItem('trayIcon', newValue);
ipcRenderer.send('forward-message', 'toggletray');
this.updateTrayOption();
}
});
}
updateBadgeOption() {
this.generateSettingOption({
$element: document.querySelector('#badge-option .setting-control'),
value: ConfigUtil.getConfigItem('badgeOption', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('badgeOption');
ConfigUtil.setConfigItem('badgeOption', newValue);
ipcRenderer.send('toggle-badge-option', newValue);
this.updateBadgeOption();
}
});
}
updateUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('betaUpdate', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.updateUpdateOption();
}
});
}
updateAutoUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
ConfigUtil.setConfigItem('autoUpdate', newValue);
this.updateAutoUpdateOption();
}
});
}
updateSilentOption() {
this.generateSettingOption({
$element: document.querySelector('#silent-option .setting-control'),
value: ConfigUtil.getConfigItem('silent', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
}
});
}
showDesktopNotification() {
this.generateSettingOption({
$element: document.querySelector('#show-notification-option .setting-control'),
value: ConfigUtil.getConfigItem('showNotification', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showNotification', true);
ConfigUtil.setConfigItem('showNotification', newValue);
this.showDesktopNotification();
}
});
}
updateSidebarOption() {
this.generateSettingOption({
$element: document.querySelector('#sidebar-option .setting-control'),
value: ConfigUtil.getConfigItem('showSidebar', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
ConfigUtil.setConfigItem('showSidebar', newValue);
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
this.updateSidebarOption();
}
});
}
updateStartAtLoginOption() {
this.generateSettingOption({
$element: document.querySelector('#startAtLogin-option .setting-control'),
value: ConfigUtil.getConfigItem('startAtLogin', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('startAtLogin');
ConfigUtil.setConfigItem('startAtLogin', newValue);
ipcRenderer.send('toggleAutoLauncher', newValue);
this.updateStartAtLoginOption();
}
});
}
clearAppDataDialog() {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName());
dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure',
detail: clearAppDataMessage
}, response => {
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
});
}
updateResetDataOption() {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
resetDataButton.addEventListener('click', () => {
this.clearAppDataDialog();
});
}
}
module.exports = GeneralSection;

View File

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

View File

@@ -0,0 +1,67 @@
'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js');
class PreferenceNav extends BaseComponent {
constructor(props) {
super();
this.props = props;
this.navItems = ['General', 'Network', 'Servers', 'Shortcuts'];
this.init();
}
template() {
let navItemsTemplate = '';
for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`;
}
return `
<div>
<div id="settings-header">Settings</div>
<div id="nav-container">${navItemsTemplate}</div>
</div>
`;
}
init() {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.registerListeners();
}
registerListeners() {
for (const navItem of this.navItems) {
const $item = document.getElementById(`nav-${navItem}`);
$item.addEventListener('click', () => {
this.props.onItemSelected(navItem);
});
}
}
select(navItemToSelect) {
for (const navItem of this.navItems) {
if (navItem === navItemToSelect) {
this.activate(navItem);
} else {
this.deactivate(navItem);
}
}
}
activate(navItem) {
const $item = document.getElementById(`nav-${navItem}`);
$item.classList.add('active');
}
deactivate(navItem) {
const $item = document.getElementById(`nav-${navItem}`);
$item.classList.remove('active');
}
}
module.exports = PreferenceNav;

View File

@@ -1,76 +0,0 @@
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";
interface PreferenceNavProps {
$root: Element;
onItemSelected: (navItem: NavItem) => void;
}
export default class PreferenceNav {
props: PreferenceNavProps;
navItems: NavItem[];
$el: Element;
constructor(props: PreferenceNavProps) {
this.props = props;
this.navItems = [
"General",
"Network",
"AddServer",
"Organizations",
"Shortcuts",
];
this.$el = generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.registerListeners();
}
templateHTML(): HTML {
const navItemsHTML = html``.join(
this.navItems.map(
(navItem) => html`
<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>
`,
),
);
return html`
<div>
<div id="settings-header">${t.__("Settings")}</div>
<div id="nav-container">${navItemsHTML}</div>
</div>
`;
}
registerListeners(): void {
for (const navItem of this.navItems) {
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`)!;
$item.addEventListener("click", () => {
this.props.onItemSelected(navItem);
});
}
}
select(navItemToSelect: NavItem): void {
for (const navItem of this.navItems) {
if (navItem === navItemToSelect) {
this.activate(navItem);
} else {
this.deactivate(navItem);
}
}
}
activate(navItem: NavItem): void {
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`)!;
$item.classList.add("active");
}
deactivate(navItem: NavItem): void {
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`)!;
$item.classList.remove("active");
}
}

View File

@@ -0,0 +1,102 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class NetworkSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane">
<div class="title">Proxy</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-proxy-option">
<div class="setting-description">Connect servers through a proxy</div>
<div class="setting-control"></div>
</div>
<div class="setting-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC script</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">Proxy rules</span>
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">Proxy bypass rules</span>
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<div class="setting-row">
<div class="action green" id="proxy-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
</div>
</div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.getElementById('proxy-save-action');
this.$settingBlock = this.props.$root.querySelector('.setting-block');
this.initProxyOption();
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
this.$proxySaveAction.addEventListener('click', () => {
ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
ipcRenderer.send('forward-message', 'reload-proxy', true);
});
}
initProxyOption() {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
this.toggleProxySettings(proxyEnabled);
this.updateProxyOption();
}
toggleProxySettings(option) {
if (option) {
this.$settingBlock.classList.remove('hidden');
} else {
this.$settingBlock.classList.add('hidden');
}
}
updateProxyOption() {
this.generateSettingOption({
$element: document.querySelector('#use-proxy-option .setting-control'),
value: ConfigUtil.getConfigItem('useProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useProxy');
ConfigUtil.setConfigItem('useProxy', newValue);
this.toggleProxySettings(newValue);
if (newValue === false) {
// Reload proxy if the proxy is turned off
ipcRenderer.send('forward-message', 'reload-proxy', false);
}
this.updateProxyOption();
}
});
}
}
module.exports = NetworkSection;

View File

@@ -1,144 +0,0 @@
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 {generateSettingOption} from "./base-section";
interface NetworkSectionProps {
$root: Element;
}
export function initNetworkSection(props: NetworkSectionProps): void {
props.$root.innerHTML = html`
<div class="settings-pane">
<div class="title">${t.__("Proxy")}</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-system-settings">
<div class="setting-description">
${t.__("Use system proxy settings (requires restart)")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="use-manual-settings">
<div class="setting-description">
${t.__("Manual proxy configuration")}
</div>
<div class="setting-control"></div>
</div>
<div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC ${t.__("script")}</span>
<input
class="setting-input-value"
placeholder="e.g. foobar.com/pacfile.js"
/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">${t.__("Proxy rules")}</span>
<input
class="setting-input-value"
placeholder="e.g. http=foopy:80;ftp=foopy2"
/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">${t.__("Proxy bypass rules")}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com" />
</div>
<div class="setting-row">
<div class="action green" id="proxy-save-action">
<span>${t.__("Save")}</span>
</div>
</div>
</div>
</div>
</div>
`.html;
const $proxyPAC: HTMLInputElement = document.querySelector(
"#proxy-pac-option .setting-input-value",
)!;
const $proxyRules: HTMLInputElement = document.querySelector(
"#proxy-rules-option .setting-input-value",
)!;
const $proxyBypass: HTMLInputElement = document.querySelector(
"#proxy-bypass-option .setting-input-value",
)!;
const $proxySaveAction = document.querySelector("#proxy-save-action")!;
const $manualProxyBlock = props.$root.querySelector(".manual-proxy-block")!;
toggleManualProxySettings(ConfigUtil.getConfigItem("useManualProxy", false));
updateProxyOption();
$proxyPAC.value = ConfigUtil.getConfigItem("proxyPAC", "");
$proxyRules.value = ConfigUtil.getConfigItem("proxyRules", "");
$proxyBypass.value = ConfigUtil.getConfigItem("proxyBypass", "");
$proxySaveAction.addEventListener("click", () => {
ConfigUtil.setConfigItem("proxyPAC", $proxyPAC.value);
ConfigUtil.setConfigItem("proxyRules", $proxyRules.value);
ConfigUtil.setConfigItem("proxyBypass", $proxyBypass.value);
ipcRenderer.send("forward-message", "reload-proxy", true);
});
function toggleManualProxySettings(option: boolean): void {
if (option) {
$manualProxyBlock.classList.remove("hidden");
} else {
$manualProxyBlock.classList.add("hidden");
}
}
function updateProxyOption(): void {
generateSettingOption({
$element: document.querySelector(
"#use-system-settings .setting-control",
)!,
value: ConfigUtil.getConfigItem("useSystemProxy", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("useSystemProxy", false);
const manualProxyValue = ConfigUtil.getConfigItem(
"useManualProxy",
false,
);
if (manualProxyValue && newValue) {
ConfigUtil.setConfigItem("useManualProxy", !manualProxyValue);
toggleManualProxySettings(!manualProxyValue);
}
if (!newValue) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem("proxyRules", "");
ipcRenderer.send("forward-message", "reload-proxy", false);
}
ConfigUtil.setConfigItem("useSystemProxy", newValue);
updateProxyOption();
},
});
generateSettingOption({
$element: document.querySelector(
"#use-manual-settings .setting-control",
)!,
value: ConfigUtil.getConfigItem("useManualProxy", false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem("useManualProxy", false);
const systemProxyValue = ConfigUtil.getConfigItem(
"useSystemProxy",
false,
);
toggleManualProxySettings(newValue);
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem("useSystemProxy", !systemProxyValue);
}
ConfigUtil.setConfigItem("proxyRules", "");
ConfigUtil.setConfigItem("useManualProxy", newValue);
// Reload app only when turning manual proxy off, hence !newValue
ipcRenderer.send("forward-message", "reload-proxy", !newValue);
updateProxyOption();
},
});
}
}

View File

@@ -0,0 +1,68 @@
'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
class NewServerForm extends BaseComponent {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-card">
<div class="server-info-right">
<div class="server-info-row">
<input class="setting-input-value" autofocus placeholder="Entert the URL of your Zulip organization..."/>
</div>
<div class="server-info-row">
<div class="action green server-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
</div>
</div>
</div>
`;
}
init() {
this.initForm();
this.initActions();
}
initForm() {
this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$saveServerButton = this.$newServerForm.getElementsByClassName('server-save-action')[0];
this.props.$root.innerHTML = '';
this.props.$root.appendChild(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
}
submitFormHandler() {
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
alert(errorMessage);
});
}
initActions() {
this.$saveServerButton.addEventListener('click', () => {
this.submitFormHandler();
});
this.$newServerUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode;
// Submit form when Enter key is pressed
if (EnterkeyCode === 13) {
this.submitFormHandler();
}
});
}
}
module.exports = NewServerForm;

View File

@@ -1,106 +0,0 @@
import {remote} from "electron";
import {html} from "../../../../common/html";
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 * as LinkUtil from "../../utils/link-util";
const {dialog} = remote;
interface NewServerFormProps {
$root: Element;
onChange: () => void;
}
export function initNewServerForm(props: NewServerFormProps): void {
const $newServerForm = generateNodeFromHTML(html`
<div class="server-input-container">
<div class="title">${t.__("Organization URL")}</div>
<div class="add-server-info-row">
<input
class="setting-input-value"
autofocus
placeholder="your-organization.zulipchat.com or zulip.your-organization.com"
/>
</div>
<div class="server-center">
<button id="connect">${t.__("Connect")}</button>
</div>
<div class="server-center">
<div class="divider">
<hr class="left" />
${t.__("OR")}
<hr class="right" />
</div>
</div>
<div class="server-center">
<button id="open-create-org-link">
${t.__("Create a new organization")}
</button>
</div>
<div class="server-center">
<div class="server-network-option">
<span id="open-network-settings"
>${t.__("Network and Proxy Settings")}</span
>
<i class="material-icons open-network-button">open_in_new</i>
</div>
</div>
</div>
`);
const $saveServerButton: HTMLButtonElement = $newServerForm.querySelector(
"#connect",
)!;
props.$root.textContent = "";
props.$root.append($newServerForm);
const $newServerUrl: HTMLInputElement = $newServerForm.querySelector(
"input.setting-input-value",
)!;
async function submitFormHandler(): Promise<void> {
$saveServerButton.textContent = "Connecting...";
let serverConf;
try {
serverConf = await DomainUtil.checkDomain($newServerUrl.value.trim());
} catch (error: unknown) {
$saveServerButton.textContent = "Connect";
await dialog.showMessageBox({
type: "error",
message:
error instanceof Error
? `${error.name}: ${error.message}`
: "Unknown error",
buttons: ["OK"],
});
return;
}
await DomainUtil.addDomain(serverConf);
props.onChange();
}
$saveServerButton.addEventListener("click", async () => {
await submitFormHandler();
});
$newServerUrl.addEventListener("keypress", async (event) => {
if (event.key === "Enter") {
await submitFormHandler();
}
});
// Open create new org link in default browser
const link = "https://zulip.com/new/";
const externalCreateNewOrgElement = document.querySelector(
"#open-create-org-link",
)!;
externalCreateNewOrgElement.addEventListener("click", async () => {
await LinkUtil.openBrowser(new URL(link));
});
const networkSettingsId = document.querySelector(".server-network-option")!;
networkSettingsId.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-network-settings");
});
}

View File

@@ -0,0 +1,82 @@
'use strict';
const BaseComponent = require(__dirname + '/js/components/base.js');
const {ipcRenderer} = require('electron');
const Nav = require(__dirname + '/js/pages/preference/nav.js');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
class PreferenceView extends BaseComponent {
constructor() {
super();
this.$sidebarContainer = document.getElementById('sidebar');
this.$settingsContainer = document.getElementById('settings-container');
}
init() {
this.nav = new Nav({
$root: this.$sidebarContainer,
onItemSelected: this.handleNavigation.bind(this)
});
this.setDefaultView();
this.registerIpcs();
}
setDefaultView() {
let nav = 'General';
const hasTag = window.location.hash;
if (hasTag) {
nav = hasTag.substring(1);
}
this.handleNavigation(nav);
}
handleNavigation(navItem) {
this.nav.select(navItem);
switch (navItem) {
case 'Servers': {
this.section = new ServersSection({
$root: this.$settingsContainer
});
break;
}
case 'General': {
this.section = new GeneralSection({
$root: this.$settingsContainer
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
});
break;
}
case 'Shortcuts': {
this.section = new ShortcutsSection({
$root: this.$settingsContainer
});
break;
}
default: break;
}
this.section.init();
window.location.hash = `#${navItem}`;
}
registerIpcs() {
ipcRenderer.on('switch-settings-nav', (event, navItem) => {
this.handleNavigation(navItem);
});
}
}
window.onload = () => {
const preferenceView = new PreferenceView();
preferenceView.init();
};

View File

@@ -1,106 +0,0 @@
import type {DNDSettings} from "../../../../common/dnd-util";
import type {NavItem} from "../../../../common/types";
import {ipcRenderer} from "../../typed-ipc-renderer";
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";
export function initPreferenceView(): void {
const $sidebarContainer = document.querySelector("#sidebar")!;
const $settingsContainer = document.querySelector("#settings-container")!;
const nav = new Nav({
$root: $sidebarContainer,
onItemSelected: handleNavigation,
});
const navItem =
nav.navItems.find((navItem) => window.location.hash === `#${navItem}`) ??
"General";
handleNavigation(navItem);
function handleNavigation(navItem: NavItem): void {
nav.select(navItem);
switch (navItem) {
case "AddServer":
initServersSection({
$root: $settingsContainer,
});
break;
case "General":
initGeneralSection({
$root: $settingsContainer,
});
break;
case "Organizations":
initConnectedOrgSection({
$root: $settingsContainer,
});
break;
case "Network":
initNetworkSection({
$root: $settingsContainer,
});
break;
case "Shortcuts": {
initShortcutsSection({
$root: $settingsContainer,
});
break;
}
default:
((n: never) => n)(navItem);
}
window.location.hash = `#${navItem}`;
}
// Handle toggling and reflect changes in preference page
function handleToggle(elementName: string, state = false): void {
const inputSelector = `#${elementName} .action .switch input`;
const input: HTMLInputElement = document.querySelector(inputSelector)!;
if (input) {
input.checked = state;
}
}
ipcRenderer.on("switch-settings-nav", (_event: Event, navItem: NavItem) => {
handleNavigation(navItem);
});
ipcRenderer.on("toggle-sidebar-setting", (_event: Event, state: boolean) => {
handleToggle("sidebar-option", state);
});
ipcRenderer.on("toggle-menubar-setting", (_event: Event, state: boolean) => {
handleToggle("menubar-option", state);
});
ipcRenderer.on("toggle-tray", (_event: Event, state: boolean) => {
handleToggle("tray-option", state);
});
ipcRenderer.on(
"toggle-dnd",
(_event: Event, _state: boolean, newSettings: Partial<DNDSettings>) => {
handleToggle("show-notification-option", newSettings.showNotification);
handleToggle("silent-option", newSettings.silent);
if (process.platform === "win32") {
handleToggle("flash-taskbar-option", newSettings.flashTaskbarOnMessage);
}
},
);
}
window.addEventListener("load", initPreferenceView);

View File

@@ -0,0 +1,77 @@
'use strict';
const {dialog} = require('electron').remote;
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
class ServerInfoForm extends BaseComponent {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-card">
<div class="server-info-left">
<img class="server-info-icon" src="${this.props.server.icon}"/>
</div>
<div class="server-info-right">
<div class="server-info-row">
<span class="server-info-alias">${this.props.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i>
</div>
<div class="server-info-row">
<input class="setting-input-value" disabled value="${this.props.server.url}"/>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
</div>
</div>
</div>
</div>
`;
}
init() {
this.initForm();
this.initActions();
}
initForm() {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0];
this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0];
this.props.$root.appendChild(this.$serverInfoForm);
}
initActions() {
this.$deleteServerButton.addEventListener('click', () => {
dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure you want to delete this server?'
}, response => {
if (response === 0) {
DomainUtil.removeDomain(this.props.index);
this.props.onChange(this.props.index);
}
});
});
this.$openServerButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
this.$serverInfoAlias.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
}
}
module.exports = ServerInfoForm;

View File

@@ -1,82 +0,0 @@
import {remote} from "electron";
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";
const {dialog} = remote;
interface 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}" />
<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>
</div>
</div>
<div class="server-info-right">
<div class="server-info-row server-url">
<span class="server-url-info" title="${props.server.url}"
>${props.server.url}</span
>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<span>${t.__("Disconnect")}</span>
</div>
</div>
</div>
</div>
`);
const $serverInfoAlias = $serverInfoForm.querySelector(".server-info-alias")!;
const $serverIcon = $serverInfoForm.querySelector(".server-info-icon")!;
const $deleteServerButton = $serverInfoForm.querySelector(
".server-delete-action",
)!;
const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!;
props.$root.append($serverInfoForm);
$deleteServerButton.addEventListener("click", async () => {
const {response} = await dialog.showMessageBox({
type: "warning",
buttons: [t.__("YES"), t.__("NO")],
defaultId: 0,
message: t.__("Are you sure you want to disconnect this organization?"),
});
if (response === 0) {
if (DomainUtil.removeDomain(props.index)) {
ipcRenderer.send("reload-full-app");
} else {
const {title, content} = Messages.orgRemovalError(
DomainUtil.getDomain(props.index).url,
);
dialog.showErrorBox(title, content);
}
}
});
$openServerButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index);
});
$serverInfoAlias.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index);
});
$serverIcon.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index);
});
}

View File

@@ -0,0 +1,62 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js');
const NewServerForm = require(__dirname + '/new-server-form.js');
class ServersSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="title">Add Server</div>
<div id="new-server-container"></div>
<div class="title" id="existing-servers"></div>
<div id="server-info-container"></div>
</div>
`;
}
init() {
this.initServers();
}
initServers() {
this.props.$root.innerHTML = '';
const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template();
this.$serverInfoContainer = document.getElementById('server-info-container');
this.$existingServers = document.getElementById('existing-servers');
this.$newServerContainer = document.getElementById('new-server-container');
this.$newServerButton = document.getElementById('new-server-action');
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
// Show Existing servers if servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
this.initNewServerForm();
for (let i = 0; i < servers.length; i++) {
new ServerInfoForm({
$root: this.$serverInfoContainer,
server: servers[i],
index: i,
onChange: this.reloadApp
}).init();
}
}
initNewServerForm() {
new NewServerForm({
$root: this.$newServerContainer,
onChange: this.reloadApp
}).init();
}
}
module.exports = ServersSection;

Some files were not shown because too many files have changed in this diff Show More