Compare commits

..

101 Commits

Author SHA1 Message Date
Kanishk Kakar
7338f802e1 docs: Add release notes for v3.1.0-beta. 2019-07-22 20:09:04 +05:30
Akash Nimare
6860f6d836 release: New release v3.1.0-beta. 2019-07-19 00:37:12 +05:30
Kanishk Kakar
088ddf9c62 dev: Use .env file for Sentry DSN.
Reads .env file in root folder of repo to get Sentry DSN for builds.
2019-07-19 00:23:10 +05:30
Priyank Patel
95da6c0d58 dependencies: Remove @types/node.
Electron already depends on the @types/node and extends
it in electron.d.ts.
2019-07-17 00:22:17 +05:30
Akash Nimare
1a3d3fc5ff system-util: Fix user-agent. 2019-07-17 00:22:17 +05:30
Priyank Patel
535039216b package.json: Add clean-ts-files script. 2019-07-17 00:22:17 +05:30
Priyank Patel
f449db1dc1 package.json: Run tsc before running electron-builder. 2019-07-17 00:22:17 +05:30
vsvipul
73e0e81abd gulp: Add tasks for compile and run from src. 2019-07-17 00:22:17 +05:30
Priyank Patel
dd4f3047c3 tools: Add tools for running typescript compiler and electron app.
This tool handles edge cases where all the typescript files might not
be compiled when the electron process starts this defer that; it runs
tsc compiler first.

Next, we want to only run the compiler if the files are not
compiled.
2019-07-17 00:22:17 +05:30
vsvipul
e4ef0e195a typescript: Do final cleanup of issues.
Using `import * as` import syntax causes some problem if the
module exports a class or function. Because the whole point of
star import is to import every property the module exports. It turns
out we have been using it incorrectly in many places which this commit
fixes.

Then we fix a linting error by adding a eslint disable rule to solve
it along with a TODO because the way we currently do it is wrong.

Finally, to conclude this cleanup, we merge all the .gitignore paths
into once now that we can.
2019-07-17 00:22:17 +05:30
Priyank Patel
00a925e39e typscript: Fix electron-debug not found error in app.
The import in question is electron-is-dev one, others we are
just fixing while we are at it. The problem with this way of importing
`import * as` is that the isDev would be { default: true | false } not
true | false as we like it to be. This causes and error in production app
because electron-debug isn't installed.

Next, we fix the electron-connect import in main.js.
2019-07-17 00:22:17 +05:30
Priyank Patel
c34b4f9151 tsconfig: Remove declare module * declaration and normalize imports.
The change in this commits are pretty involved but cannot be split
into small commits. The main changes in this commits are:
    * Remove declare module * now that we don't need it
    * Normalize import paths so typescript is happy

Previously, we were using wrong import paths and so typescript couldn't
really provide full types information for imports. The wrong paths isn't
a bug because it was done to make sure it work when it was imported via a
script tag; we fix this by using require inside the script tag in main.html.

Also, did audit to make sure we correctly use __dirname not that it's
value will be diffrent, it won't be js/ but will be respective to the file
path of the module.
2019-07-17 00:22:17 +05:30
Priyank Patel
8a13f7914c typescript: Fix incorrect types for feedback.ts.
Not that the previous types were incorrect but they were wrong.
SendFeedback extends the HTMLElement and is a class so it has it's
own properties so extended the properties accordingly.
2019-07-17 00:22:17 +05:30
vsvipul
b13c86e2db typescript: Migrate main.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
4fb4773a2b typescript: Migrate tray.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
deda98b9d4 typescript: Migrate preload.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
f98457a0cb typescript: Add typings for window and page_params. 2019-07-17 00:22:17 +05:30
vsvipul
3436fb3e68 domain-util: Change object to any in domain-util. 2019-07-17 00:22:17 +05:30
vsvipul
448822a5fb typescript: Migrate messages.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
6e07945d7c typescript: Migrate spellchecker.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
54a8e33b01 typescript: Migrate feedback.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
93c22ca89d gitignore: Add js and resources dir to gitignore. 2019-07-17 00:22:17 +05:30
vsvipul
a2fca89551 typescript: Migrate electron-bridge.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
1516452a8c tsconfig: Turn off noUnusedParamaters rule.
We want to turn off this rule because this requires us to add _
in front of parameter that are unused which we have lot of, for example,
the ipc listeners.
2019-07-17 00:22:17 +05:30
vsvipul
700e73105e typescript: Migrate network.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
c41bbf9dfd typescript: Migrate shortcuts-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
347de5981a typescript: Migrate servers-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
d932a2b2c4 typescript: Migrate server-info-form.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
34e403d0d2 typescript: Migrate preference.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
77c5b8af30 typescript: Migrate new-server-form.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
b2bc268c09 typescript: Migrate network-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
8c52b563bf typescript: Migrate nav.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
a8109c5106 typescript: Migrate general-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
76818b7def typescript: Migrate find-accounts.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
a5ea3d475f typescript: Migrate connected-org-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
2f9205b0c4 typescript: Migrate base-section.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
3f00beb291 typescript: Migrate badge-settings.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
72f356de9c typescript: Migrate add-certificate.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
482264d878 gitignore: Add pages/ dir to gitignore. 2019-07-17 00:22:17 +05:30
Priyank Patel
d7725c121c typescript: Migrate default-notification to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
2b19cdecf2 typescript: Migrate index.js to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
1182af23e4 typescript: Migrate darwin-notification to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
6192801083 typescript: Migrate helper.js to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
63a913487c .gitignore: Ignore typescript compiled files in notifications directory. 2019-07-17 00:22:17 +05:30
vsvipul
77899f9f48 typescript: Migrate webview.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
2b1e594286 tabs: Fix constructors in webview tabs.
template() property used earlier in tab.ts was a
property of its child classes, which causes problems
in compiler, which this PR fixes.
2019-07-17 00:22:17 +05:30
vsvipul
312dc1daaf typescript: Migrate functional-tab.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
744d056e26 typescript: Migrate server-tab.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
433f11ebd1 typescript: Migrate tab.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
506982b4c6 typescript: Migrate handle-external-link.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
4adda8b8ae typescript: Migrate base.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
fbef3e4d7b utils: Change export statement in domain util. 2019-07-17 00:22:17 +05:30
vsvipul
f076343068 gitignore: Ignore typescript compiled file in js/components. 2019-07-17 00:22:17 +05:30
Priyank Patel
d5c9e638ca typescript: Migrate preventdrag to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
d5a343d549 typescript: Add needed type annotations after rebasing to master. 2019-07-17 00:22:17 +05:30
Priyank Patel
4412f60435 typescript: Add types to node-json-db variables.
This adds JsonDB type to db variables we use, which were
previously any due to issue with node-json-db not specifying
the types field in package.json.
2019-07-17 00:22:17 +05:30
Priyank Patel
cec2b3136d dependencies: Upgrade node-json-db to v0.9.2.
This version was released just for the fact that the
bundles typescript declaration file was not defined
in package.json hence typescript couldn't detect it.
2019-07-17 00:22:17 +05:30
Priyank Patel
9ba2e0ad1e .editconfig: Set the indent_size to 4.
This will make GitHub show 4 spaces for a tab instead of 8; makes
PR diffs more readable.
2019-07-17 00:22:17 +05:30
vsvipul
eb30f48a82 typescript: Migrate index.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
edc5b92ab4 typescript: Migrate linuxupdater to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
29753b1a46 typescript: Migrate autoupdater to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
f490efb2df .gitignore: Ignore .js files in app dir. 2019-07-17 00:22:17 +05:30
vsvipul
bb7426a789 utils: Change export statement in linux-update-util. 2019-07-17 00:22:17 +05:30
vsvipul
e7edfffa89 config: Linting changes to package.json and tsconfig.json 2019-07-17 00:22:17 +05:30
Priyank Patel
fb700350f9 typescript: Fix some issues with recent changes.
In domain-util, we were using `import * as ` syntax which
compiled down to `__importStar(require('...'))` which were
not giving the same result as before and causing errors. This
fixes that.

In logger-util, we were missing the call to .apply function
so it should have been `this._log.apply(...)`, but then when we
use apply typescript still complains about merging string with any[]
so we decided to go with bind.
2019-07-17 00:22:17 +05:30
Priyank Patel
79d0688bcd .gitignore: Ignore typescript compiled files. 2019-07-17 00:22:17 +05:30
Priyank Patel
c6c36b698d typescript: Migrate domain-util to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
8ef39553b8 typescript: Migrate linux-update-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
7374fcbe78 typescript: Migrate config-util to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
aa2c55538f typescript: Migrate params-util.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
4d679acb0c typescript: Migrate logger-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
795b9fdc17 xo: Upgrade xo to v0.24.0.
This version also includes new rule we want to ignore.
2019-07-17 00:22:17 +05:30
Priyank Patel
82d9f795c2 xo: Disable some of the typescript-eslint rule.
One rule requires use to move all the functions which is
unnessacary churn.
2019-07-17 00:22:17 +05:30
vsvipul
ec91d46f7b typescript: Migrate menu.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
728418742d typescript: Migrate proxy-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
8b0e9b55fb typescript: Migrate reconnect-util to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
b40be8d891 xo: Disable lines-between-class-members rule.
Apparently this doesn't work well with typescript files.
For example this will throw this linting error:
class Test {
  a: boolean;
  b: string; // needs a new line before this one to fix this linting error
}
2019-07-17 00:22:17 +05:30
Priyank Patel
e12d69720d typing.d.ts: Add comment for why we use declare module. 2019-07-17 00:22:17 +05:30
vsvipul
8353c4de16 typescript: Migrate dnd-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
8c3a2a80c7 typescript: Migrate link-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
5c4221fc31 typescript: Migrate default-util to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
a57c269935 typescript: Migrate common-util to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
ce81a1b367 typescript: Fix wrong import syntax in request-util.ts.
Using the previous syntax was wrong because the file export
the class For example we would use previous syntax if the class
was export by `module.export = { ProxyUtil }`.
2019-07-17 00:22:17 +05:30
vsvipul
26e97c8746 typescript: Migrate certificate-util to typescript. 2019-07-17 00:22:17 +05:30
Priyank Patel
393a0fc2be request-util: Fix linting error. 2019-07-17 00:22:17 +05:30
vsvipul
b0348d6bca typescript: Migrate system-util.js to typescript.
We use to check for a case where os.platform() == win64
which is not possible so removed the condition.
2019-07-17 00:22:17 +05:30
vsvipul
9c68fcdda8 typescript: Migrate request-util to typescript.
We use newer javascript method for string that linting enforces like
startsWith, includes.
2019-07-17 00:22:17 +05:30
vsvipul
543ead4da3 typescript: Migrate sentry-util.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
b5f8d54aea typescript: Migrate startup.js to typescript. 2019-07-17 00:22:17 +05:30
vsvipul
7b07a20c04 typescript: Setup typescript project.
This adds all the dependencies and tsconfig.json and setup
for working on the typescript migration.
2019-07-17 00:22:17 +05:30
Akash Nimare
6b1a55c7e4 macOS: Support dark mode on macOS.
Fixes: #792.
2019-07-16 21:14:05 +05:30
Akash Nimare
d46bdba8ce asar: Unpack node binding files.
We have noticed that on Windows, Zulip app is creating a temp file at %appdata%/Local/Temp
as <guid>.tmp.node on each run. Adding *.node files to asarUnpack fixes this issue.
More info - https://github.com/brave/browser-laptop/issues/12534

Improves: #789.
2019-07-15 18:45:27 +05:30
Tyler Thrailkill
d3bc765e59 Add missing transitive dependencies.
zulip was using a dependency on fs-extra that was resolving transitively
through electron. This adds the dependency explicitly. This is safer and
allows tools like pnpm to function.
2019-07-02 15:27:46 +05:30
Priyank Patel
613e44ef8d reconnect-util: Gracefully reload current webview. (#783)
Before we were destroying, removing the webview elements when we
need to reload webviews which cause unwanted side-effect for example
drafts not saving because no beforeunload event handler isn't run.

Fixes: #767.
2019-07-01 23:35:50 +05:30
ViPuL
f0e4a3bc53 menu: Disable menu items on non-server pages.
Disables 'Copy Zulip URL' and 'Back' and 'Forward'
when user is on non-server pages, like Settings page.
Also, simplifies the logic for setting enable state
for 'Switch to Next Organization' and 'Switch to
Previous Organization'.
2019-06-29 01:46:14 +05:30
Kanishk Kakar
427c86afaf spellchecker: Improve auto detection of spellchecker language.
*Enabling spellchecker forces app to auto-detect the language the user
is typing in.

*Provide context of the server language when initialising spellchecker.

Fixes: #542.
2019-06-25 22:29:13 +05:30
vsvipul
0e89880cfb proxy-util: Fix syntax error.
Fixes a wrongly-placed bracket.
2019-06-25 21:05:41 +05:30
vsvipul
b1f1b49982 preference: Remove unused defaultId parameter.
defaultId is not a property of OpenDialogOptions.
2019-06-25 21:05:41 +05:30
Priyank Patel
390bf907e5 components: Remove unused isLoading function from Tab. 2019-06-23 00:46:38 +05:30
Akash Nimare
02c8a27567 menu: Open help page of current active server.
* menu: Open help page of the currently active server.

Fixes: #758.
2019-06-21 01:02:48 +05:30
Kanishk Kakar
db4e8e5129 db: Fix JSON db errors on tab switch. 2019-06-20 20:54:30 +05:30
68 changed files with 3295 additions and 2052 deletions

View File

@@ -2,6 +2,7 @@ root = true
[*] [*]
indent_style = tab indent_style = tab
indent_size = 4
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

3
.gitignore vendored
View File

@@ -36,3 +36,6 @@ config.gypi
# tests/package.json # tests/package.json
.python-version .python-version
# Ignore all the typescript compiled files
app/**/*.js

View File

@@ -1,18 +1,19 @@
'use strict'; 'use strict';
const { app, dialog, shell } = require('electron'); import { app, dialog, shell } from 'electron';
const { autoUpdater } = require('electron-updater'); import { autoUpdater } from 'electron-updater';
const isDev = require('electron-is-dev'); import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
const ConfigUtil = require('./../renderer/js/utils/config-util.js'); import log = require('electron-log');
import isDev = require('electron-is-dev');
import ConfigUtil = require('../renderer/js/utils/config-util');
function appUpdater(updateFromMenu = false) { export function appUpdater(updateFromMenu = false): void {
// Don't initiate auto-updates in development // Don't initiate auto-updates in development
if (isDev) { if (isDev) {
return; return;
} }
if (process.platform === 'linux' && !process.env.APPIMAGE) { if (process.platform === 'linux' && !process.env.APPIMAGE) {
const { linuxUpdateNotification } = require('./linuxupdater');
linuxUpdateNotification(); linuxUpdateNotification();
return; return;
} }
@@ -23,8 +24,6 @@ function appUpdater(updateFromMenu = false) {
const LogsDir = `${app.getPath('userData')}/Logs`; const LogsDir = `${app.getPath('userData')}/Logs`;
// Log whats happening // Log whats happening
const log = require('electron-log');
log.transports.file.file = `${LogsDir}/updates.log`; log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = 'info'; log.transports.file.level = 'info';
autoUpdater.logger = log; autoUpdater.logger = log;
@@ -84,7 +83,6 @@ Current Version: ${app.getVersion()}`
}); });
// Ask the user if update is available // Ask the user if update is available
// eslint-disable-next-line no-unused-vars
autoUpdater.on('update-downloaded', event => { autoUpdater.on('update-downloaded', event => {
// Ask user to update the app // Ask user to update the app
dialog.showMessageBox({ dialog.showMessageBox({
@@ -106,7 +104,3 @@ Current Version: ${app.getVersion()}`
// Init for updates // Init for updates
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
} }
module.exports = {
appUpdater
};

View File

@@ -1,21 +1,25 @@
'use strict'; 'use strict';
const path = require('path'); import { sentryInit } from '../renderer/js/utils/sentry-util';
const fs = require('fs'); import { appUpdater } from './autoupdater';
import { setAutoLaunch } from './startup';
const electron = require('electron');
const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater');
const { setAutoLaunch } = require('./startup');
import windowStateKeeper = require('electron-window-state');
import path = require('path');
import fs = require('fs');
import isDev = require('electron-is-dev');
import electron = require('electron');
const { app, ipcMain } = electron; const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js'); import AppMenu = require('./menu');
const ConfigUtil = require('./../renderer/js/utils/config-util.js'); import BadgeSettings = require('../renderer/js/pages/preference/badge-settings');
const ProxyUtil = require('./../renderer/js/utils/proxy-util.js'); import ConfigUtil = require('../renderer/js/utils/config-util');
const { sentryInit } = require('./../renderer/js/utils/sentry-util.js'); import ProxyUtil = require('../renderer/js/utils/proxy-util');
interface PatchedGlobal extends NodeJS.Global {
mainWindowState: windowStateKeeper.State;
}
const globalPatched = global as PatchedGlobal;
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
// in development mode // in development mode
@@ -24,8 +28,8 @@ if (isDev) {
} }
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow; let mainWindow: Electron.BrowserWindow;
let badgeCount; let badgeCount: number;
let isQuitting = false; let isQuitting = false;
@@ -49,20 +53,20 @@ if (singleInstanceLock) {
const APP_ICON = path.join(__dirname, '../resources', 'Icon'); const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = () => { const iconPath = (): string => {
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png'); return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
}; };
function createMainWindow() { function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({ const mainWindowState: windowStateKeeper.State = windowStateKeeper({
defaultWidth: 1100, defaultWidth: 1100,
defaultHeight: 720, defaultHeight: 720,
path: `${app.getPath('userData')}/config` path: `${app.getPath('userData')}/config`
}); });
// Let's keep the window position global so that we can access it in other process // Let's keep the window position global so that we can access it in other process
global.mainWindowState = mainWindowState; globalPatched.mainWindowState = mainWindowState;
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
// This settings needs to be saved in config // This settings needs to be saved in config
@@ -134,7 +138,7 @@ app.disableHardwareAcceleration();
app.commandLine.appendSwitch('force-color-profile', 'srgb'); app.commandLine.appendSwitch('force-color-profile', 'srgb');
// eslint-disable-next-line max-params // eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _url: string, _error: string, _certificate: any, callback) => {
event.preventDefault(); event.preventDefault();
callback(true); callback(true);
}); });
@@ -146,7 +150,7 @@ app.on('activate', () => {
}); });
app.on('ready', () => { app.on('ready', () => {
appMenu.setMenu({ AppMenu.setMenu({
tabs: [] tabs: []
}); });
mainWindow = createMainWindow(); mainWindow = createMainWindow();
@@ -234,7 +238,7 @@ app.on('ready', () => {
}); });
ipcMain.on('clear-app-settings', () => { ipcMain.on('clear-app-settings', () => {
global.mainWindowState.unmanage(mainWindow); globalPatched.mainWindowState.unmanage();
app.relaunch(); app.relaunch();
app.exit(); app.exit();
}); });
@@ -251,49 +255,49 @@ app.on('ready', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
}); });
ipcMain.on('toggle-menubar', (event, showMenubar) => { ipcMain.on('toggle-menubar', (_event: Electron.IpcMessageEvent, showMenubar: boolean) => {
mainWindow.setAutoHideMenuBar(showMenubar); mainWindow.setAutoHideMenuBar(showMenubar);
mainWindow.setMenuBarVisibility(!showMenubar); mainWindow.setMenuBarVisibility(!showMenubar);
page.send('toggle-autohide-menubar', showMenubar, true); page.send('toggle-autohide-menubar', showMenubar, true);
}); });
ipcMain.on('update-badge', (event, messageCount) => { ipcMain.on('update-badge', (_event: Electron.IpcMessageEvent, messageCount: number) => {
badgeCount = messageCount; badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount); page.send('tray', messageCount);
}); });
ipcMain.on('update-taskbar-icon', (event, data, text) => { ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMessageEvent, data: any, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow); BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
}); });
ipcMain.on('forward-message', (event, listener, ...params) => { ipcMain.on('forward-message', (_event: Electron.IpcMessageEvent, listener: any, ...params: any[]) => {
page.send(listener, ...params); page.send(listener, ...params);
}); });
ipcMain.on('update-menu', (event, props) => { ipcMain.on('update-menu', (_event: Electron.IpcMessageEvent, props: any) => {
appMenu.setMenu(props); AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) { if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`); mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
} }
}); });
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => { ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMessageEvent, AutoLaunchValue: boolean) => {
setAutoLaunch(AutoLaunchValue); setAutoLaunch(AutoLaunchValue);
}); });
ipcMain.on('downloadFile', (event, url, downloadPath) => { ipcMain.on('downloadFile', (_event: Electron.IpcMessageEvent, url: string, downloadPath: string) => {
page.downloadURL(url); page.downloadURL(url);
page.session.once('will-download', (event, item) => { page.session.once('will-download', (_event: Event, item) => {
const filePath = path.join(downloadPath, item.getFilename()); const filePath = path.join(downloadPath, item.getFilename());
const getTimeStamp = () => { const getTimeStamp = (): any => {
const date = new Date(); const date = new Date();
return date.getTime(); return date.getTime();
}; };
const formatFile = filePath => { const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath); const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension); const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`; return `${baseName}-${getTimeStamp()}${fileExtension}`;
@@ -307,7 +311,7 @@ app.on('ready', () => {
item.setSavePath(setFilePath); item.setSavePath(setFilePath);
item.on('updated', (event, state) => { item.on('updated', (_event: Event, state) => {
switch (state) { switch (state) {
case 'interrupted': { case 'interrupted': {
// Can interrupted to due to network error, cancel download then // Can interrupted to due to network error, cancel download then
@@ -327,7 +331,7 @@ app.on('ready', () => {
} }
} }
}); });
item.once('done', (event, state) => { item.once('done', (_event: Event, state) => {
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename(); const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
if (state === 'completed') { if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), getFileName); page.send('downloadFileCompleted', item.getSavePath(), getFileName);
@@ -341,19 +345,23 @@ app.on('ready', () => {
}); });
}); });
ipcMain.on('realm-name-changed', (event, serverURL, realmName) => { ipcMain.on('realm-name-changed', (_event: Electron.IpcMessageEvent, serverURL: string, realmName: string) => {
page.send('update-realm-name', serverURL, realmName); page.send('update-realm-name', serverURL, realmName);
}); });
ipcMain.on('realm-icon-changed', (event, serverURL, iconURL) => { ipcMain.on('realm-icon-changed', (_event: Electron.IpcMessageEvent, serverURL: string, iconURL: string) => {
page.send('update-realm-icon', serverURL, iconURL); page.send('update-realm-icon', serverURL, iconURL);
}); });
// Using event.sender.send instead of page.send here to // Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load. // make sure the value of errorReporting is sent only once on load.
ipcMain.on('error-reporting', event => { ipcMain.on('error-reporting', (event: Electron.IpcMessageEvent) => {
event.sender.send('error-reporting-val', errorReporting); event.sender.send('error-reporting-val', errorReporting);
}); });
ipcMain.on('save-last-tab', (_event: Electron.IpcMessageEvent, index: number) => {
ConfigUtil.setConfigItem('lastActiveTab', index);
});
}); });
app.on('before-quit', () => { app.on('before-quit', () => {

View File

@@ -1,19 +1,18 @@
const { app } = require('electron'); import { app, Notification } from 'electron';
const { Notification } = require('electron');
const request = require('request'); import request = require('request');
const semver = require('semver'); import semver = require('semver');
const ConfigUtil = require('../renderer/js/utils/config-util'); import ConfigUtil = require('../renderer/js/utils/config-util');
const ProxyUtil = require('../renderer/js/utils/proxy-util'); import ProxyUtil = require('../renderer/js/utils/proxy-util');
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util'); import LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
const Logger = require('../renderer/js/utils/logger-util'); import Logger = require('../renderer/js/utils/logger-util');
const logger = new Logger({ const logger = new Logger({
file: 'linux-update-util.log', file: 'linux-update-util.log',
timestamp: true timestamp: true
}); });
function linuxUpdateNotification() { export function linuxUpdateNotification(): void {
let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases'; let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases';
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest'; url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy'); const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
@@ -25,7 +24,7 @@ function linuxUpdateNotification() {
ecdhCurve: 'auto' ecdhCurve: 'auto'
}; };
request(options, (error, response, body) => { request(options, (error: any, response: any, body: any) => {
if (error) { if (error) {
logger.error('Linux update error.'); logger.error('Linux update error.');
logger.error(error); logger.error(error);
@@ -47,7 +46,3 @@ function linuxUpdateNotification() {
} }
}); });
} }
module.exports = {
linuxUpdateNotification
};

View File

@@ -1,15 +1,13 @@
'use strict'; 'use strict';
const path = require('path'); import { app, shell, BrowserWindow, Menu, dialog } from 'electron';
import { appUpdater } from './autoupdater';
const { app, shell, BrowserWindow, Menu, dialog } = require('electron'); import AdmZip = require('adm-zip');
import fs = require('fs-extra');
const fs = require('fs-extra'); import path = require('path');
const AdmZip = require('adm-zip'); import DNDUtil = require('../renderer/js/utils/dnd-util');
const { appUpdater } = require('./autoupdater'); import Logger = require('../renderer/js/utils/logger-util');
import ConfigUtil = require('../renderer/js/utils/config-util');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
const DNDUtil = require(__dirname + '/../renderer/js/utils/dnd-util.js');
const Logger = require(__dirname + '/../renderer/js/utils/logger-util.js');
const appName = app.getName(); const appName = app.getName();
@@ -19,11 +17,12 @@ const logger = new Logger({
}); });
class AppMenu { class AppMenu {
getHistorySubmenu() { getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: 'Back', label: 'Back',
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left', accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
click(item, focusedWindow) { enabled: enableMenu,
click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('back'); AppMenu.sendAction('back');
} }
@@ -31,7 +30,8 @@ class AppMenu {
}, { }, {
label: 'Forward', label: 'Forward',
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right', accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
click(item, focusedWindow) { enabled: enableMenu,
click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('forward'); AppMenu.sendAction('forward');
} }
@@ -39,58 +39,64 @@ class AppMenu {
}]; }];
} }
getToolsSubmenu() { getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: `Check for Updates`, label: 'Check for Updates',
click() { click() {
AppMenu.checkForUpdate(); AppMenu.checkForUpdate();
} }
}, },
{ {
label: `Release Notes`, label: 'Release Notes',
click() { click() {
shell.openExternal(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`); shell.openExternal(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`);
} }
}, { },
{
type: 'separator' type: 'separator'
}, { },
{
label: 'Factory Reset', label: 'Factory Reset',
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D', accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
click() { click() {
AppMenu.resetAppSettings(); AppMenu.resetAppSettings();
} }
}, { },
{
label: 'Download App Logs', label: 'Download App Logs',
click() { click() {
const zip = new AdmZip(); const zip = new AdmZip();
let date = new Date(); const date = new Date();
date = date.toLocaleDateString().replace(/\//g, '-'); const dateString = date.toLocaleDateString().replace(/\//g, '-');
// Create a zip file of all the logs and config data // Create a zip file of all the logs and config data
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`); zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`);
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`); zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`);
// Put the log file in downloads folder // Put the log file in downloads folder
const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${date}.zip`; const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${dateString}.zip`;
zip.writeZip(logFilePath); zip.writeZip(logFilePath);
// Open and select the log file // Open and select the log file
shell.showItemInFolder(logFilePath); shell.showItemInFolder(logFilePath);
} }
}, { },
{
type: 'separator' type: 'separator'
}, { },
{
label: 'Toggle DevTools for Zulip App', label: 'Toggle DevTools for Zulip App',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.toggleDevTools(); focusedWindow.webContents.toggleDevTools();
} }
} }
}, { },
{
label: 'Toggle DevTools for Active Tab', label: 'Toggle DevTools for Active Tab',
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U', accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('tab-devtools'); AppMenu.sendAction('tab-devtools');
} }
@@ -98,11 +104,11 @@ class AppMenu {
}]; }];
} }
getViewSubmenu() { getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: 'Reload', label: 'Reload',
accelerator: 'CommandOrControl+R', accelerator: 'CommandOrControl+R',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('reload-current-viewer'); AppMenu.sendAction('reload-current-viewer');
} }
@@ -110,7 +116,7 @@ class AppMenu {
}, { }, {
label: 'Hard Reload', label: 'Hard Reload',
accelerator: 'CommandOrControl+Shift+R', accelerator: 'CommandOrControl+Shift+R',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('hard-reload'); AppMenu.sendAction('hard-reload');
} }
@@ -122,7 +128,7 @@ class AppMenu {
}, { }, {
label: 'Zoom In', label: 'Zoom In',
accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=', accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomIn'); AppMenu.sendAction('zoomIn');
} }
@@ -130,7 +136,7 @@ class AppMenu {
}, { }, {
label: 'Zoom Out', label: 'Zoom Out',
accelerator: 'CommandOrControl+-', accelerator: 'CommandOrControl+-',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomOut'); AppMenu.sendAction('zoomOut');
} }
@@ -138,7 +144,7 @@ class AppMenu {
}, { }, {
label: 'Actual Size', label: 'Actual Size',
accelerator: 'CommandOrControl+0', accelerator: 'CommandOrControl+0',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomActualSize'); AppMenu.sendAction('zoomActualSize');
} }
@@ -147,7 +153,7 @@ class AppMenu {
type: 'separator' type: 'separator'
}, { }, {
label: 'Toggle Tray Icon', label: 'Toggle Tray Icon',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.send('toggletray'); focusedWindow.webContents.send('toggletray');
} }
@@ -155,7 +161,7 @@ class AppMenu {
}, { }, {
label: 'Toggle Sidebar', label: 'Toggle Sidebar',
accelerator: 'CommandOrControl+Shift+S', accelerator: 'CommandOrControl+Shift+S',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('showSidebar'); const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue); focusedWindow.webContents.send('toggle-sidebar', newValue);
@@ -166,7 +172,7 @@ class AppMenu {
label: 'Auto hide Menu bar', label: 'Auto hide Menu bar',
checked: ConfigUtil.getConfigItem('autoHideMenubar', false), checked: ConfigUtil.getConfigItem('autoHideMenubar', false),
visible: process.platform !== 'darwin', visible: process.platform !== 'darwin',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar'); const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
focusedWindow.setAutoHideMenuBar(newValue); focusedWindow.setAutoHideMenuBar(newValue);
@@ -179,7 +185,7 @@ class AppMenu {
}]; }];
} }
getHelpSubmenu() { getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
return [ return [
{ {
label: `${appName + ' Desktop'} v${app.getVersion()}`, label: `${appName + ' Desktop'} v${app.getVersion()}`,
@@ -187,7 +193,7 @@ class AppMenu {
}, },
{ {
label: 'About Zulip', label: 'About Zulip',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-about'); AppMenu.sendAction('open-about');
} }
@@ -200,7 +206,8 @@ class AppMenu {
AppMenu.sendAction('open-help'); AppMenu.sendAction('open-help');
} }
} }
}, { },
{
label: 'Report an Issue', label: 'Report an Issue',
click() { click() {
// the goal is to notify the main.html BrowserWindow // the goal is to notify the main.html BrowserWindow
@@ -209,11 +216,12 @@ class AppMenu {
window.webContents.send('open-feedback-modal'); window.webContents.send('open-feedback-modal');
}); });
} }
}]; }
];
} }
getWindowSubmenu(tabs, activeTabIndex) { getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
const initialSubmenu = [{ const initialSubmenu: any[] = [{
role: 'minimize' role: 'minimize'
}, { }, {
role: 'close' role: 'close'
@@ -224,32 +232,32 @@ class AppMenu {
initialSubmenu.push({ initialSubmenu.push({
type: 'separator' type: 'separator'
}); });
for (let i = 0; i < tabs.length; i++) { tabs.forEach(tab => {
// Do not add functional tab settings to list of windows in menu bar // Do not add functional tab settings to list of windows in menu bar
if (tabs[i].props.role === 'function' && tabs[i].webview.props.name === 'Settings') { if (tab.props.role === 'function' && tab.webview.props.name === 'Settings') {
continue; return;
} }
initialSubmenu.push({ initialSubmenu.push({
label: tabs[i].webview.props.name, label: tab.webview.props.name,
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`, accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`,
checked: tabs[i].props.index === activeTabIndex, checked: tab.props.index === activeTabIndex,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', tabs[i].props.index); AppMenu.sendAction('switch-server-tab', tab.props.index);
} }
}, },
type: 'checkbox' type: 'checkbox'
}); });
} });
initialSubmenu.push({ initialSubmenu.push({
type: 'separator' type: 'separator'
}); });
initialSubmenu.push({ initialSubmenu.push({
label: 'Switch to Next Organization', label: 'Switch to Next Organization',
accelerator: `Ctrl+Tab`, accelerator: `Ctrl+Tab`,
enabled: tabs[activeTabIndex].props.role === 'server', enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', AppMenu.getNextServer(tabs, activeTabIndex)); AppMenu.sendAction('switch-server-tab', AppMenu.getNextServer(tabs, activeTabIndex));
} }
@@ -257,8 +265,8 @@ class AppMenu {
}, { }, {
label: 'Switch to Previous Organization', label: 'Switch to Previous Organization',
accelerator: `Ctrl+Shift+Tab`, accelerator: `Ctrl+Shift+Tab`,
enabled: tabs[activeTabIndex].props.role === 'server', enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', AppMenu.getPreviousServer(tabs, activeTabIndex)); AppMenu.sendAction('switch-server-tab', AppMenu.getPreviousServer(tabs, activeTabIndex));
} }
@@ -269,7 +277,7 @@ class AppMenu {
return initialSubmenu; return initialSubmenu;
} }
getDarwinTpl(props) { getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props; const { tabs, activeTabIndex, enableMenu } = props;
return [{ return [{
@@ -277,7 +285,7 @@ class AppMenu {
submenu: [{ submenu: [{
label: 'Add Organization', label: 'Add Organization',
accelerator: 'Cmd+Shift+N', accelerator: 'Cmd+Shift+N',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('new-server'); AppMenu.sendAction('new-server');
} }
@@ -292,7 +300,7 @@ class AppMenu {
}, { }, {
label: 'Desktop Settings', label: 'Desktop Settings',
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-settings'); AppMenu.sendAction('open-settings');
} }
@@ -301,7 +309,7 @@ class AppMenu {
label: 'Keyboard Shortcuts', label: 'Keyboard Shortcuts',
accelerator: 'Cmd+Shift+K', accelerator: 'Cmd+Shift+K',
enabled: enableMenu, enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('shortcut'); AppMenu.sendAction('shortcut');
} }
@@ -311,7 +319,8 @@ class AppMenu {
}, { }, {
label: 'Copy Zulip URL', label: 'Copy Zulip URL',
accelerator: 'Cmd+Shift+C', accelerator: 'Cmd+Shift+C',
click(item, focusedWindow) { enabled: enableMenu,
click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('copy-zulip-url'); AppMenu.sendAction('copy-zulip-url');
} }
@@ -320,7 +329,7 @@ class AppMenu {
label: 'Log Out of Organization', label: 'Log Out of Organization',
accelerator: 'Cmd+L', accelerator: 'Cmd+L',
enabled: enableMenu, enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('log-out'); AppMenu.sendAction('log-out');
} }
@@ -367,10 +376,10 @@ class AppMenu {
submenu: this.getViewSubmenu() submenu: this.getViewSubmenu()
}, { }, {
label: 'History', label: 'History',
submenu: this.getHistorySubmenu() submenu: this.getHistorySubmenu(enableMenu)
}, { }, {
label: 'Window', label: 'Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex) submenu: this.getWindowSubmenu(tabs, activeTabIndex, enableMenu)
}, { }, {
label: 'Tools', label: 'Tools',
submenu: this.getToolsSubmenu() submenu: this.getToolsSubmenu()
@@ -380,7 +389,7 @@ class AppMenu {
}]; }];
} }
getOtherTpl(props) { getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props; const { tabs, activeTabIndex, enableMenu } = props;
return [{ return [{
@@ -388,7 +397,7 @@ class AppMenu {
submenu: [{ submenu: [{
label: 'Add Organization', label: 'Add Organization',
accelerator: 'Ctrl+Shift+N', accelerator: 'Ctrl+Shift+N',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('new-server'); AppMenu.sendAction('new-server');
} }
@@ -405,7 +414,7 @@ class AppMenu {
}, { }, {
label: 'Desktop Settings', label: 'Desktop Settings',
accelerator: 'Ctrl+,', accelerator: 'Ctrl+,',
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-settings'); AppMenu.sendAction('open-settings');
} }
@@ -414,7 +423,7 @@ class AppMenu {
label: 'Keyboard Shortcuts', label: 'Keyboard Shortcuts',
accelerator: 'Ctrl+Shift+K', accelerator: 'Ctrl+Shift+K',
enabled: enableMenu, enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('shortcut'); AppMenu.sendAction('shortcut');
} }
@@ -424,7 +433,8 @@ class AppMenu {
}, { }, {
label: 'Copy Zulip URL', label: 'Copy Zulip URL',
accelerator: 'Ctrl+Shift+C', accelerator: 'Ctrl+Shift+C',
click(item, focusedWindow) { enabled: enableMenu,
click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('copy-zulip-url'); AppMenu.sendAction('copy-zulip-url');
} }
@@ -433,7 +443,7 @@ class AppMenu {
label: 'Log Out of Organization', label: 'Log Out of Organization',
accelerator: 'Ctrl+L', accelerator: 'Ctrl+L',
enabled: enableMenu, enabled: enableMenu,
click(item, focusedWindow) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('log-out'); AppMenu.sendAction('log-out');
} }
@@ -470,10 +480,10 @@ class AppMenu {
submenu: this.getViewSubmenu() submenu: this.getViewSubmenu()
}, { }, {
label: '&History', label: '&History',
submenu: this.getHistorySubmenu() submenu: this.getHistorySubmenu(enableMenu)
}, { }, {
label: '&Window', label: '&Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex) submenu: this.getWindowSubmenu(tabs, activeTabIndex, enableMenu)
}, { }, {
label: '&Tools', label: '&Tools',
submenu: this.getToolsSubmenu() submenu: this.getToolsSubmenu()
@@ -484,7 +494,7 @@ class AppMenu {
}]; }];
} }
static sendAction(action, ...params) { static sendAction(action: any, ...params: any[]): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -494,11 +504,11 @@ class AppMenu {
win.webContents.send(action, ...params); win.webContents.send(action, ...params);
} }
static checkForUpdate() { static checkForUpdate(): void {
appUpdater(true); appUpdater(true);
} }
static getNextServer(tabs, activeTabIndex) { static getNextServer(tabs: any[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex + 1) % tabs.length; activeTabIndex = (activeTabIndex + 1) % tabs.length;
} }
@@ -506,7 +516,7 @@ class AppMenu {
return activeTabIndex; return activeTabIndex;
} }
static getPreviousServer(tabs, activeTabIndex) { static getPreviousServer(tabs: any[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length; activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} }
@@ -514,7 +524,7 @@ class AppMenu {
return activeTabIndex; return activeTabIndex;
} }
static resetAppSettings() { static resetAppSettings(): void {
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.'; const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';
// We save App's settings/configurations in following files // We save App's settings/configurations in following files
@@ -530,7 +540,7 @@ class AppMenu {
if (response === 0) { if (response === 0) {
settingFiles.forEach(settingFileName => { settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName); const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, error => { fs.access(getSettingFilesPath, (error: any) => {
if (error) { if (error) {
logger.error('Error while resetting app settings.'); logger.error('Error while resetting app settings.');
logger.error(error); logger.error(error);
@@ -545,11 +555,11 @@ class AppMenu {
}); });
} }
setMenu(props) { setMenu(props: any): void {
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props); const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl); const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} }
} }
module.exports = new AppMenu(); export = new AppMenu();

View File

@@ -1,10 +1,11 @@
'use strict'; 'use strict';
const { app } = require('electron'); import { app } from 'electron';
const AutoLaunch = require('auto-launch');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const setAutoLaunch = AutoLaunchValue => { import AutoLaunch = require('auto-launch');
import isDev = require('electron-is-dev');
import ConfigUtil = require('../renderer/js/utils/config-util');
export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
// Don't run this in development // Don't run this in development
if (isDev) { if (isDev) {
return; return;
@@ -28,7 +29,3 @@ const setAutoLaunch = AutoLaunchValue => {
ZulipAutoLauncher.disable(); ZulipAutoLauncher.disable();
} }
}; };
module.exports = {
setAutoLaunch
};

21
app/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "zulip", "name": "zulip",
"version": "3.0.0", "version": "3.1.0-beta",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -260,7 +260,7 @@
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -464,6 +464,11 @@
"dns-packet": "^1.1.0" "dns-packet": "^1.1.0"
} }
}, },
"dotenv": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg=="
},
"duplexer3": { "duplexer3": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -775,7 +780,7 @@
"hashids": { "hashids": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.4.tgz", "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.4.tgz",
"integrity": "sha1-5P+SrWa2hKO9aqznwX1mYY7l+iE=" "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w=="
}, },
"hawk": { "hawk": {
"version": "6.0.2", "version": "6.0.2",
@@ -1094,7 +1099,7 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -1123,9 +1128,9 @@
"integrity": "sha1-ltDNYQ69WNS03pzAxoKM2pnHVI8=" "integrity": "sha1-ltDNYQ69WNS03pzAxoKM2pnHVI8="
}, },
"node-json-db": { "node-json-db": {
"version": "0.9.1", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-0.9.1.tgz", "resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-0.9.2.tgz",
"integrity": "sha512-4BydUI7a10W8QBdHq/J3UBswU1i8WhCgTS4BZU0MjlUKrSU7cuUti71eojistgqe5hIrb4adj/wvAT5dw63NPg==", "integrity": "sha512-vd8A6wznm3hi82IKlVKdomZvsEnASYWl0n4iwLkI7ukbIQkXTtFdJG8aZg0BNbEN+Vr/VktDFSqwWx/iSdf31Q==",
"requires": { "requires": {
"mkdirp": "0.5.x" "mkdirp": "0.5.x"
} }
@@ -1497,7 +1502,7 @@
"spawn-rx": { "spawn-rx": {
"version": "2.0.12", "version": "2.0.12",
"resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.12.tgz", "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.12.tgz",
"integrity": "sha1-tihSlEmUJgib7qDDwewy1/xXo3Y=", "integrity": "sha512-gOPXiQQFQ9lTOLuys0iMn3jfxxv9c7zzwhbYLOEbQGvEShHVJ5sSR1oD3Daj88os7jKArDYT7rbOKdvNhe7iEg==",
"requires": { "requires": {
"debug": "^2.5.1", "debug": "^2.5.1",
"lodash.assign": "^4.2.0", "lodash.assign": "^4.2.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "3.0.0", "version": "3.1.0-beta",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.", "copyright": "Kandra Labs, Inc.",
@@ -30,6 +30,7 @@
"@sentry/electron": "0.14.0", "@sentry/electron": "0.14.0",
"adm-zip": "0.4.11", "adm-zip": "0.4.11",
"auto-launch": "5.0.5", "auto-launch": "5.0.5",
"dotenv": "8.0.0",
"electron-is-dev": "0.3.0", "electron-is-dev": "0.3.0",
"electron-log": "2.2.14", "electron-log": "2.2.14",
"electron-spellchecker": "1.1.2", "electron-spellchecker": "1.1.2",
@@ -37,7 +38,7 @@
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"is-online": "7.0.0", "is-online": "7.0.0",
"node-json-db": "0.9.1", "node-json-db": "0.9.2",
"request": "2.85.0", "request": "2.85.0",
"semver": "5.4.1", "semver": "5.4.1",
"wurl": "2.5.0" "wurl": "2.5.0"

View File

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

View File

@@ -1,9 +1,10 @@
'use strict'; 'use strict';
const Tab = require(__dirname + '/../components/tab.js'); import Tab = require('./tab');
class FunctionalTab extends Tab { class FunctionalTab extends Tab {
template() { $closeButton: Element;
template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
<i class="material-icons">close</i> <i class="material-icons">close</i>
@@ -14,16 +15,22 @@ class FunctionalTab extends Tab {
</div>`; </div>`;
} }
init() { // TODO: Typescript - This type for props should be TabProps
constructor(props: any) {
super(props);
this.init();
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template());
if (this.props.name !== 'Settings') { if (this.props.name !== 'Settings') {
this.props.$root.appendChild(this.$el); this.props.$root.append(this.$el);
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0]; this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0];
this.registerListeners(); this.registerListeners();
} }
} }
registerListeners() { registerListeners(): void {
super.registerListeners(); super.registerListeners();
this.$el.addEventListener('mouseover', () => { this.$el.addEventListener('mouseover', () => {
@@ -34,11 +41,11 @@ class FunctionalTab extends Tab {
this.$closeButton.classList.remove('active'); this.$closeButton.classList.remove('active');
}); });
this.$closeButton.addEventListener('click', e => { this.$closeButton.addEventListener('click', (e: Event) => {
this.props.onDestroy(); this.props.onDestroy();
e.stopPropagation(); e.stopPropagation();
}); });
} }
} }
module.exports = FunctionalTab; export = FunctionalTab;

View File

@@ -1,12 +1,15 @@
const { ipcRenderer } = require('electron'); import { ipcRenderer, remote } from 'electron';
const { shell, app } = require('electron').remote;
const LinkUtil = require('../utils/link-util'); import LinkUtil = require('../utils/link-util');
const DomainUtil = require('../utils/domain-util'); import DomainUtil = require('../utils/domain-util');
const ConfigUtil = require('../utils/config-util'); import ConfigUtil = require('../utils/config-util');
const { shell, app } = remote;
const dingSound = new Audio('../resources/sounds/ding.ogg'); const dingSound = new Audio('../resources/sounds/ding.ogg');
function handleExternalLink(event) { // TODO: TypeScript - Figure out a way to pass correct type here.
function handleExternalLink(this: any, event: any): void {
const { url } = event; const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url; const domainPrefix = DomainUtil.getDomain(this.props.index).url;
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`); const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
@@ -33,11 +36,11 @@ function handleExternalLink(event) {
// and not trigger webview reload while image in webview will // and not trigger webview reload while image in webview will
// do nothing and will not save it // do nothing and will not save it
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream) // Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) { // if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) {
if (!LinkUtil.isImage(url) && isUploadsURL) { if (!LinkUtil.isImage(url) && isUploadsURL) {
ipcRenderer.send('downloadFile', url, downloadPath); ipcRenderer.send('downloadFile', url, downloadPath);
ipcRenderer.once('downloadFileCompleted', (event, filePath, fileName) => { ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', { const downloadNotification = new Notification('Download Complete', {
body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`, body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`,
silent: true // We'll play our own sound - ding.ogg silent: true // We'll play our own sound - ding.ogg
@@ -48,7 +51,7 @@ function handleExternalLink(event) {
dingSound.play(); dingSound.play();
} }
downloadNotification.onclick = () => { downloadNotification.addEventListener('click', () => {
if (shouldShowInFolder) { if (shouldShowInFolder) {
// Reveal file in download folder // Reveal file in download folder
shell.showItemInFolder(filePath); shell.showItemInFolder(filePath);
@@ -56,7 +59,7 @@ function handleExternalLink(event) {
// Open file in the default native app // Open file in the default native app
shell.openItem(filePath); shell.openItem(filePath);
} }
}; });
ipcRenderer.removeAllListeners('downloadFileFailed'); ipcRenderer.removeAllListeners('downloadFileFailed');
}); });
@@ -77,4 +80,4 @@ function handleExternalLink(event) {
} }
} }
module.exports = handleExternalLink; export = handleExternalLink;

View File

@@ -1,12 +1,14 @@
'use strict'; 'use strict';
const Tab = require(__dirname + '/../components/tab.js'); import { ipcRenderer } from 'electron';
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const {ipcRenderer} = require('electron'); import Tab = require('./tab');
import SystemUtil = require('../utils/system-util');
class ServerTab extends Tab { class ServerTab extends Tab {
template() { $badge: Element;
template(): string {
return `<div class="tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div> <div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div> <div class="server-tab-badge"></div>
@@ -17,16 +19,22 @@ class ServerTab extends Tab {
</div>`; </div>`;
} }
init() { // TODO: Typescript - This type for props should be TabProps
super.init(); constructor(props: any) {
super(props);
this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0]; this.init();
} }
updateBadge(count) { init(): void {
if (count > 0) { this.$el = this.generateNodeFromTemplate(this.template());
const formattedCount = count > 999 ? '1K+' : count; this.props.$root.append(this.$el);
this.registerListeners();
this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0];
}
updateBadge(count: number): void {
if (count > 0) {
const formattedCount = count > 999 ? '1K+' : count.toString();
this.$badge.innerHTML = formattedCount; this.$badge.innerHTML = formattedCount;
this.$badge.classList.add('active'); this.$badge.classList.add('active');
} else { } else {
@@ -34,7 +42,7 @@ class ServerTab extends Tab {
} }
} }
generateShortcutText() { generateShortcutText(): string {
// Only provide shortcuts for server [0..10] // Only provide shortcuts for server [0..10]
if (this.props.index >= 10) { if (this.props.index >= 10) {
return ''; return '';
@@ -57,4 +65,4 @@ class ServerTab extends Tab {
} }
} }
module.exports = ServerTab; export = ServerTab;

View File

@@ -1,48 +1,44 @@
'use strict'; 'use strict';
const BaseComponent = require(__dirname + '/../components/base.js'); import WebView = require('./webview');
import BaseComponent = require('./base');
// TODO: TypeScript - Type annotate props
interface TabProps {
[key: string]: any;
}
class Tab extends BaseComponent { class Tab extends BaseComponent {
constructor(props) { props: TabProps;
webview: WebView;
$el: Element;
constructor(props: TabProps) {
super(); super();
this.props = props; this.props = props;
this.webview = this.props.webview; this.webview = this.props.webview;
this.init();
} }
init() { registerListeners(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.registerListeners();
}
registerListeners() {
this.$el.addEventListener('click', this.props.onClick); this.$el.addEventListener('click', this.props.onClick);
this.$el.addEventListener('mouseover', this.props.onHover); this.$el.addEventListener('mouseover', this.props.onHover);
this.$el.addEventListener('mouseout', this.props.onHoverOut); this.$el.addEventListener('mouseout', this.props.onHoverOut);
} }
isLoading() { activate(): void {
return this.webview.isLoading;
}
activate() {
this.$el.classList.add('active'); this.$el.classList.add('active');
this.webview.load(); this.webview.load();
} }
deactivate() { deactivate(): void {
this.$el.classList.remove('active'); this.$el.classList.remove('active');
this.webview.hide(); this.webview.hide();
} }
destroy() { destroy(): void {
this.$el.parentNode.removeChild(this.$el); this.$el.parentNode.removeChild(this.$el);
this.webview.$el.parentNode.removeChild(this.webview.$el); this.webview.$el.parentNode.removeChild(this.webview.$el);
} }
} }
module.exports = Tab; export = Tab;

View File

@@ -1,22 +1,39 @@
'use strict'; 'use strict';
import { remote } from 'electron';
const path = require('path'); import path = require('path');
const fs = require('fs'); import fs = require('fs');
import ConfigUtil = require('../utils/config-util');
import SystemUtil = require('../utils/system-util');
import BaseComponent = require('../components/base');
import handleExternalLink = require('../components/handle-external-link');
const ConfigUtil = require(__dirname + '/../utils/config-util.js'); const { app, dialog } = remote;
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const { app, dialog } = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js');
const handleExternalLink = require(__dirname + '/../components/handle-external-link.js');
const shouldSilentWebview = ConfigUtil.getConfigItem('silent'); const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
// TODO: TypeScript - Type annotate WebViewProps.
interface WebViewProps {
[key: string]: any;
}
class WebView extends BaseComponent { class WebView extends BaseComponent {
constructor(props) { props: any;
zoomFactor: number;
badgeCount: number;
loading: boolean;
customCSS: string;
$webviewsContainer: DOMTokenList;
$el: Electron.WebviewTag;
// This is required because in main.js we access WebView.method as
// webview[method].
[key: string]: any;
constructor(props: WebViewProps) {
super(); super();
this.props = props; this.props = props;
this.zoomFactor = 1.0; this.zoomFactor = 1.0;
this.loading = true; this.loading = true;
this.badgeCount = 0; this.badgeCount = 0;
@@ -24,7 +41,7 @@ class WebView extends BaseComponent {
this.$webviewsContainer = document.querySelector('#webviews-container').classList; this.$webviewsContainer = document.querySelector('#webviews-container').classList;
} }
template() { template(): string {
return `<webview return `<webview
class="disabled" class="disabled"
data-tab-id="${this.props.tabIndex}" data-tab-id="${this.props.tabIndex}"
@@ -38,14 +55,14 @@ class WebView extends BaseComponent {
</webview>`; </webview>`;
} }
init() { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
this.props.$root.appendChild(this.$el); this.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
} }
registerListeners() { registerListeners(): void {
this.$el.addEventListener('new-window', event => { this.$el.addEventListener('new-window', event => {
handleExternalLink.call(this, event); handleExternalLink.call(this, event);
}); });
@@ -105,7 +122,7 @@ class WebView extends BaseComponent {
this.$el.addEventListener('did-fail-load', event => { this.$el.addEventListener('did-fail-load', event => {
const { errorDescription } = event; const { errorDescription } = event;
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0); const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) { if (hasConnectivityErr) {
console.error('error', errorDescription); console.error('error', errorDescription);
this.props.onNetworkError(); this.props.onNetworkError();
@@ -130,12 +147,12 @@ class WebView extends BaseComponent {
}); });
} }
getBadgeCount(title) { getBadgeCount(title: string): number {
const messageCountInTitle = (/\(([0-9]+)\)/).exec(title); const messageCountInTitle = (/\((\d+)\)/).exec(title);
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0; return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
} }
show() { show(): void {
// Do not show WebView if another tab was selected and this tab should be in background. // Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) { if (!this.props.isActive()) {
return; return;
@@ -176,7 +193,7 @@ class WebView extends BaseComponent {
} }
} }
focus() { focus(): void {
// focus Webview and it's contents when Window regain focus. // focus Webview and it's contents when Window regain focus.
const webContents = this.$el.getWebContents(); const webContents = this.$el.getWebContents();
// HACK: webContents.isFocused() seems to be true even without the element // HACK: webContents.isFocused() seems to be true even without the element
@@ -185,18 +202,18 @@ class WebView extends BaseComponent {
// HACK: Looks like blur needs to be called on the previously focused // HACK: Looks like blur needs to be called on the previously focused
// element to transfer focus correctly, in Electron v3.0.10 // element to transfer focus correctly, in Electron v3.0.10
// See https://github.com/electron/electron/issues/15718 // See https://github.com/electron/electron/issues/15718
document.activeElement.blur(); (document.activeElement as HTMLElement).blur();
this.$el.focus(); this.$el.focus();
webContents.focus(); webContents.focus();
} }
} }
hide() { hide(): void {
this.$el.classList.add('disabled'); this.$el.classList.add('disabled');
this.$el.classList.remove('active'); this.$el.classList.remove('active');
} }
load() { load(): void {
if (this.$el) { if (this.$el) {
this.show(); this.show();
} else { } else {
@@ -204,41 +221,41 @@ class WebView extends BaseComponent {
} }
} }
zoomIn() { zoomIn(): void {
this.zoomFactor += 0.1; this.zoomFactor += 0.1;
this.$el.setZoomFactor(this.zoomFactor); this.$el.setZoomFactor(this.zoomFactor);
} }
zoomOut() { zoomOut(): void {
this.zoomFactor -= 0.1; this.zoomFactor -= 0.1;
this.$el.setZoomFactor(this.zoomFactor); this.$el.setZoomFactor(this.zoomFactor);
} }
zoomActualSize() { zoomActualSize(): void {
this.zoomFactor = 1.0; this.zoomFactor = 1.0;
this.$el.setZoomFactor(this.zoomFactor); this.$el.setZoomFactor(this.zoomFactor);
} }
logOut() { logOut(): void {
this.$el.executeJavaScript('logout()'); this.$el.executeJavaScript('logout()');
} }
showShortcut() { showShortcut(): void {
this.$el.executeJavaScript('shortcut()'); this.$el.executeJavaScript('shortcut()');
} }
openDevTools() { openDevTools(): void {
this.$el.openDevTools(); this.$el.openDevTools();
} }
back() { back(): void {
if (this.$el.canGoBack()) { if (this.$el.canGoBack()) {
this.$el.goBack(); this.$el.goBack();
this.focus(); this.focus();
} }
} }
canGoBackButton() { canGoBackButton(): void {
const $backButton = document.querySelector('#actions-container #back-action'); const $backButton = document.querySelector('#actions-container #back-action');
if (this.$el.canGoBack()) { if (this.$el.canGoBack()) {
$backButton.classList.remove('disable'); $backButton.classList.remove('disable');
@@ -247,13 +264,13 @@ class WebView extends BaseComponent {
} }
} }
forward() { forward(): void {
if (this.$el.canGoForward()) { if (this.$el.canGoForward()) {
this.$el.goForward(); this.$el.goForward();
} }
} }
reload() { reload(): void {
this.hide(); this.hide();
// Shows the loading indicator till the webview is reloaded // Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove('loaded'); this.$webviewsContainer.remove('loaded');
@@ -261,9 +278,9 @@ class WebView extends BaseComponent {
this.$el.reload(); this.$el.reload();
} }
send(...param) { send(channel: string, ...param: any[]): void {
this.$el.send(...param); this.$el.send(channel, ...param);
} }
} }
module.exports = WebView; export = WebView;

View File

@@ -1,17 +1,20 @@
const events = require('events'); import { ipcRenderer } from 'electron';
const { ipcRenderer } = require('electron');
import events = require('events');
type ListenerType = ((...args: any[]) => void);
// we have and will have some non camelcase stuff // we have and will have some non camelcase stuff
// while working with zulip so just turning the rule off // while working with zulip so just turning the rule off
// for the whole file. // for the whole file.
/* eslint-disable camelcase */ /* eslint-disable @typescript-eslint/camelcase */
class ElectronBridge extends events { class ElectronBridge extends events {
send_event(...args) { send_event(eventName: string | symbol, ...args: any[]): void {
this.emit(...args); this.emit(eventName, ...args);
} }
on_event(...args) { on_event(eventName: string, listener: ListenerType): void {
this.on(...args); this.on(eventName, listener);
} }
} }
@@ -37,4 +40,6 @@ electron_bridge.on('realm_icon_url', iconURL => {
// functions zulip side will emit event using ElectronBrigde.send_event // 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 // which is alias of .emit and on this side we can handle the data by adding
// a listener for the event. // a listener for the event.
module.exports = electron_bridge; export = electron_bridge;
/* eslint-enable @typescript-eslint/camelcase */

View File

@@ -1,7 +1,16 @@
const { app } = require('electron').remote; import { remote } from 'electron';
const path = require('path'); import SendFeedback from '@electron-elements/send-feedback';
const fs = require('fs');
const SendFeedback = require('@electron-elements/send-feedback'); import path = require('path');
import fs = require('fs');
const { app } = remote;
interface SendFeedback extends HTMLElement {
[key: string]: any;
}
type SendFeedbackType = SendFeedback;
// make the button color match zulip app's theme // make the button color match zulip app's theme
SendFeedback.customStyles = ` SendFeedback.customStyles = `
@@ -22,8 +31,8 @@ button {
`; `;
customElements.define('send-feedback', SendFeedback); customElements.define('send-feedback', SendFeedback);
const sendFeedback = document.querySelector('send-feedback'); export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback');
const feedbackHolder = sendFeedback.parentElement; export const feedbackHolder = sendFeedback.parentElement;
// customize the fields of custom elements // customize the fields of custom elements
sendFeedback.title = 'Report Issue'; sendFeedback.title = 'Report Issue';
@@ -38,9 +47,9 @@ sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com' email: 'akash@zulipchat.com'
}); });
feedbackHolder.addEventListener('click', e => { feedbackHolder.addEventListener('click', (e: Event) => {
// only remove the class if the grey out faded // only remove the class if the grey out faded
// part is clicked and not the feedback element itself // part is clicked and not the feedback element itself
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
feedbackHolder.classList.remove('show'); feedbackHolder.classList.remove('show');
} }
@@ -55,8 +64,3 @@ sendFeedback.addEventListener('feedback-submitted', () => {
const dataDir = app.getPath('userData'); const dataDir = app.getPath('userData');
const logsDir = path.join(dataDir, '/Logs'); const logsDir = path.join(dataDir, '/Logs');
sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file))); sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));
module.exports = {
feedbackHolder,
sendFeedback
};

View File

@@ -1,53 +1,127 @@
'use strict'; 'use strict';
const { ipcRenderer, remote, clipboard, shell } = require('electron'); import { ipcRenderer, remote, clipboard, shell } from 'electron';
const isDev = require('electron-is-dev'); import { feedbackHolder } from './feedback';
import path = require('path');
import escape = require('escape-html');
import isDev = require('electron-is-dev');
const { session, app, Menu, dialog } = remote; const { session, app, Menu, dialog } = remote;
const escape = require('escape-html');
require(__dirname + '/js/tray.js'); // eslint-disable-next-line import/no-unassigned-import
const DomainUtil = require(__dirname + '/js/utils/domain-util.js'); require('./tray');
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');
const DNDUtil = require(__dirname + '/js/utils/dnd-util.js');
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
const Logger = require(__dirname + '/js/utils/logger-util.js');
const CommonUtil = require(__dirname + '/js/utils/common-util.js');
const { feedbackHolder } = require(__dirname + '/js/feedback.js'); import DomainUtil = require('./utils/domain-util');
import WebView = require('./components/webview');
import ServerTab = require('./components/server-tab');
import FunctionalTab = require('./components/functional-tab');
import ConfigUtil = require('./utils/config-util');
import DNDUtil = require('./utils/dnd-util');
import ReconnectUtil = require('./utils/reconnect-util');
import Logger = require('./utils/logger-util');
import CommonUtil = require('./utils/common-util');
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
timestamp: true timestamp: true
}); });
class ServerManagerView { interface FunctionalTabProps {
constructor() { name: string;
this.$addServerButton = document.getElementById('add-tab'); materialIcon: string;
this.$tabsContainer = document.getElementById('tabs-container'); url: string;
}
const $actionsContainer = document.getElementById('actions-container'); interface AnyObject {
[key: string]: any;
}
interface SettingsOptions {
autoHideMenubar: boolean;
trayIcon: boolean;
useManualProxy: boolean;
useSystemProxy: boolean;
showSidebar: boolean;
badgeOption: boolean;
startAtLogin: boolean;
startMinimized: boolean;
enableSpellchecker: boolean;
showNotification: boolean;
autoUpdate: boolean;
betaUpdate: boolean;
errorReporting: boolean;
customCSS: boolean;
silent: boolean;
lastActiveTab: number;
dnd: boolean;
dndPreviousSettings: {
showNotification: boolean;
silent: boolean;
flashTaskbarOnMessage?: boolean;
};
downloadsPath: string;
showDownloadFolder: boolean;
flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean;
loading?: AnyObject;
}
const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
class ServerManagerView {
$addServerButton: HTMLButtonElement;
$tabsContainer: Element;
$reloadButton: HTMLButtonElement;
$loadingIndicator: HTMLButtonElement;
$settingsButton: HTMLButtonElement;
$webviewsContainer: Element;
$backButton: HTMLButtonElement;
$dndButton: HTMLButtonElement;
$addServerTooltip: HTMLElement;
$reloadTooltip: HTMLElement;
$loadingTooltip: HTMLElement;
$settingsTooltip: HTMLElement;
$serverIconTooltip: HTMLCollectionOf<HTMLElement>;
$backTooltip: HTMLElement;
$dndTooltip: HTMLElement;
$sidebar: Element;
$fullscreenPopup: Element;
$fullscreenEscapeKey: string;
loading: AnyObject;
activeTabIndex: number;
tabs: ServerOrFunctionalTab[];
functionalTabs: AnyObject;
tabIndex: number;
constructor() {
this.$addServerButton = document.querySelector('#add-tab');
this.$tabsContainer = document.querySelector('#tabs-container');
const $actionsContainer = document.querySelector('#actions-container');
this.$reloadButton = $actionsContainer.querySelector('#reload-action'); this.$reloadButton = $actionsContainer.querySelector('#reload-action');
this.$loadingIndicator = $actionsContainer.querySelector('#loading-action'); this.$loadingIndicator = $actionsContainer.querySelector('#loading-action');
this.$settingsButton = $actionsContainer.querySelector('#settings-action'); this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container'); this.$webviewsContainer = document.querySelector('#webviews-container');
this.$backButton = $actionsContainer.querySelector('#back-action'); this.$backButton = $actionsContainer.querySelector('#back-action');
this.$dndButton = $actionsContainer.querySelector('#dnd-action'); this.$dndButton = $actionsContainer.querySelector('#dnd-action');
this.$addServerTooltip = document.getElementById('add-server-tooltip'); this.$addServerTooltip = document.querySelector('#add-server-tooltip');
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip'); this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
this.$loadingTooltip = $actionsContainer.querySelector('#loading-tooltip'); this.$loadingTooltip = $actionsContainer.querySelector('#loading-tooltip');
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip'); this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip');
// TODO: This should have been querySelector but the problem is that
// querySelector doesn't return elements not present in dom whereas somehow
// getElementsByClassName does. To fix this we need to call this after this.initTabs
// is called in this.init.
// eslint-disable-next-line unicorn/prefer-query-selector
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip') as HTMLCollectionOf<HTMLElement>;
this.$backTooltip = $actionsContainer.querySelector('#back-tooltip'); this.$backTooltip = $actionsContainer.querySelector('#back-tooltip');
this.$dndTooltip = $actionsContainer.querySelector('#dnd-tooltip'); this.$dndTooltip = $actionsContainer.querySelector('#dnd-tooltip');
this.$sidebar = document.getElementById('sidebar'); this.$sidebar = document.querySelector('#sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup'); this.$fullscreenPopup = document.querySelector('#fullscreen-popup');
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11'; this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`; this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
@@ -58,7 +132,7 @@ class ServerManagerView {
this.tabIndex = 0; this.tabIndex = 0;
} }
init() { init(): void {
this.loadProxy().then(() => { this.loadProxy().then(() => {
this.initSidebar(); this.initSidebar();
this.initTabs(); this.initTabs();
@@ -68,7 +142,7 @@ class ServerManagerView {
}); });
} }
loadProxy() { loadProxy(): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
// To change proxyEnable to useManualProxy in older versions // To change proxyEnable to useManualProxy in older versions
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy'); const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
@@ -100,9 +174,9 @@ class ServerManagerView {
// Settings are initialized only when user clicks on General/Server/Network section settings // Settings are initialized only when user clicks on General/Server/Network section settings
// In case, user doesn't visit these section, those values set to be null automatically // In case, user doesn't visit these section, those values set to be null automatically
// This will make sure the default settings are correctly set to either true or false // This will make sure the default settings are correctly set to either true or false
initDefaultSettings() { initDefaultSettings(): void {
// Default settings which should be respected // Default settings which should be respected
const settingOptions = { const settingOptions: SettingsOptions = {
autoHideMenubar: false, autoHideMenubar: false,
trayIcon: true, trayIcon: true,
useManualProxy: false, useManualProxy: false,
@@ -146,18 +220,19 @@ class ServerManagerView {
} }
for (const i in settingOptions) { for (const i in settingOptions) {
const setting = i as keyof SettingsOptions;
if (ConfigUtil.getConfigItem(i) === null) { if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[i]); ConfigUtil.setConfigItem(i, settingOptions[setting]);
} }
} }
} }
initSidebar() { initSidebar(): void {
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true); const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
this.toggleSidebar(showSidebar); this.toggleSidebar(showSidebar);
} }
initTabs() { initTabs(): void {
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
if (servers.length > 0) { if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) { for (let i = 0; i < servers.length; i++) {
@@ -174,7 +249,7 @@ class ServerManagerView {
} }
} }
initServer(server, index) { initServer(server: any, index: number): void {
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
@@ -196,7 +271,7 @@ class ServerManagerView {
isActive: () => { isActive: () => {
return index === this.activeTabIndex; return index === this.activeTabIndex;
}, },
switchLoading: (loading, url) => { switchLoading: (loading: boolean, url: string) => {
if (!loading && this.loading[url]) { if (!loading && this.loading[url]) {
this.loading[url] = false; this.loading[url] = false;
} else if (loading && !this.loading[url]) { } else if (loading && !this.loading[url]) {
@@ -213,14 +288,14 @@ class ServerManagerView {
this.loading[server.url] = true; this.loading[server.url] = true;
} }
initActions() { initActions(): void {
this.initDNDButton(); this.initDNDButton();
this.initServerActions(); this.initServerActions();
this.initLeftSidebarEvents(); this.initLeftSidebarEvents();
} }
initServerActions() { initServerActions(): void {
const $serverImgs = document.querySelectorAll('.server-icons'); const $serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll('.server-icons');
$serverImgs.forEach(($serverImg, index) => { $serverImgs.forEach(($serverImg, index) => {
this.addContextMenu($serverImg, index); this.addContextMenu($serverImg, index);
if ($serverImg.src.includes('img/icon.png')) { if ($serverImg.src.includes('img/icon.png')) {
@@ -232,7 +307,7 @@ class ServerManagerView {
}); });
} }
initLeftSidebarEvents() { initLeftSidebarEvents(): void {
this.$dndButton.addEventListener('click', () => { this.$dndButton.addEventListener('click', () => {
const dndUtil = DNDUtil.toggle(); const dndUtil = DNDUtil.toggle();
ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings); ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
@@ -258,22 +333,22 @@ class ServerManagerView {
this.sidebarHoverEvent(this.$dndButton, this.$dndTooltip); this.sidebarHoverEvent(this.$dndButton, this.$dndTooltip);
} }
initDNDButton() { initDNDButton(): void {
const dnd = ConfigUtil.getConfigItem('dnd', false); const dnd = ConfigUtil.getConfigItem('dnd', false);
this.toggleDNDButton(dnd); this.toggleDNDButton(dnd);
} }
getTabIndex() { getTabIndex(): number {
const currentIndex = this.tabIndex; const currentIndex = this.tabIndex;
this.tabIndex++; this.tabIndex++;
return currentIndex; return currentIndex;
} }
getCurrentActiveServer() { getCurrentActiveServer(): string {
return this.tabs[this.activeTabIndex].webview.props.url; return this.tabs[this.activeTabIndex].webview.props.url;
} }
displayInitialCharLogo($img, index) { displayInitialCharLogo($img: HTMLImageElement, index: number): void {
/* /*
index parameter needed because webview[data-tab-id] can increment index parameter needed because webview[data-tab-id] can increment
beyond size of sidebar org array and throw error beyond size of sidebar org array and throw error
@@ -296,12 +371,12 @@ class ServerManagerView {
$altIcon.classList.add('alt-icon'); $altIcon.classList.add('alt-icon');
$parent.removeChild($img); $parent.removeChild($img);
$parent.appendChild($altIcon); $parent.append($altIcon);
this.addContextMenu($altIcon, index); this.addContextMenu($altIcon as HTMLImageElement, index);
} }
sidebarHoverEvent(SidebarButton, SidebarTooltip, addServer = false) { sidebarHoverEvent(SidebarButton: HTMLButtonElement, SidebarTooltip: HTMLElement, addServer = false): void {
SidebarButton.addEventListener('mouseover', () => { SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style'); SidebarTooltip.removeAttribute('style');
// To handle position of add server tooltip due to scrolling of list of organizations // To handle position of add server tooltip due to scrolling of list of organizations
@@ -318,7 +393,7 @@ class ServerManagerView {
}); });
} }
onHover(index) { onHover(index: number): void {
// this.$serverIconTooltip[index].innerHTML already has realm name, so we are just // this.$serverIconTooltip[index].innerHTML already has realm name, so we are just
// removing the style. // removing the style.
this.$serverIconTooltip[index].removeAttribute('style'); this.$serverIconTooltip[index].removeAttribute('style');
@@ -329,11 +404,11 @@ class ServerManagerView {
this.$serverIconTooltip[index].style.top = top + 'px'; this.$serverIconTooltip[index].style.top = top + 'px';
} }
onHoverOut(index) { onHoverOut(index: number): void {
this.$serverIconTooltip[index].style.display = 'none'; this.$serverIconTooltip[index].style.display = 'none';
} }
openFunctionalTab(tabProps) { openFunctionalTab(tabProps: FunctionalTabProps): void {
if (this.functionalTabs[tabProps.name] !== undefined) { if (this.functionalTabs[tabProps.name] !== undefined) {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
return; return;
@@ -362,7 +437,7 @@ class ServerManagerView {
isActive: () => { isActive: () => {
return this.functionalTabs[tabProps.name] === this.activeTabIndex; return this.functionalTabs[tabProps.name] === this.activeTabIndex;
}, },
switchLoading: (loading, url) => { switchLoading: (loading: AnyObject, url: string) => {
if (!loading && this.loading[url]) { if (!loading && this.loading[url]) {
this.loading[url] = false; this.loading[url] = false;
} else if (loading && !this.loading[url]) { } else if (loading && !this.loading[url]) {
@@ -384,46 +459,46 @@ class ServerManagerView {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
} }
openSettings(nav = 'General') { openSettings(nav = 'General'): void {
this.openFunctionalTab({ this.openFunctionalTab({
name: 'Settings', name: 'Settings',
materialIcon: 'settings', materialIcon: 'settings',
url: `file://${__dirname}/preference.html#${nav}` url: `file://${rendererDirectory}/preference.html#${nav}`
}); });
this.$settingsButton.classList.add('active'); this.$settingsButton.classList.add('active');
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav); this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
} }
openAbout() { openAbout(): void {
this.openFunctionalTab({ this.openFunctionalTab({
name: 'About', name: 'About',
materialIcon: 'sentiment_very_satisfied', materialIcon: 'sentiment_very_satisfied',
url: `file://${__dirname}/about.html` url: `file://${rendererDirectory}/about.html`
}); });
} }
openNetworkTroubleshooting() { openNetworkTroubleshooting(): void {
this.openFunctionalTab({ this.openFunctionalTab({
name: 'Network Troubleshooting', name: 'Network Troubleshooting',
materialIcon: 'network_check', materialIcon: 'network_check',
url: `file://${__dirname}/network.html` url: `file://${rendererDirectory}/network.html`
}); });
} }
activateLastTab(index) { activateLastTab(index: number): void {
// Open all the tabs in background, also activate the tab based on the index // Open all the tabs in background, also activate the tab based on the index
this.activateTab(index); this.activateTab(index);
// Save last active tab // Save last active tab via main process to avoid JSON DB errors
ConfigUtil.setConfigItem('lastActiveTab', index); ipcRenderer.send('save-last-tab', index);
} }
// returns this.tabs in an way that does // returns this.tabs in an way that does
// not crash app when this.tabs is passed into // not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview // ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash. // properties in ServerTab causes the app to crash.
get tabsForIpc() { get tabsForIpc(): ServerOrFunctionalTab[] {
const tabs = []; const tabs: ServerOrFunctionalTab[] = [];
this.tabs.forEach(tab => { this.tabs.forEach((tab: ServerOrFunctionalTab) => {
const proto = Object.create(Object.getPrototypeOf(tab)); const proto = Object.create(Object.getPrototypeOf(tab));
const tabClone = Object.assign(proto, tab); const tabClone = Object.assign(proto, tab);
@@ -436,7 +511,7 @@ class ServerManagerView {
return tabs; return tabs;
} }
activateTab(index, hideOldTab = true) { activateTab(index: number, hideOldTab = true): void {
if (!this.tabs[index]) { if (!this.tabs[index]) {
return; return;
} }
@@ -473,7 +548,7 @@ class ServerManagerView {
}); });
} }
showLoading(loading) { showLoading(loading: boolean): void {
if (!loading) { if (!loading) {
this.$reloadButton.removeAttribute('style'); this.$reloadButton.removeAttribute('style');
this.$loadingIndicator.style.display = 'none'; this.$loadingIndicator.style.display = 'none';
@@ -483,7 +558,7 @@ class ServerManagerView {
} }
} }
destroyTab(name, index) { destroyTab(name: string, index: number): void {
if (this.tabs[index].webview.loading) { if (this.tabs[index].webview.loading) {
return; return;
} }
@@ -499,7 +574,7 @@ class ServerManagerView {
} }
} }
destroyView() { destroyView(): void {
// Show loading indicator // Show loading indicator
this.$webviewsContainer.classList.remove('loaded'); this.$webviewsContainer.classList.remove('loaded');
@@ -513,7 +588,7 @@ class ServerManagerView {
this.$webviewsContainer.innerHTML = ''; this.$webviewsContainer.innerHTML = '';
} }
reloadView() { reloadView(): void {
// Save and remember the index of last active tab so that we can use it later // Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index; const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab); ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
@@ -526,33 +601,33 @@ class ServerManagerView {
// This will trigger when pressed CTRL/CMD + R [WIP] // This will trigger when pressed CTRL/CMD + R [WIP]
// It won't reload the current view properly when you add/delete a server. // It won't reload the current view properly when you add/delete a server.
reloadCurrentView() { reloadCurrentView(): void {
this.$reloadButton.click(); this.$reloadButton.click();
} }
updateBadge() { updateBadge(): void {
let messageCountAll = 0; let messageCountAll = 0;
for (let i = 0; i < this.tabs.length; i++) { for (const tab of this.tabs) {
if (this.tabs[i] && this.tabs[i].updateBadge) { if (tab && tab instanceof ServerTab && tab.updateBadge) {
const count = this.tabs[i].webview.badgeCount; const count = tab.webview.badgeCount;
messageCountAll += count; messageCountAll += count;
this.tabs[i].updateBadge(count); tab.updateBadge(count);
} }
} }
ipcRenderer.send('update-badge', messageCountAll); ipcRenderer.send('update-badge', messageCountAll);
} }
updateGeneralSettings(setting, value) { updateGeneralSettings(setting: string, value: any): void {
const selector = 'webview:not([class*=disabled])'; const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector); const webview: Electron.WebviewTag = document.querySelector(selector);
if (webview) { if (webview) {
const webContents = webview.getWebContents(); const webContents = webview.getWebContents();
webContents.send(setting, value); webContents.send(setting, value);
} }
} }
toggleSidebar(show) { toggleSidebar(show: boolean): void {
if (show) { if (show) {
this.$sidebar.classList.remove('sidebar-hide'); this.$sidebar.classList.remove('sidebar-hide');
} else { } else {
@@ -561,12 +636,12 @@ class ServerManagerView {
} }
// Toggles the dnd button icon. // Toggles the dnd button icon.
toggleDNDButton(alert) { toggleDNDButton(alert: boolean): void {
this.$dndTooltip.textContent = (alert ? 'Disable' : 'Enable') + ' Do Not Disturb'; this.$dndTooltip.textContent = (alert ? 'Disable' : 'Enable') + ' Do Not Disturb';
this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications'; this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications';
} }
addContextMenu($serverImg, index) { addContextMenu($serverImg: HTMLImageElement, index: number): void {
$serverImg.addEventListener('contextmenu', e => { $serverImg.addEventListener('contextmenu', e => {
e.preventDefault(); e.preventDefault();
const template = [ const template = [
@@ -598,8 +673,8 @@ class ServerManagerView {
}); });
} }
registerIpcs() { registerIpcs(): void {
const webviewListeners = { const webviewListeners: AnyObject = {
'webview-reload': 'reload', 'webview-reload': 'reload',
back: 'back', back: 'back',
focus: 'focus', focus: 'focus',
@@ -616,12 +691,12 @@ class ServerManagerView {
ipcRenderer.on(key, () => { ipcRenderer.on(key, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview; const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) { if (activeWebview) {
activeWebview[webviewListeners[key]](); activeWebview[webviewListeners[key] as string]();
} }
}); });
} }
ipcRenderer.on('open-settings', (event, settingNav) => { ipcRenderer.on('open-settings', (event: Event, settingNav: string) => {
this.openSettings(settingNav); this.openSettings(settingNav);
}); });
@@ -645,7 +720,7 @@ class ServerManagerView {
ipcRenderer.send('clear-app-settings'); ipcRenderer.send('clear-app-settings');
}); });
ipcRenderer.on('switch-server-tab', (event, index) => { ipcRenderer.on('switch-server-tab', (event: Event, index: number) => {
this.activateLastTab(index); this.activateLastTab(index);
}); });
@@ -653,7 +728,7 @@ class ServerManagerView {
this.openSettings('AddServer'); this.openSettings('AddServer');
}); });
ipcRenderer.on('reload-proxy', (event, showAlert) => { ipcRenderer.on('reload-proxy', (event: Event, showAlert: boolean) => {
this.loadProxy().then(() => { this.loadProxy().then(() => {
if (showAlert) { if (showAlert) {
alert('Proxy settings saved!'); alert('Proxy settings saved!');
@@ -662,7 +737,7 @@ class ServerManagerView {
}); });
}); });
ipcRenderer.on('toggle-sidebar', (event, show) => { ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => {
// Toggle the left sidebar // Toggle the left sidebar
this.toggleSidebar(show); this.toggleSidebar(show);
@@ -670,8 +745,8 @@ class ServerManagerView {
this.updateGeneralSettings('toggle-sidebar-setting', show); this.updateGeneralSettings('toggle-sidebar-setting', show);
}); });
ipcRenderer.on('toggle-silent', (event, state) => { ipcRenderer.on('toggle-silent', (event: Event, state: boolean) => {
const webviews = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { webviews.forEach(webview => {
try { try {
webview.setAudioMuted(state); webview.setAudioMuted(state);
@@ -684,7 +759,7 @@ class ServerManagerView {
}); });
}); });
ipcRenderer.on('toggle-autohide-menubar', (event, autoHideMenubar, updateMenu) => { ipcRenderer.on('toggle-autohide-menubar', (event: Event, autoHideMenubar: boolean, updateMenu: boolean) => {
if (updateMenu) { if (updateMenu) {
ipcRenderer.send('update-menu', { ipcRenderer.send('update-menu', {
tabs: this.tabsForIpc, tabs: this.tabsForIpc,
@@ -695,17 +770,18 @@ class ServerManagerView {
this.updateGeneralSettings('toggle-menubar-setting', autoHideMenubar); this.updateGeneralSettings('toggle-menubar-setting', autoHideMenubar);
}); });
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => { ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => {
this.toggleDNDButton(state); this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent); ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])'; const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector); const webview: Electron.WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents(); const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings); webContents.send('toggle-dnd', state, newSettings);
}); });
ipcRenderer.on('update-realm-name', (event, serverURL, realmName) => { ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => {
DomainUtil.getDomains().forEach((domain, index) => { // TODO: TypeScript - Type annotate getDomains() or this domain paramter.
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
const serverTooltipSelector = `.tab .server-tooltip`; const serverTooltipSelector = `.tab .server-tooltip`;
const serverTooltips = document.querySelectorAll(serverTooltipSelector); const serverTooltips = document.querySelectorAll(serverTooltipSelector);
@@ -725,12 +801,13 @@ class ServerManagerView {
}); });
}); });
ipcRenderer.on('update-realm-icon', (event, serverURL, iconURL) => { ipcRenderer.on('update-realm-icon', (event: Event, serverURL: string, iconURL: string) => {
DomainUtil.getDomains().forEach((domain, index) => { // TODO: TypeScript - Type annotate getDomains() or this domain paramter.
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
DomainUtil.saveServerIcon(iconURL).then(localIconUrl => { DomainUtil.saveServerIcon(iconURL).then((localIconUrl: string) => {
const serverImgsSelector = `.tab .server-icons`; const serverImgsSelector = `.tab .server-icons`;
const serverImgs = document.querySelectorAll(serverImgsSelector); const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl; serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl; domain.icon = localIconUrl;
@@ -750,21 +827,21 @@ class ServerManagerView {
this.$fullscreenPopup.classList.remove('show'); this.$fullscreenPopup.classList.remove('show');
}); });
ipcRenderer.on('focus-webview-with-id', (event, webviewId) => { ipcRenderer.on('focus-webview-with-id', (event: Event, webviewId: number) => {
const webviews = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { webviews.forEach(webview => {
const currentId = webview.getWebContents().id; const currentId = webview.getWebContents().id;
const tabId = webview.getAttribute('data-tab-id'); const tabId = webview.getAttribute('data-tab-id');
const concurrentTab = document.querySelector(`div[data-tab-id="${tabId}"]`); const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${tabId}"]`);
if (currentId === webviewId) { if (currentId === webviewId) {
concurrentTab.click(); concurrentTab.click();
} }
}); });
}); });
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => { ipcRenderer.on('render-taskbar-icon', (event: Event, messageCount: number) => {
// Create a canvas from unread messagecounts // Create a canvas from unread messagecounts
function createOverlayIcon(messageCount) { function createOverlayIcon(messageCount: number): HTMLCanvasElement {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.height = 128; canvas.height = 128;
canvas.width = 128; canvas.width = 128;
@@ -805,7 +882,7 @@ class ServerManagerView {
} }
} }
window.onload = () => { window.addEventListener('load', () => {
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView); const reconnectUtil = new ReconnectUtil(serverManagerView);
serverManagerView.init(); serverManagerView.init();
@@ -824,7 +901,8 @@ window.onload = () => {
// `--no-electron-connect` // `--no-electron-connect`
const mainProcessArgv = remote.getGlobal('process').argv; const mainProcessArgv = remote.getGlobal('process').argv;
if (isDev && !mainProcessArgv.includes('--no-electron-connect')) { if (isDev && !mainProcessArgv.includes('--no-electron-connect')) {
const electronConnect = require('electron-connect'); require('electron-connect').client.create();
electronConnect.client.create();
} }
}; });
export = new ServerManagerView();

View File

@@ -1,18 +1,27 @@
'use strict'; 'use strict';
import { ipcRenderer } from 'electron';
const { ipcRenderer } = require('electron'); import {
const url = require('url');
const MacNotifier = require('node-mac-notifier');
const ConfigUtil = require('../utils/config-util');
const {
appId, customReply, focusCurrentServer, parseReply, setupReply appId, customReply, focusCurrentServer, parseReply, setupReply
} = require('./helpers'); } from './helpers';
import url = require('url');
import MacNotifier = require('node-mac-notifier');
import ConfigUtil = require('../utils/config-util');
type ReplyHandler = (response: string) => void;
type ClickHandler = () => void;
let replyHandler: ReplyHandler;
let clickHandler: ClickHandler;
interface NotificationHandlerArgs {
response: string;
}
let replyHandler;
let clickHandler;
class DarwinNotification { class DarwinNotification {
constructor(title, opts) { tag: string;
const silent = ConfigUtil.getConfigItem('silent') || false;
constructor(title: string, opts: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const { host, protocol } = location; const { host, protocol } = location;
const { icon } = opts; const { icon } = opts;
const profilePic = url.resolve(`${protocol}//${host}`, icon); const profilePic = url.resolve(`${protocol}//${host}`, icon);
@@ -39,45 +48,45 @@ class DarwinNotification {
notification.addEventListener('reply', this.notificationHandler); notification.addEventListener('reply', this.notificationHandler);
} }
static requestPermission() { static requestPermission(): void {
return; // eslint-disable-line no-useless-return return; // eslint-disable-line no-useless-return
} }
// Override default Notification permission // Override default Notification permission
static get permission() { static get permission(): NotificationPermission {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
set onreply(handler) { set onreply(handler: ReplyHandler) {
replyHandler = handler; replyHandler = handler;
} }
get onreply() { get onreply(): ReplyHandler {
return replyHandler; return replyHandler;
} }
set onclick(handler) { set onclick(handler: ClickHandler) {
clickHandler = handler; clickHandler = handler;
} }
get onclick() { get onclick(): ClickHandler {
return clickHandler; return clickHandler;
} }
// not something that is common or // not something that is common or
// used by zulip server but added to be // used by zulip server but added to be
// future proff. // future proff.
addEventListener(event, handler) { addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
if (event === 'click') { if (event === 'click') {
clickHandler = handler; clickHandler = handler as ClickHandler;
} }
if (event === 'reply') { if (event === 'reply') {
replyHandler = handler; replyHandler = handler as ReplyHandler;
} }
} }
notificationHandler({ response }) { notificationHandler({ response }: NotificationHandlerArgs): void {
response = parseReply(response); response = parseReply(response);
focusCurrentServer(); focusCurrentServer();
setupReply(this.tag); setupReply(this.tag);
@@ -92,7 +101,7 @@ class DarwinNotification {
// method specific to notification api // method specific to notification api
// used by zulip // used by zulip
close() { close(): void {
return; // eslint-disable-line no-useless-return return; // eslint-disable-line no-useless-return
} }
} }

View File

@@ -1,12 +1,13 @@
'use strict'; 'use strict';
const { ipcRenderer } = require('electron'); import { ipcRenderer } from 'electron';
const ConfigUtil = require('../utils/config-util'); import { focusCurrentServer } from './helpers';
const { focusCurrentServer } = require('./helpers');
import ConfigUtil = require('../utils/config-util');
const NativeNotification = window.Notification; const NativeNotification = window.Notification;
class BaseNotification extends NativeNotification { class BaseNotification extends NativeNotification {
constructor(title, opts) { constructor(title: string, opts: NotificationOptions) {
opts.silent = true; opts.silent = true;
super(title, opts); super(title, opts);
@@ -18,14 +19,14 @@ class BaseNotification extends NativeNotification {
}); });
} }
static requestPermission() { static requestPermission(): void {
return; // eslint-disable-line no-useless-return return; // eslint-disable-line no-useless-return
} }
// Override default Notification permission // Override default Notification permission
static get permission() { static get permission(): NotificationPermission {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
} }
module.exports = BaseNotification; export = BaseNotification;

View File

@@ -1,5 +1,6 @@
const { remote } = require('electron'); import { remote } from 'electron';
const Logger = require('../utils/logger-util.js');
import Logger = require('../utils/logger-util');
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
@@ -7,24 +8,25 @@ const logger = new Logger({
}); });
// Do not change this // Do not change this
const appId = 'org.zulip.zulip-electron'; export const appId = 'org.zulip.zulip-electron';
const botsList = []; export type BotListItem = [string, string];
const botsList: BotListItem[] = [];
let botsListLoaded = false; let botsListLoaded = false;
// this function load list of bots from the server // this function load list of bots from the server
// sync=True for a synchronous getJSON request // sync=True for a synchronous getJSON request
// in case botsList isn't already completely loaded when required in parseRely // in case botsList isn't already completely loaded when required in parseRely
function loadBots(sync = false) { export function loadBots(sync = false): void {
const { $ } = window; const { $ } = window;
botsList.length = 0; botsList.length = 0;
if (sync) { if (sync) {
$.ajaxSetup({async: false}); $.ajaxSetup({async: false});
} }
$.getJSON('/json/users') $.getJSON('/json/users')
.done(data => { .done((data: any) => {
const members = data.members; const { members } = data;
members.forEach(membersRow => { members.forEach((membersRow: any) => {
if (membersRow.is_bot) { if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`; const bot = `@${membersRow.full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`; const mention = `@**${bot.replace(/^@/, '')}**`;
@@ -33,7 +35,7 @@ function loadBots(sync = false) {
}); });
botsListLoaded = true; botsListLoaded = true;
}) })
.fail(error => { .fail((error: any) => {
logger.log('Load bots request failed: ', error.responseText); logger.log('Load bots request failed: ', error.responseText);
logger.log('Load bots request status: ', error.statusText); logger.log('Load bots request status: ', error.statusText);
}); });
@@ -42,7 +44,7 @@ function loadBots(sync = false) {
} }
} }
function checkElements(...elements) { export function checkElements(...elements: any[]): boolean {
let status = true; let status = true;
elements.forEach(element => { elements.forEach(element => {
if (element === null || element === undefined) { if (element === null || element === undefined) {
@@ -52,13 +54,13 @@ function checkElements(...elements) {
return status; return status;
} }
function customReply(reply) { export function customReply(reply: string): void {
// server does not support notification reply yet. // server does not support notification reply yet.
const buttonSelector = '.messagebox #send_controls button[type=submit]'; const buttonSelector = '.messagebox #send_controls button[type=submit]';
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content'; const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
const textarea = document.querySelector('#compose-textarea'); const textarea: HTMLInputElement = document.querySelector('#compose-textarea');
const messagebox = document.querySelector(messageboxSelector); const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector);
const sendButton = document.querySelector(buttonSelector); const sendButton: HTMLButtonElement = document.querySelector(buttonSelector);
// sanity check for old server versions // sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton); const elementsExists = checkElements(textarea, messagebox, sendButton);
@@ -77,19 +79,24 @@ const webContentsId = webContents.id;
// this function will focus the server that sent // this function will focus the server that sent
// the notification. Main function implemented in main.js // the notification. Main function implemented in main.js
function focusCurrentServer() { export function focusCurrentServer(): void {
currentWindow.send('focus-webview-with-id', webContentsId); // TODO: TypeScript: currentWindow of type BrowserWindow doesn't
// have a .send() property per typescript.
(currentWindow as any).send('focus-webview-with-id', webContentsId);
} }
// this function parses the reply from to notification // this function parses the reply from to notification
// making it easier to reply from notification eg // making it easier to reply from notification eg
// @username in reply will be converted to @**username** // @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream** // #stream in reply will be converted to #**stream**
// bot mentions are not yet supported // bot mentions are not yet supported
function parseReply(reply) { export function parseReply(reply: string): string {
const usersDiv = document.querySelectorAll('#user_presences li'); const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li'); const streamHolder = document.querySelectorAll('#stream_filters li');
const users = [];
const streams = []; type UsersItem = BotListItem;
type StreamsItem = BotListItem;
const users: UsersItem[] = [];
const streams: StreamsItem[] = [];
usersDiv.forEach(userRow => { usersDiv.forEach(userRow => {
const anchor = userRow.querySelector('span a'); const anchor = userRow.querySelector('span a');
@@ -139,18 +146,8 @@ function parseReply(reply) {
return reply; return reply;
} }
function setupReply(id) { export function setupReply(id: string): void {
const { narrow } = window; const { narrow } = window;
const narrowByTopic = narrow.by_topic || narrow.by_subject; const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, { trigger: 'notification' }); narrowByTopic(id, { trigger: 'notification' });
} }
module.exports = {
appId,
checkElements,
customReply,
parseReply,
setupReply,
focusCurrentServer,
loadBots
};

View File

@@ -1,12 +1,11 @@
'use strict'; 'use strict';
const { import { remote } from 'electron';
remote: { app } import * as params from '../utils/params-util';
} = require('electron'); import { appId, loadBots } from './helpers';
const params = require('../utils/params-util.js'); import DefaultNotification = require('./default-notification');
const DefaultNotification = require('./default-notification'); const { app } = remote;
const { appId, loadBots } = require('./helpers');
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work. // On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
@@ -15,12 +14,11 @@ app.setAppUserModelId(appId);
window.Notification = DefaultNotification; window.Notification = DefaultNotification;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const DarwinNotification = require('./darwin-notifications'); window.Notification = require('./darwin-notifications');
window.Notification = DarwinNotification;
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
// eslint-disable-next-line no-undef, camelcase // eslint-disable-next-line no-undef, @typescript-eslint/camelcase
if (params.isPageParams() && page_params.realm_uri) { if (params.isPageParams() && page_params.realm_uri) {
loadBots(); loadBots();
} }

View File

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

View File

@@ -1,20 +1,28 @@
'use-strict'; 'use-strict';
const { dialog } = require('electron').remote; import { remote, OpenDialogOptions } from 'electron';
const path = require('path');
const BaseComponent = require(__dirname + '/../../components/base.js'); import path = require('path');
const CertificateUtil = require(__dirname + '/../../utils/certificate-util.js'); import BaseComponent = require('../../components/base');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js'); import CertificateUtil = require('../../utils/certificate-util');
import DomainUtil = require('../../utils/domain-util');
const { dialog } = remote;
class AddCertificate extends BaseComponent { class AddCertificate extends BaseComponent {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
_certFile: string;
$addCertificate: Element | null;
addCertificateButton: Element | null;
serverUrl: HTMLInputElement | null;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
this._certFile = ''; this._certFile = '';
} }
template() { template(): string {
return ` return `
<div class="settings-card certificates-card"> <div class="settings-card certificates-card">
<div class="certificate-input"> <div class="certificate-input">
@@ -29,15 +37,15 @@ class AddCertificate extends BaseComponent {
`; `;
} }
init() { init(): void {
this.$addCertificate = this.generateNodeFromTemplate(this.template()); this.$addCertificate = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$addCertificate); this.props.$root.append(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button'); this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0]; this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
this.initListeners(); this.initListeners();
} }
validateAndAdd() { validateAndAdd(): void {
const certificate = this._certFile; const certificate = this._certFile;
const serverUrl = this.serverUrl.value; const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') { if (certificate !== '' && serverUrl !== '') {
@@ -55,14 +63,13 @@ class AddCertificate extends BaseComponent {
this.serverUrl.value = ''; this.serverUrl.value = '';
} else { } else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ? dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`); 'Enter an Organization URL' : 'Choose certificate file'}`);
} }
} }
addHandler() { addHandler(): void {
const showDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
defaultId: 1,
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }] filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
}; };
@@ -74,7 +81,7 @@ class AddCertificate extends BaseComponent {
}); });
} }
initListeners() { initListeners(): void {
this.addCertificateButton.addEventListener('click', () => { this.addCertificateButton.addEventListener('click', () => {
this.addHandler(); this.addHandler();
}); });
@@ -89,4 +96,4 @@ class AddCertificate extends BaseComponent {
} }
} }
module.exports = AddCertificate; export = AddCertificate;

View File

@@ -1,10 +1,10 @@
'use strict'; 'use strict';
const electron = require('electron'); import { app } from 'electron';
const { app } = require('electron');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js'); import electron = require('electron');
import ConfigUtil = require('../../utils/config-util');
let instance = null; let instance: BadgeSettings | any = null;
class BadgeSettings { class BadgeSettings {
constructor() { constructor() {
@@ -17,7 +17,7 @@ class BadgeSettings {
return instance; return instance;
} }
showBadgeCount(messageCount, mainWindow) { showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.setBadgeCount(messageCount); app.setBadgeCount(messageCount);
} }
@@ -26,7 +26,7 @@ class BadgeSettings {
} }
} }
hideBadgeCount(mainWindow) { hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.setBadgeCount(0); app.setBadgeCount(0);
} }
@@ -35,7 +35,7 @@ class BadgeSettings {
} }
} }
updateBadge(badgeCount, mainWindow) { updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) { if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow); this.showBadgeCount(badgeCount, mainWindow);
} else { } else {
@@ -43,7 +43,7 @@ class BadgeSettings {
} }
} }
updateOverlayIcon(messageCount, mainWindow) { updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (!mainWindow.isFocused()) { if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage')); mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
} }
@@ -54,10 +54,10 @@ class BadgeSettings {
} }
} }
updateTaskbarIcon(data, text, mainWindow) { updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
const img = electron.nativeImage.createFromDataURL(data); const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text); mainWindow.setOverlayIcon(img, text);
} }
} }
module.exports = new BadgeSettings(); export = new BadgeSettings();

View File

@@ -1,22 +1,23 @@
'use strict'; 'use strict';
const {ipcRenderer} = require('electron'); import { ipcRenderer } from 'electron';
const BaseComponent = require(__dirname + '/../../components/base.js'); import BaseComponent = require('../../components/base');
class BaseSection extends BaseComponent { class BaseSection extends BaseComponent {
generateSettingOption(props) { // TODO: TypeScript - Here props should be object type
generateSettingOption(props: any): void {
const {$element, value, clickHandler} = props; const {$element, value, clickHandler} = props;
$element.innerHTML = ''; $element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value)); const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
$element.appendChild($optionControl); $element.append($optionControl);
$optionControl.addEventListener('click', clickHandler); $optionControl.addEventListener('click', clickHandler);
} }
generateOptionTemplate(settingOption) { generateOptionTemplate(settingOption: boolean): string {
if (settingOption) { if (settingOption) {
return ` return `
<div class="action"> <div class="action">
@@ -38,9 +39,9 @@ class BaseSection extends BaseComponent {
} }
} }
reloadApp() { reloadApp(): void {
ipcRenderer.send('forward-message', 'reload-viewer'); ipcRenderer.send('forward-message', 'reload-viewer');
} }
} }
module.exports = BaseSection; export = BaseSection;

View File

@@ -1,18 +1,27 @@
'use strict'; 'use strict';
const BaseSection = require(__dirname + '/base-section.js'); import { ipcRenderer } from 'electron';
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js'); import BaseSection = require('./base-section');
const AddCertificate = require(__dirname + '/add-certificate.js'); import DomainUtil = require('../../utils/domain-util');
const FindAccounts = require(__dirname + '/find-accounts.js'); import ServerInfoForm = require('./server-info-form');
import AddCertificate = require('./add-certificate');
import FindAccounts = require('./find-accounts');
class ConnectedOrgSection extends BaseSection { class ConnectedOrgSection extends BaseSection {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$serverInfoContainer: Element | null;
$existingServers: Element | null;
$newOrgButton: HTMLButtonElement | null;
$addCertificateContainer: Element | null;
$findAccountsContainer: Element | null;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
<div class="page-title">Connected organizations</div> <div class="page-title">Connected organizations</div>
@@ -27,21 +36,21 @@ class ConnectedOrgSection extends BaseSection {
`; `;
} }
init() { init(): void {
this.initServers(); this.initServers();
} }
initServers() { initServers(): void {
this.props.$root.innerHTML = ''; this.props.$root.innerHTML = '';
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.template();
this.$serverInfoContainer = document.getElementById('server-info-container'); this.$serverInfoContainer = document.querySelector('#server-info-container');
this.$existingServers = document.getElementById('existing-servers'); this.$existingServers = document.querySelector('#existing-servers');
this.$newOrgButton = document.getElementById('new-org-button'); this.$newOrgButton = document.querySelector('#new-org-button');
this.$addCertificateContainer = document.getElementById('add-certificate-container'); this.$addCertificateContainer = document.querySelector('#add-certificate-container');
this.$findAccountsContainer = document.getElementById('find-accounts-container'); this.$findAccountsContainer = document.querySelector('#find-accounts-container');
const noServerText = 'All the connected orgnizations will appear here'; const noServerText = 'All the connected orgnizations will appear here';
// Show noServerText if no servers are there otherwise hide it // Show noServerText if no servers are there otherwise hide it
@@ -57,8 +66,6 @@ class ConnectedOrgSection extends BaseSection {
} }
this.$newOrgButton.addEventListener('click', () => { this.$newOrgButton.addEventListener('click', () => {
// We don't need to import this since it's already imported in other files
// eslint-disable-next-line no-undef
ipcRenderer.send('forward-message', 'open-org-tab'); ipcRenderer.send('forward-message', 'open-org-tab');
}); });
@@ -66,17 +73,17 @@ class ConnectedOrgSection extends BaseSection {
this.initFindAccounts(); this.initFindAccounts();
} }
initAddCertificate() { initAddCertificate(): void {
new AddCertificate({ new AddCertificate({
$root: this.$addCertificateContainer $root: this.$addCertificateContainer
}).init(); }).init();
} }
initFindAccounts() { initFindAccounts(): void {
new FindAccounts({ new FindAccounts({
$root: this.$findAccountsContainer $root: this.$findAccountsContainer
}).init(); }).init();
} }
} }
module.exports = ConnectedOrgSection; export = ConnectedOrgSection;

View File

@@ -1,16 +1,21 @@
'use-strict'; 'use-strict';
const { shell } = require('electron'); import { shell } from 'electron';
const BaseComponent = require(__dirname + '/../../components/base.js'); import BaseComponent = require('../../components/base');
class FindAccounts extends BaseComponent { class FindAccounts extends BaseComponent {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$findAccounts: Element | null;
$findAccountsButton: Element | null;
$serverUrlField: HTMLInputElement | null;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="settings-card certificate-card"> <div class="settings-card certificate-card">
<div class="certificate-input"> <div class="certificate-input">
@@ -24,15 +29,15 @@ class FindAccounts extends BaseComponent {
`; `;
} }
init() { init(): void {
this.$findAccounts = this.generateNodeFromTemplate(this.template()); this.$findAccounts = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$findAccounts); this.props.$root.append(this.$findAccounts);
this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button'); this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button');
this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0]; this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
this.initListeners(); this.initListeners();
} }
findAccounts(url) { findAccounts(url: string): void {
if (!url) { if (!url) {
return; return;
} }
@@ -42,7 +47,7 @@ class FindAccounts extends BaseComponent {
shell.openExternal(url + '/accounts/find'); shell.openExternal(url + '/accounts/find');
} }
initListeners() { initListeners(): void {
this.$findAccountsButton.addEventListener('click', () => { this.$findAccountsButton.addEventListener('click', () => {
this.findAccounts(this.$serverUrlField.value); this.findAccounts(this.$serverUrlField.value);
}); });
@@ -69,4 +74,4 @@ class FindAccounts extends BaseComponent {
} }
} }
module.exports = FindAccounts; export = FindAccounts;

View File

@@ -1,20 +1,24 @@
'use strict'; 'use strict';
const path = require('path'); import { ipcRenderer, remote, OpenDialogOptions } from 'electron';
const { ipcRenderer, remote } = require('electron');
const fs = require('fs-extra'); import path = require('path');
import fs = require('fs-extra');
const { app, dialog } = remote; const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow(); const currentBrowserWindow = remote.getCurrentWindow();
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js'); import BaseSection = require('./base-section');
import ConfigUtil = require('../../utils/config-util');
class GeneralSection extends BaseSection { class GeneralSection extends BaseSection {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="settings-pane"> <div class="settings-pane">
<div class="title">Appearance</div> <div class="title">Appearance</div>
@@ -131,7 +135,7 @@ class GeneralSection extends BaseSection {
`; `;
} }
init() { init(): void {
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.template();
this.updateTrayOption(); this.updateTrayOption();
this.updateBadgeOption(); this.updateBadgeOption();
@@ -168,7 +172,7 @@ class GeneralSection extends BaseSection {
} }
} }
updateTrayOption() { updateTrayOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#tray-option .setting-control'), $element: document.querySelector('#tray-option .setting-control'),
value: ConfigUtil.getConfigItem('trayIcon', true), value: ConfigUtil.getConfigItem('trayIcon', true),
@@ -181,7 +185,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateMenubarOption() { updateMenubarOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#menubar-option .setting-control'), $element: document.querySelector('#menubar-option .setting-control'),
value: ConfigUtil.getConfigItem('autoHideMenubar', false), value: ConfigUtil.getConfigItem('autoHideMenubar', false),
@@ -194,7 +198,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateBadgeOption() { updateBadgeOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#badge-option .setting-control'), $element: document.querySelector('#badge-option .setting-control'),
value: ConfigUtil.getConfigItem('badgeOption', true), value: ConfigUtil.getConfigItem('badgeOption', true),
@@ -207,7 +211,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateDockBouncing() { updateDockBouncing(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#dock-bounce-option .setting-control'), $element: document.querySelector('#dock-bounce-option .setting-control'),
value: ConfigUtil.getConfigItem('dockBouncing', true), value: ConfigUtil.getConfigItem('dockBouncing', true),
@@ -219,7 +223,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateFlashTaskbar() { updateFlashTaskbar(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#flash-taskbar-option .setting-control'), $element: document.querySelector('#flash-taskbar-option .setting-control'),
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true), value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
@@ -231,7 +235,7 @@ class GeneralSection extends BaseSection {
}); });
} }
autoUpdateOption() { autoUpdateOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'), $element: document.querySelector('#autoupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('autoUpdate', true), value: ConfigUtil.getConfigItem('autoUpdate', true),
@@ -247,7 +251,7 @@ class GeneralSection extends BaseSection {
}); });
} }
betaUpdateOption() { betaUpdateOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'), $element: document.querySelector('#betaupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('betaUpdate', false), value: ConfigUtil.getConfigItem('betaUpdate', false),
@@ -261,7 +265,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateSilentOption() { updateSilentOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#silent-option .setting-control'), $element: document.querySelector('#silent-option .setting-control'),
value: ConfigUtil.getConfigItem('silent', false), value: ConfigUtil.getConfigItem('silent', false),
@@ -269,12 +273,14 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true); const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue); ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption(); this.updateSilentOption();
currentBrowserWindow.send('toggle-silent', newValue); // TODO: TypeScript: currentWindow of type BrowserWindow doesn't
// have a .send() property per typescript.
(currentBrowserWindow as any).send('toggle-silent', newValue);
} }
}); });
} }
showDesktopNotification() { showDesktopNotification(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#show-notification-option .setting-control'), $element: document.querySelector('#show-notification-option .setting-control'),
value: ConfigUtil.getConfigItem('showNotification', true), value: ConfigUtil.getConfigItem('showNotification', true),
@@ -286,7 +292,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateSidebarOption() { updateSidebarOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#sidebar-option .setting-control'), $element: document.querySelector('#sidebar-option .setting-control'),
value: ConfigUtil.getConfigItem('showSidebar', true), value: ConfigUtil.getConfigItem('showSidebar', true),
@@ -299,7 +305,7 @@ class GeneralSection extends BaseSection {
}); });
} }
updateStartAtLoginOption() { updateStartAtLoginOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#startAtLogin-option .setting-control'), $element: document.querySelector('#startAtLogin-option .setting-control'),
value: ConfigUtil.getConfigItem('startAtLogin', false), value: ConfigUtil.getConfigItem('startAtLogin', false),
@@ -312,7 +318,7 @@ class GeneralSection extends BaseSection {
}); });
} }
enableSpellchecker() { enableSpellchecker(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#enable-spellchecker-option .setting-control'), $element: document.querySelector('#enable-spellchecker-option .setting-control'),
value: ConfigUtil.getConfigItem('enableSpellchecker', true), value: ConfigUtil.getConfigItem('enableSpellchecker', true),
@@ -324,7 +330,7 @@ class GeneralSection extends BaseSection {
}); });
} }
enableErrorReporting() { enableErrorReporting(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#enable-error-reporting .setting-control'), $element: document.querySelector('#enable-error-reporting .setting-control'),
value: ConfigUtil.getConfigItem('errorReporting', true), value: ConfigUtil.getConfigItem('errorReporting', true),
@@ -336,7 +342,7 @@ class GeneralSection extends BaseSection {
}); });
} }
clearAppDataDialog() { clearAppDataDialog(): void {
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 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()); const getAppPath = path.join(app.getPath('appData'), app.getName());
@@ -354,10 +360,9 @@ class GeneralSection extends BaseSection {
}); });
} }
customCssDialog() { customCssDialog(): void {
const showDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
defaultId: 1,
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'CSS file', extensions: ['css'] }] filters: [{ name: 'CSS file', extensions: ['css'] }]
}; };
@@ -370,14 +375,14 @@ class GeneralSection extends BaseSection {
}); });
} }
updateResetDataOption() { updateResetDataOption(): void {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button'); const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
resetDataButton.addEventListener('click', () => { resetDataButton.addEventListener('click', () => {
this.clearAppDataDialog(); this.clearAppDataDialog();
}); });
} }
minimizeOnStart() { minimizeOnStart(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#start-minimize-option .setting-control'), $element: document.querySelector('#start-minimize-option .setting-control'),
value: ConfigUtil.getConfigItem('startMinimized', false), value: ConfigUtil.getConfigItem('startMinimized', false),
@@ -389,51 +394,50 @@ class GeneralSection extends BaseSection {
}); });
} }
addCustomCSS() { addCustomCSS(): void {
const customCSSButton = document.querySelector('#add-custom-css .custom-css-button'); const customCSSButton = document.querySelector('#add-custom-css .custom-css-button');
customCSSButton.addEventListener('click', () => { customCSSButton.addEventListener('click', () => {
this.customCssDialog(); this.customCssDialog();
}); });
} }
showCustomCSSPath() { showCustomCSSPath(): void {
if (!ConfigUtil.getConfigItem('customCSS')) { if (!ConfigUtil.getConfigItem('customCSS')) {
const cssPATH = document.getElementById('remove-custom-css'); const cssPATH: HTMLElement = document.querySelector('#remove-custom-css');
cssPATH.style.display = 'none'; cssPATH.style.display = 'none';
} }
} }
removeCustomCSS() { removeCustomCSS(): void {
const removeCSSButton = document.getElementById('css-delete-action'); const removeCSSButton = document.querySelector('#css-delete-action');
removeCSSButton.addEventListener('click', () => { removeCSSButton.addEventListener('click', () => {
ConfigUtil.setConfigItem('customCSS'); ConfigUtil.setConfigItem('customCSS', "");
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
}); });
} }
downloadFolderDialog() { downloadFolderDialog(): void {
const showDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select Download Location', title: 'Select Download Location',
defaultId: 1,
properties: ['openDirectory'] properties: ['openDirectory']
}; };
dialog.showOpenDialog(showDialogOptions, selectedFolder => { dialog.showOpenDialog(showDialogOptions, selectedFolder => {
if (selectedFolder) { if (selectedFolder) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]); ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]);
const downloadFolderPath = document.querySelector('.download-folder-path'); const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0]; downloadFolderPath.innerText = selectedFolder[0];
} }
}); });
} }
downloadFolder() { downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button'); const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => { downloadFolder.addEventListener('click', () => {
this.downloadFolderDialog(); this.downloadFolderDialog();
}); });
} }
showDownloadFolder() { showDownloadFolder(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#show-download-folder .setting-control'), $element: document.querySelector('#show-download-folder .setting-control'),
value: ConfigUtil.getConfigItem('showDownloadFolder', false), value: ConfigUtil.getConfigItem('showDownloadFolder', false),
@@ -444,7 +448,6 @@ class GeneralSection extends BaseSection {
} }
}); });
} }
} }
module.exports = GeneralSection; export = GeneralSection;

View File

@@ -1,19 +1,20 @@
'use strict'; 'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js'); import BaseComponent = require('../../components/base');
class PreferenceNav extends BaseComponent { class PreferenceNav extends BaseComponent {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
navItems: string[];
$el: Element;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts']; this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
this.init(); this.init();
} }
template() { template(): string {
let navItemsTemplate = ''; let navItemsTemplate = '';
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`; navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`;
@@ -27,23 +28,22 @@ class PreferenceNav extends BaseComponent {
`; `;
} }
init() { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el); this.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
} }
registerListeners() { registerListeners(): void {
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
const $item = document.getElementById(`nav-${navItem}`); const $item = document.querySelector(`#nav-${navItem}`);
$item.addEventListener('click', () => { $item.addEventListener('click', () => {
this.props.onItemSelected(navItem); this.props.onItemSelected(navItem);
}); });
} }
} }
select(navItemToSelect) { select(navItemToSelect: string): void {
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
if (navItem === navItemToSelect) { if (navItem === navItemToSelect) {
this.activate(navItem); this.activate(navItem);
@@ -53,15 +53,15 @@ class PreferenceNav extends BaseComponent {
} }
} }
activate(navItem) { activate(navItem: string): void {
const $item = document.getElementById(`nav-${navItem}`); const $item = document.querySelector(`#nav-${navItem}`);
$item.classList.add('active'); $item.classList.add('active');
} }
deactivate(navItem) { deactivate(navItem: string): void {
const $item = document.getElementById(`nav-${navItem}`); const $item = document.querySelector(`#nav-${navItem}`);
$item.classList.remove('active'); $item.classList.remove('active');
} }
} }
module.exports = PreferenceNav; export = PreferenceNav;

View File

@@ -1,17 +1,24 @@
'use strict'; 'use strict';
const {ipcRenderer} = require('electron'); import { ipcRenderer } from 'electron';
const BaseSection = require(__dirname + '/base-section.js'); import BaseSection = require('./base-section');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js'); import ConfigUtil = require('../../utils/config-util');
class NetworkSection extends BaseSection { class NetworkSection extends BaseSection {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$proxyPAC: HTMLInputElement;
$proxyRules: HTMLInputElement;
$proxyBypass: HTMLInputElement;
$proxySaveAction: Element;
$manualProxyBlock: Element;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="settings-pane"> <div class="settings-pane">
<div class="title">Proxy</div> <div class="title">Proxy</div>
@@ -48,12 +55,12 @@ class NetworkSection extends BaseSection {
`; `;
} }
init() { init(): void {
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.template();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value'); this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-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.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.getElementById('proxy-save-action'); this.$proxySaveAction = document.querySelector('#proxy-save-action');
this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block'); this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
this.initProxyOption(); this.initProxyOption();
@@ -70,14 +77,14 @@ class NetworkSection extends BaseSection {
}); });
} }
initProxyOption() { initProxyOption(): void {
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false); const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
this.toggleManualProxySettings(manualProxyEnabled); this.toggleManualProxySettings(manualProxyEnabled);
this.updateProxyOption(); this.updateProxyOption();
} }
toggleManualProxySettings(option) { toggleManualProxySettings(option: boolean): void {
if (option) { if (option) {
this.$manualProxyBlock.classList.remove('hidden'); this.$manualProxyBlock.classList.remove('hidden');
} else { } else {
@@ -85,7 +92,7 @@ class NetworkSection extends BaseSection {
} }
} }
updateProxyOption() { updateProxyOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#use-system-settings .setting-control'), $element: document.querySelector('#use-system-settings .setting-control'),
value: ConfigUtil.getConfigItem('useSystemProxy', false), value: ConfigUtil.getConfigItem('useSystemProxy', false),
@@ -125,4 +132,4 @@ class NetworkSection extends BaseSection {
} }
} }
module.exports = NetworkSection; export = NetworkSection;

View File

@@ -1,16 +1,22 @@
'use strict'; 'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js'); import { shell } from 'electron';
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const shell = require('electron').shell; import BaseComponent = require('../../components/base');
import DomainUtil = require('../../utils/domain-util');
class NewServerForm extends BaseComponent { class NewServerForm extends BaseComponent {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$newServerForm: Element;
$saveServerButton: HTMLButtonElement;
$newServerUrl: HTMLInputElement;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="server-input-container"> <div class="server-input-container">
<div class="title">Organization URL</div> <div class="title">Organization URL</div>
@@ -36,21 +42,20 @@ class NewServerForm extends BaseComponent {
`; `;
} }
init() { init(): void {
this.initForm(); this.initForm();
this.initActions(); this.initActions();
} }
initForm() { initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template()); this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$saveServerButton = this.$newServerForm.getElementsByClassName('server-save-action')[0]; this.$saveServerButton = this.$newServerForm.querySelectorAll('.server-save-action')[0] as HTMLButtonElement;
this.props.$root.innerHTML = ''; this.props.$root.innerHTML = '';
this.props.$root.appendChild(this.$newServerForm); this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
} }
submitFormHandler() { submitFormHandler(): void {
this.$saveServerButton.children[0].innerHTML = 'Connecting...'; this.$saveServerButton.children[0].innerHTML = 'Connecting...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => { DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => { DomainUtil.addDomain(serverConf).then(() => {
@@ -62,15 +67,15 @@ class NewServerForm extends BaseComponent {
}); });
} }
openCreateNewOrgExternalLink() { openCreateNewOrgExternalLink(): void {
const link = 'https://zulipchat.com/new/'; const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgEl = document.getElementById('open-create-org-link'); const externalCreateNewOrgEl = document.querySelector('#open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => { externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link); shell.openExternal(link);
}); });
} }
initActions() { initActions(): void {
this.$saveServerButton.addEventListener('click', () => { this.$saveServerButton.addEventListener('click', () => {
this.submitFormHandler(); this.submitFormHandler();
}); });
@@ -86,4 +91,4 @@ class NewServerForm extends BaseComponent {
} }
} }
module.exports = NewServerForm; export = NewServerForm;

View File

@@ -1,24 +1,29 @@
'use strict'; 'use strict';
const BaseComponent = require(__dirname + '/js/components/base.js'); import { ipcRenderer } from 'electron';
const { ipcRenderer } = require('electron');
const Nav = require(__dirname + '/js/pages/preference/nav.js'); import BaseComponent = require('../../components/base');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js'); import Nav = require('./nav');
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js'); import ServersSection = require('./servers-section');
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js'); import GeneralSection = require('./general-section');
const ConnectedOrgSection = require(__dirname + '/js/pages/preference/connected-org-section.js'); import NetworkSection = require('./network-section');
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js'); import ConnectedOrgSection = require('./connected-org-section');
import ShortcutsSection = require('./shortcuts-section');
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
class PreferenceView extends BaseComponent { class PreferenceView extends BaseComponent {
$sidebarContainer: Element;
$settingsContainer: Element;
nav: Nav;
section: Section;
constructor() { constructor() {
super(); super();
this.$sidebarContainer = document.querySelector('#sidebar');
this.$sidebarContainer = document.getElementById('sidebar'); this.$settingsContainer = document.querySelector('#settings-container');
this.$settingsContainer = document.getElementById('settings-container');
} }
init() { init(): void {
this.nav = new Nav({ this.nav = new Nav({
$root: this.$sidebarContainer, $root: this.$sidebarContainer,
onItemSelected: this.handleNavigation.bind(this) onItemSelected: this.handleNavigation.bind(this)
@@ -28,7 +33,7 @@ class PreferenceView extends BaseComponent {
this.registerIpcs(); this.registerIpcs();
} }
setDefaultView() { setDefaultView(): void {
let nav = 'General'; let nav = 'General';
const hasTag = window.location.hash; const hasTag = window.location.hash;
if (hasTag) { if (hasTag) {
@@ -37,7 +42,7 @@ class PreferenceView extends BaseComponent {
this.handleNavigation(nav); this.handleNavigation(nav);
} }
handleNavigation(navItem) { handleNavigation(navItem: string): void {
this.nav.select(navItem); this.nav.select(navItem);
switch (navItem) { switch (navItem) {
case 'AddServer': { case 'AddServer': {
@@ -77,32 +82,32 @@ class PreferenceView extends BaseComponent {
} }
// Handle toggling and reflect changes in preference page // Handle toggling and reflect changes in preference page
handleToggle(elementName, state) { handleToggle(elementName: string, state: boolean): void {
const inputSelector = `#${elementName} .action .switch input`; const inputSelector = `#${elementName} .action .switch input`;
const input = document.querySelector(inputSelector); const input: HTMLInputElement = document.querySelector(inputSelector);
if (input) { if (input) {
input.checked = state; input.checked = state;
} }
} }
registerIpcs() { registerIpcs(): void {
ipcRenderer.on('switch-settings-nav', (event, navItem) => { ipcRenderer.on('switch-settings-nav', (_event: Event, navItem: string) => {
this.handleNavigation(navItem); this.handleNavigation(navItem);
}); });
ipcRenderer.on('toggle-sidebar-setting', (event, state) => { ipcRenderer.on('toggle-sidebar-setting', (_event: Event, state: boolean) => {
this.handleToggle('sidebar-option', state); this.handleToggle('sidebar-option', state);
}); });
ipcRenderer.on('toggle-menubar-setting', (event, state) => { ipcRenderer.on('toggle-menubar-setting', (_event: Event, state: boolean) => {
this.handleToggle('menubar-option', state); this.handleToggle('menubar-option', state);
}); });
ipcRenderer.on('toggletray', (event, state) => { ipcRenderer.on('toggletray', (_event: Event, state: boolean) => {
this.handleToggle('tray-option', state); this.handleToggle('tray-option', state);
}); });
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => { ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: any) => {
this.handleToggle('show-notification-option', newSettings.showNotification); this.handleToggle('show-notification-option', newSettings.showNotification);
this.handleToggle('silent-option', newSettings.silent); this.handleToggle('silent-option', newSettings.silent);
@@ -113,7 +118,9 @@ class PreferenceView extends BaseComponent {
} }
} }
window.onload = () => { window.addEventListener('load', () => {
const preferenceView = new PreferenceView(); const preferenceView = new PreferenceView();
preferenceView.init(); preferenceView.init();
}; });
export = PreferenceView;

View File

@@ -1,17 +1,26 @@
'use strict'; 'use strict';
const { dialog } = require('electron').remote;
const { ipcRenderer } = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js'); import { remote, ipcRenderer } from 'electron';
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
import BaseComponent = require('../../components/base');
import DomainUtil = require('../../utils/domain-util');
const { dialog } = remote;
class ServerInfoForm extends BaseComponent { class ServerInfoForm extends BaseComponent {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$serverInfoForm: Element;
$serverInfoAlias: Element;
$serverIcon: Element;
$deleteServerButton: Element;
$openServerButton: Element;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="settings-card"> <div class="settings-card">
<div class="server-info-left"> <div class="server-info-left">
@@ -35,21 +44,21 @@ class ServerInfoForm extends BaseComponent {
`; `;
} }
init() { init(): void {
this.initForm(); this.initForm();
this.initActions(); this.initActions();
} }
initForm() { initForm(): void {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template()); this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0]; this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0];
this.$serverIcon = this.$serverInfoForm.getElementsByClassName('server-info-icon')[0]; this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0];
this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0]; this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0];
this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0]; this.$openServerButton = this.$serverInfoForm.querySelectorAll('.open-tab-button')[0];
this.props.$root.appendChild(this.$serverInfoForm); this.props.$root.append(this.$serverInfoForm);
} }
initActions() { initActions(): void {
this.$deleteServerButton.addEventListener('click', () => { this.$deleteServerButton.addEventListener('click', () => {
dialog.showMessageBox({ dialog.showMessageBox({
type: 'warning', type: 'warning',
@@ -76,7 +85,6 @@ class ServerInfoForm extends BaseComponent {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index); ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
}); });
} }
} }
module.exports = ServerInfoForm; export = ServerInfoForm;

View File

@@ -1,15 +1,18 @@
'use strict'; 'use strict';
const BaseSection = require(__dirname + '/base-section.js'); import BaseSection = require('./base-section');
const NewServerForm = require(__dirname + '/new-server-form.js'); import NewServerForm = require('./new-server-form');
class ServersSection extends BaseSection { class ServersSection extends BaseSection {
constructor(props) { // TODO: TypeScript - Here props should be object type
props: any;
$newServerContainer: Element;
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
template() { template(): string {
return ` return `
<div class="add-server-modal"> <div class="add-server-modal">
<div class="modal-container"> <div class="modal-container">
@@ -22,20 +25,20 @@ class ServersSection extends BaseSection {
`; `;
} }
init() { init(): void {
this.initServers(); this.initServers();
} }
initServers() { initServers(): void {
this.props.$root.innerHTML = ''; this.props.$root.innerHTML = '';
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.template();
this.$newServerContainer = document.getElementById('new-server-container'); this.$newServerContainer = document.querySelector('#new-server-container');
this.initNewServerForm(); this.initNewServerForm();
} }
initNewServerForm() { initNewServerForm(): void {
new NewServerForm({ new NewServerForm({
$root: this.$newServerContainer, $root: this.$newServerContainer,
onChange: this.reloadApp onChange: this.reloadApp
@@ -43,4 +46,4 @@ class ServersSection extends BaseSection {
} }
} }
module.exports = ServersSection; export = ServersSection;

View File

@@ -1,338 +0,0 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
const shell = require('electron').shell;
class ShortcutsSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
templateMac() {
const userOSKey = '⌘';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>Reset App Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Zulip</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Others</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>Select All</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>Emoji & Symbols</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Enter Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Previous Organization</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
templateWinLin() {
const userOSKey = 'Ctrl';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>Select All</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>Toggle Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Previous Organization</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
openHotkeysExternalLink() {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.getElementById('open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
init() {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink();
}
}
module.exports = ShortcutsSection;

View File

@@ -0,0 +1,344 @@
'use strict';
import { shell } from 'electron';
import BaseSection = require('./base-section');
class ShortcutsSection extends BaseSection {
// TODO: TypeScript - Here props should be object type
props: any;
constructor(props: any) {
super();
this.props = props;
}
// TODO - Deduplicate templateMac and templateWinLin functions. In theory
// they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more
// lines though one may just be using more new lines and other thing is the use of +.
templateMac(): string {
const userOSKey = '⌘';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>Reset App Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Zulip</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Others</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>Select All</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>Emoji & Symbols</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Enter Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Previous Organization</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
templateWinLin(): string {
const userOSKey = 'Ctrl';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>Select All</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>Toggle Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Previous Organization</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
openHotkeysExternalLink(): void {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink();
}
}
export = ShortcutsSection;

View File

@@ -1,11 +1,17 @@
'use strict'; 'use strict';
const { ipcRenderer, shell } = require('electron'); import { ipcRenderer, shell } from 'electron';
const SetupSpellChecker = require('./spellchecker'); import SetupSpellChecker from './spellchecker';
const ConfigUtil = require(__dirname + '/utils/config-util.js'); import LinkUtil = require('./utils/link-util');
const LinkUtil = require(__dirname + '/utils/link-util.js'); import params = require('./utils/params-util');
const params = require(__dirname + '/utils/params-util.js');
interface PatchedGlobal extends NodeJS.Global {
logout: () => void;
shortcut: () => void;
}
const globalPatched = global as PatchedGlobal;
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./notification'); require('./notification');
@@ -13,44 +19,46 @@ require('./notification');
// Prevent drag and drop event in main process which prevents remote code executaion // Prevent drag and drop event in main process which prevents remote code executaion
require(__dirname + '/shared/preventdrag.js'); require(__dirname + '/shared/preventdrag.js');
// eslint-disable-next-line camelcase declare let window: ZulipWebWindow;
// eslint-disable-next-line @typescript-eslint/camelcase
window.electron_bridge = require('./electron-bridge'); window.electron_bridge = require('./electron-bridge');
const logout = () => { const logout = (): void => {
// Create the menu for the below // Create the menu for the below
document.querySelector('.dropdown-toggle').click(); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
const nodes = document.querySelectorAll('.dropdown-menu li:last-child a'); const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a');
nodes[nodes.length - 1].click(); nodes[nodes.length - 1].click();
}; };
const shortcut = () => { const shortcut = (): void => {
// Create the menu for the below // Create the menu for the below
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]'); const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check // Additional check
if (node.text.trim().toLowerCase() === 'keyboard shortcuts (?)') { if (node.textContent.trim().toLowerCase() === 'keyboard shortcuts (?)') {
node.click(); node.click();
} else { } else {
// Atleast click the dropdown // Atleast click the dropdown
document.querySelector('.dropdown-toggle').click(); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
} }
}; };
process.once('loaded', () => { process.once('loaded', (): void => {
global.logout = logout; globalPatched.logout = logout;
global.shortcut = shortcut; globalPatched.shortcut = shortcut;
}); });
// To prevent failing this script on linux we need to load it after the document loaded // To prevent failing this script on linux we need to load it after the document loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', (): void => {
if (params.isPageParams()) { if (params.isPageParams()) {
// Get the default language of the server // Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, @typescript-eslint/camelcase
if (serverLanguage) { if (serverLanguage) {
// Set spellcheker language
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
// Init spellchecker // Init spellchecker
SetupSpellChecker.init(); SetupSpellChecker.init(serverLanguage);
} }
// redirect users to network troubleshooting page // redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button'); const getRestartButton = document.querySelector('.restart_get_events_button');
@@ -61,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Open image attachment link in the lightbox instead of opening in the default browser // Open image attachment link in the lightbox instead of opening in the default browser
const { $, lightbox } = window; const { $, lightbox } = window;
$('#main_div').on('click', '.message_content p a', function (e) { $('#main_div').on('click', '.message_content p a', function (this: HTMLElement, e: Event) {
const url = $(this).attr('href'); const url = $(this).attr('href');
if (LinkUtil.isImage(url)) { if (LinkUtil.isImage(url)) {
@@ -85,7 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Clean up spellchecker events after you navigate away from this page; // Clean up spellchecker events after you navigate away from this page;
// otherwise, you may experience errors // otherwise, you may experience errors
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', (): void => {
SetupSpellChecker.unsubscribeSpellChecker(); SetupSpellChecker.unsubscribeSpellChecker();
}); });

View File

@@ -5,7 +5,7 @@
// It doesn't affect the compose box so that users can still // It doesn't affect the compose box so that users can still
// use drag and drop event to share files etc // use drag and drop event to share files etc
const preventDragAndDrop = () => { const preventDragAndDrop = (): void => {
const preventEvents = ['dragover', 'drop']; const preventEvents = ['dragover', 'drop'];
preventEvents.forEach(dragEvents => { preventEvents.forEach(dragEvents => {
document.addEventListener(dragEvents, event => { document.addEventListener(dragEvents, event => {

View File

@@ -1,55 +0,0 @@
'use strict';
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const Logger = require(__dirname + '/utils/logger-util.js');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class SetupSpellChecker {
init() {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu();
}
enableSpellChecker() {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
logger.error(err);
}
}
enableContextMenu() {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.attachToInput();
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
this.SpellCheckHandler.switchLanguage(userLanguage);
}
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
this.contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
}
unsubscribeSpellChecker() {
// eslint-disable-next-line no-undef
if (this.SpellCheckHandler) {
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
}
}
module.exports = new SetupSpellChecker();

View File

@@ -0,0 +1,56 @@
'use strict';
import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
import ConfigUtil = require('./utils/config-util');
import Logger = require('./utils/logger-util');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class SetupSpellChecker {
SpellCheckHandler: typeof SpellCheckHandler;
contextMenuListener: typeof ContextMenuListener;
init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu(serverLanguage);
}
enableSpellChecker(): void {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
logger.error(err);
}
}
enableContextMenu(serverLanguage: string): void {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.attachToInput();
this.SpellCheckHandler.switchLanguage(serverLanguage);
this.SpellCheckHandler.currentSpellcheckerChanged.subscribe(() => {
this.SpellCheckHandler.switchLanguage(this.SpellCheckHandler.currentSpellcheckerLanguage);
});
}
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
this.contextMenuListener = new ContextMenuListener((info: object) => {
contextMenuBuilder.showPopupMenu(info);
});
}
unsubscribeSpellChecker(): void {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
}
}
export = new SetupSpellChecker();

View File

@@ -1,17 +1,15 @@
'use strict'; 'use strict';
const path = require('path'); import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron';
const electron = require('electron');
const { ipcRenderer, remote } = electron;
import path = require('path');
import ConfigUtil = require('./utils/config-util.js');
const { Tray, Menu, nativeImage, BrowserWindow } = remote; const { Tray, Menu, nativeImage, BrowserWindow } = remote;
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
const ConfigUtil = require(__dirname + '/utils/config-util.js'); declare let window: ZulipWebWindow;
const iconPath = () => { const iconPath = (): string => {
if (process.platform === 'linux') { if (process.platform === 'linux') {
return APP_ICON + 'linux.png'; return APP_ICON + 'linux.png';
} }
@@ -20,7 +18,7 @@ const iconPath = () => {
let unread = 0; let unread = 0;
const trayIconSize = () => { const trayIconSize = (): number => {
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
return 20; return 20;
@@ -45,7 +43,7 @@ const config = {
thick: process.platform === 'win32' thick: process.platform === 'win32'
}; };
const renderCanvas = function (arg) { const renderCanvas = function (arg: number): Promise<HTMLCanvasElement> {
config.unreadCount = arg; config.unreadCount = arg;
return new Promise(resolve => { return new Promise(resolve => {
@@ -79,10 +77,10 @@ const renderCanvas = function (arg) {
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
} else if (config.unreadCount < 10) { } else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.20));
} else { } else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
} }
resolve(canvas); resolve(canvas);
@@ -94,18 +92,23 @@ const renderCanvas = function (arg) {
* @param arg: Unread count * @param arg: Unread count
* @return the native image * @return the native image
*/ */
const renderNativeImage = function (arg) { const renderNativeImage = function (arg: number): Promise<NativeImage> {
return Promise.resolve() return Promise.resolve()
.then(() => renderCanvas(arg)) .then(() => renderCanvas(arg))
.then(canvas => { .then(canvas => {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG(); const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
// TODO: Fix the function to correctly use Promise correctly.
// the Promise.resolve().then(...) above is useless we should
// start with renderCanvas(arg).then
// eslint-disable-next-line promise/no-return-wrap
return Promise.resolve(nativeImage.createFromBuffer(pngData, { return Promise.resolve(nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio scaleFactor: config.pixelRatio
})); }));
}); });
}; };
function sendAction(action) { function sendAction(action: string): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -115,7 +118,7 @@ function sendAction(action) {
win.webContents.send(action); win.webContents.send(action);
} }
const createTray = function () { const createTray = function (): void {
window.tray = new Tray(iconPath()); window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
@@ -149,9 +152,9 @@ const createTray = function () {
} }
}; };
ipcRenderer.on('destroytray', event => { ipcRenderer.on('destroytray', (event: Event): Event => {
if (!window.tray) { if (!window.tray) {
return; return undefined;
} }
window.tray.destroy(); window.tray.destroy();
@@ -164,7 +167,7 @@ ipcRenderer.on('destroytray', event => {
return event; return event;
}); });
ipcRenderer.on('tray', (event, arg) => { ipcRenderer.on('tray', (_event: Event, arg: number): void => {
if (!window.tray) { if (!window.tray) {
return; return;
} }
@@ -184,7 +187,7 @@ ipcRenderer.on('tray', (event, arg) => {
} }
}); });
function toggleTray() { function toggleTray(): void {
let state; let state;
if (window.tray) { if (window.tray) {
state = false; state = false;
@@ -205,7 +208,7 @@ function toggleTray() {
ConfigUtil.setConfigItem('trayIcon', true); ConfigUtil.setConfigItem('trayIcon', true);
} }
const selector = 'webview:not([class*=disabled])'; const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector); const webview: WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents(); const webContents = webview.getWebContents();
webContents.send('toggletray', state); webContents.send('toggletray', state);
} }

View File

@@ -1,11 +1,14 @@
'use strict'; 'use strict';
const { app, dialog } = require('electron').remote; import { remote } from 'electron';
const fs = require('fs'); import JsonDB from 'node-json-db';
const path = require('path'); import { initSetUp } from './default-util';
const JsonDB = require('node-json-db');
const Logger = require('./logger-util'); import fs = require('fs');
const { initSetUp } = require('./default-util'); import path = require('path');
import Logger = require('./logger-util');
const { app, dialog } = remote;
initSetUp(); initSetUp();
@@ -14,10 +17,12 @@ const logger = new Logger({
timestamp: true timestamp: true
}); });
let instance = null; let instance: null | CertificateUtil = null;
const certificatesDir = `${app.getPath('userData')}/certificates`; const certificatesDir = `${app.getPath('userData')}/certificates`;
class CertificateUtil { class CertificateUtil {
db: JsonDB;
constructor() { constructor() {
if (instance) { if (instance) {
return instance; return instance;
@@ -28,7 +33,8 @@ class CertificateUtil {
this.reloadDB(); this.reloadDB();
return instance; return instance;
} }
getCertificate(server, defaultValue = null) {
getCertificate(server: string, defaultValue: any = null): any {
this.reloadDB(); this.reloadDB();
const value = this.db.getData('/')[server]; const value = this.db.getData('/')[server];
if (value === undefined) { if (value === undefined) {
@@ -37,8 +43,9 @@ class CertificateUtil {
return value; return value;
} }
} }
// Function to copy the certificate to userData folder // Function to copy the certificate to userData folder
copyCertificate(server, location, fileName) { copyCertificate(_server: string, location: string, fileName: string): boolean {
let copied = false; let copied = false;
const filePath = `${certificatesDir}/${fileName}`; const filePath = `${certificatesDir}/${fileName}`;
try { try {
@@ -54,16 +61,19 @@ class CertificateUtil {
} }
return copied; return copied;
} }
setCertificate(server, fileName) {
setCertificate(server: string, fileName: string): void {
const filePath = `${certificatesDir}/${fileName}`; const filePath = `${certificatesDir}/${fileName}`;
this.db.push(`/${server}`, filePath, true); this.db.push(`/${server}`, filePath, true);
this.reloadDB(); this.reloadDB();
} }
removeCertificate(server) {
removeCertificate(server: string): void {
this.db.delete(`/${server}`); this.db.delete(`/${server}`);
this.reloadDB(); this.reloadDB();
} }
reloadDB() {
reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json'); const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
@@ -83,4 +93,4 @@ class CertificateUtil {
} }
} }
module.exports = new CertificateUtil(); export = new CertificateUtil();

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
let instance = null; let instance: null | CommonUtil = null;
class CommonUtil { class CommonUtil {
constructor() { constructor() {
@@ -13,13 +13,13 @@ class CommonUtil {
} }
// unescape already encoded/escaped strings // unescape already encoded/escaped strings
decodeString(string) { decodeString(stringInput: string): string {
const parser = new DOMParser(); const parser = new DOMParser();
const dom = parser.parseFromString( const dom = parser.parseFromString(
'<!doctype html><body>' + string, '<!doctype html><body>' + stringInput,
'text/html'); 'text/html');
return dom.body.textContent; return dom.body.textContent;
} }
} }
module.exports = new CommonUtil(); export = new CommonUtil();

View File

@@ -1,32 +1,33 @@
'use strict'; 'use strict';
import JsonDB from 'node-json-db';
const fs = require('fs'); import fs = require('fs');
const path = require('path'); import path = require('path');
const process = require('process'); import electron = require('electron');
const JsonDB = require('node-json-db'); import Logger = require('./logger-util');
const Logger = require('./logger-util');
const logger = new Logger({ const logger = new Logger({
file: 'config-util.log', file: 'config-util.log',
timestamp: true timestamp: true
}); });
let instance = null; let instance: null | ConfigUtil = null;
let dialog = null; let dialog: Electron.Dialog = null;
let app = null; let app: Electron.App = null;
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') { if (process.type === 'renderer') {
const remote = require('electron').remote; const { remote } = electron;
dialog = remote.dialog; dialog = remote.dialog;
app = remote.app; app = remote.app;
} else { } else {
const electron = require('electron');
dialog = electron.dialog; dialog = electron.dialog;
app = electron.app; app = electron.app;
} }
class ConfigUtil { class ConfigUtil {
db: JsonDB;
constructor() { constructor() {
if (instance) { if (instance) {
return instance; return instance;
@@ -38,7 +39,7 @@ class ConfigUtil {
return instance; return instance;
} }
getConfigItem(key, defaultValue = null) { getConfigItem(key: string, defaultValue: any = null): any {
try { try {
this.db.reload(); this.db.reload();
} catch (err) { } catch (err) {
@@ -53,8 +54,9 @@ class ConfigUtil {
return value; return value;
} }
} }
// This function returns whether a key exists in the configuration file (settings.json) // This function returns whether a key exists in the configuration file (settings.json)
isConfigItemExists(key) { isConfigItemExists(key: string): boolean {
try { try {
this.db.reload(); this.db.reload();
} catch (err) { } catch (err) {
@@ -65,17 +67,17 @@ class ConfigUtil {
return (value !== undefined); return (value !== undefined);
} }
setConfigItem(key, value) { setConfigItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true); this.db.push(`/${key}`, value, true);
this.db.save(); this.db.save();
} }
removeConfigItem(key) { removeConfigItem(key: string): void {
this.db.delete(`/${key}`); this.db.delete(`/${key}`);
this.db.save(); this.db.save();
} }
reloadDB() { reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json'); const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
@@ -96,4 +98,4 @@ class ConfigUtil {
} }
} }
module.exports = new ConfigUtil(); export = new ConfigUtil();

View File

@@ -1,6 +1,6 @@
const fs = require('fs'); import fs = require('fs');
let app = null; let app: Electron.App = null;
let setupCompleted = false; let setupCompleted = false;
if (process.type === 'renderer') { if (process.type === 'renderer') {
app = require('electron').remote.app; app = require('electron').remote.app;
@@ -12,7 +12,7 @@ const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`; const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`; const certificatesDir = `${zulipDir}/certificates/`;
const configDir = `${zulipDir}/config/`; const configDir = `${zulipDir}/config/`;
const initSetUp = () => { export const initSetUp = (): void => {
// if it is the first time the app is running // if it is the first time the app is running
// create zulip dir in userData folder to // create zulip dir in userData folder to
// avoid errors // avoid errors
@@ -72,7 +72,3 @@ const initSetUp = () => {
setupCompleted = true; setupCompleted = true;
} }
}; };
module.exports = {
initSetUp
};

View File

@@ -1,17 +1,30 @@
'use strict'; 'use strict';
const ConfigUtil = require(__dirname + '/config-util.js'); import ConfigUtil = require('./config-util');
function toggle() { // TODO: TypeScript - add to Setting interface
// the list of settings since we have fixed amount of them
// We want to do this by creating a new module that exports
// this interface
interface Setting {
[key: string]: any;
}
interface Toggle {
dnd: boolean;
newSettings: Setting;
}
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem('dnd', false); const dnd = !ConfigUtil.getConfigItem('dnd', false);
const dndSettingList = ['showNotification', 'silent']; const dndSettingList = ['showNotification', 'silent'];
if (process.platform === 'win32') { if (process.platform === 'win32') {
dndSettingList.push('flashTaskbarOnMessage'); dndSettingList.push('flashTaskbarOnMessage');
} }
let newSettings; let newSettings: Setting;
if (dnd) { if (dnd) {
const oldSettings = {}; const oldSettings: Setting = {};
newSettings = {}; newSettings = {};
// Iterate through the dndSettingList. // Iterate through the dndSettingList.
@@ -35,7 +48,3 @@ function toggle() {
ConfigUtil.setConfigItem('dnd', dnd); ConfigUtil.setConfigItem('dnd', dnd);
return {dnd, newSettings}; return {dnd, newSettings};
} }
module.exports = {
toggle
};

View File

@@ -1,27 +1,29 @@
'use strict'; 'use strict';
import JsonDB from 'node-json-db';
const { app, dialog } = require('electron').remote; import escape = require('escape-html');
const fs = require('fs'); import request = require('request');
const path = require('path'); import fs = require('fs');
const JsonDB = require('node-json-db'); import path = require('path');
const request = require('request'); import Logger = require('./logger-util');
const escape = require('escape-html'); import electron = require('electron');
const Logger = require('./logger-util'); import RequestUtil = require('./request-util');
import Messages = require('../../../resources/messages');
const RequestUtil = require(__dirname + '/../utils/request-util.js'); const { app, dialog } = electron.remote;
const Messages = require(__dirname + '/../../../resources/messages.js');
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: `domain-util.log`,
timestamp: true timestamp: true
}); });
let instance = null; let instance: null | DomainUtil = null;
const defaultIconUrl = '../renderer/img/icon.png'; const defaultIconUrl = '../renderer/img/icon.png';
class DomainUtil { class DomainUtil {
db: JsonDB;
constructor() { constructor() {
if (instance) { if (instance) {
return instance; return instance;
@@ -42,7 +44,7 @@ class DomainUtil {
return instance; return instance;
} }
getDomains() { getDomains(): any {
this.reloadDB(); this.reloadDB();
if (this.db.getData('/').domains === undefined) { if (this.db.getData('/').domains === undefined) {
return []; return [];
@@ -51,18 +53,18 @@ class DomainUtil {
} }
} }
getDomain(index) { getDomain(index: number): any {
this.reloadDB(); this.reloadDB();
return this.db.getData(`/domains[${index}]`); return this.db.getData(`/domains[${index}]`);
} }
updateDomain(index, server) { updateDomain(index: number, server: object): void {
this.reloadDB(); this.reloadDB();
this.db.push(`/domains[${index}]`, server, true); this.db.push(`/domains[${index}]`, server, true);
} }
addDomain(server) { addDomain(server: any): Promise<void> {
const ignoreCerts = server.ignoreCerts; const { ignoreCerts } = server;
return new Promise(resolve => { return new Promise(resolve => {
if (server.icon) { if (server.icon) {
this.saveServerIcon(server, ignoreCerts).then(localIconUrl => { this.saveServerIcon(server, ignoreCerts).then(localIconUrl => {
@@ -80,18 +82,18 @@ class DomainUtil {
}); });
} }
removeDomains() { removeDomains(): void {
this.db.delete('/domains'); this.db.delete('/domains');
this.reloadDB(); this.reloadDB();
} }
removeDomain(index) { removeDomain(index: number): void {
this.db.delete(`/domains[${index}]`); this.db.delete(`/domains[${index}]`);
this.reloadDB(); this.reloadDB();
} }
// Check if domain is already added // Check if domain is already added
duplicateDomain(domain) { duplicateDomain(domain: any): boolean {
domain = this.formatUrl(domain); domain = this.formatUrl(domain);
const servers = this.getDomains(); const servers = this.getDomains();
for (const i in servers) { for (const i in servers) {
@@ -102,30 +104,30 @@ class DomainUtil {
return false; return false;
} }
async checkCertError(domain, serverConf, error, silent) { async checkCertError(domain: any, serverConf: any, error: string, silent: boolean): Promise<string | object> {
if (silent) { if (silent) {
// since getting server settings has already failed // since getting server settings has already failed
return serverConf; return serverConf;
} else { } else {
// Report error to sentry to get idea of possible certificate errors // Report error to sentry to get idea of possible certificate errors
// users get when adding the servers // users get when adding the servers
logger.reportSentry(new Error(error)); logger.reportSentry(new Error(error).toString());
const certErrorMessage = Messages.certErrorMessage(domain, error); const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail(); const certErrorDetail = Messages.certErrorDetail();
const response = await dialog.showMessageBox({ const response = await (dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['Yes', 'No'], buttons: ['Yes', 'No'],
defaultId: 1, defaultId: 1,
message: certErrorMessage, message: certErrorMessage,
detail: certErrorDetail detail: certErrorDetail
}); }) as any); // TODO: TypeScript - Figure this out
if (response === 0) { if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes // set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true; serverConf.ignoreCerts = true;
try { try {
return await this.getServerSettings(domain, serverConf.ignoreCerts); return await this.getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) { } catch (_) {
if (error === Messages.noOrgsError(domain)) { if (error === Messages.noOrgsError(domain)) {
throw new Error(error); throw new Error(error);
} }
@@ -139,7 +141,7 @@ class DomainUtil {
// ignoreCerts parameter helps in fetching server icon and // ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings // other server details when user chooses to ignore certificate warnings
async checkDomain(domain, ignoreCerts = false, silent = false) { async checkDomain(domain: any, ignoreCerts = false, silent = false): Promise<any> {
if (!silent && this.duplicateDomain(domain)) { if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode // Do not check duplicate in silent mode
throw new Error('This server has been added.'); throw new Error('This server has been added.');
@@ -168,25 +170,21 @@ class DomainUtil {
const certsError = error.toString().includes('certificate'); const certsError = error.toString().includes('certificate');
if (domain.indexOf(whitelistDomains) >= 0 || certsError) { if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
try { return this.checkCertError(domain, serverConf, error, silent);
return await this.checkCertError(domain, serverConf, error, silent);
} catch (err) {
throw err;
}
} else { } else {
throw Messages.invalidZulipServerError(domain); throw Messages.invalidZulipServerError(domain);
} }
} }
} }
getServerSettings(domain, ignoreCerts = false) { getServerSettings(domain: any, ignoreCerts = false): Promise<object | string> {
const serverSettingsOptions = { const serverSettingsOptions = {
url: domain + '/api/v1/server_settings', url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts) ...RequestUtil.requestOptions(domain, ignoreCerts)
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error, response) => { request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) { if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body); const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) { if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
@@ -208,7 +206,7 @@ class DomainUtil {
}); });
} }
saveServerIcon(server, ignoreCerts = false) { saveServerIcon(server: any, ignoreCerts = false): Promise<string> {
const url = server.icon; const url = server.icon;
const domain = server.url; const domain = server.url;
@@ -222,8 +220,8 @@ class DomainUtil {
const filePath = this.generateFilePath(url); const filePath = this.generateFilePath(url);
const file = fs.createWriteStream(filePath); const file = fs.createWriteStream(filePath);
try { try {
request(serverIconOptions).on('response', response => { request(serverIconOptions).on('response', (response: any) => {
response.on('error', err => { response.on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
@@ -232,7 +230,7 @@ class DomainUtil {
response.pipe(file).on('finish', () => { response.pipe(file).on('finish', () => {
resolve(filePath); resolve(filePath);
}); });
}).on('error', err => { }).on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
@@ -247,9 +245,9 @@ class DomainUtil {
}); });
} }
updateSavedServer(url, index) { updateSavedServer(url: string, index: number): void {
// Does not promise successful update // Does not promise successful update
const ignoreCerts = this.getDomain(index).ignoreCerts; const { ignoreCerts } = this.getDomain(index);
this.checkDomain(url, ignoreCerts, true).then(newServerConf => { this.checkDomain(url, ignoreCerts, true).then(newServerConf => {
this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => { this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => {
newServerConf.icon = localIconUrl; newServerConf.icon = localIconUrl;
@@ -259,7 +257,7 @@ class DomainUtil {
}); });
} }
reloadDB() { reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json'); const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try { try {
const file = fs.readFileSync(domainJsonPath, 'utf8'); const file = fs.readFileSync(domainJsonPath, 'utf8');
@@ -280,7 +278,7 @@ class DomainUtil {
this.db = new JsonDB(domainJsonPath, true, true); this.db = new JsonDB(domainJsonPath, true, true);
} }
generateFilePath(url) { generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`; const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0]; const extension = path.extname(url).split('?')[0];
@@ -299,7 +297,7 @@ class DomainUtil {
return `${dir}/${hash >>> 0}${extension}`; return `${dir}/${hash >>> 0}${extension}`;
} }
formatUrl(domain) { formatUrl(domain: any): string {
const hasPrefix = (domain.indexOf('http') === 0); const hasPrefix = (domain.indexOf('http') === 0);
if (hasPrefix) { if (hasPrefix) {
return domain; return domain;
@@ -309,4 +307,4 @@ class DomainUtil {
} }
} }
module.exports = new DomainUtil(); export = new DomainUtil();

View File

@@ -1,8 +1,14 @@
'use strict'; 'use strict';
const wurl = require('wurl'); // TODO: TypeScript - Add @types/
import wurl = require('wurl');
let instance = null; let instance: null | LinkUtil = null;
interface IsInternalResponse {
isInternalUrl: boolean;
isUploadsUrl: boolean;
}
class LinkUtil { class LinkUtil {
constructor() { constructor() {
@@ -15,7 +21,7 @@ class LinkUtil {
return instance; return instance;
} }
isInternal(currentUrl, newUrl) { isInternal(currentUrl: string, newUrl: string): IsInternalResponse {
const currentDomain = wurl('hostname', currentUrl); const currentDomain = wurl('hostname', currentUrl);
const newDomain = wurl('hostname', newUrl); const newDomain = wurl('hostname', newUrl);
@@ -29,17 +35,17 @@ class LinkUtil {
}; };
} }
isImage(url) { isImage(url: string): boolean {
// test for images extension as well as urls like .png?s=100 // test for images extension as well as urls like .png?s=100
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i; const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
return isImageUrl.test(url); return isImageUrl.test(url);
} }
isPDF(url) { isPDF(url: string): boolean {
// test for pdf extension // test for pdf extension
const isPDFUrl = /\.(pdf)\?*.*$/i; const isPDFUrl = /\.(pdf)\?*.*$/i;
return isPDFUrl.test(url); return isPDFUrl.test(url);
} }
} }
module.exports = new LinkUtil(); export = new LinkUtil();

View File

@@ -1,12 +1,13 @@
'use strict'; 'use strict';
import JsonDB from 'node-json-db';
import fs = require('fs');
import path = require('path');
import electron = require('electron');
import Logger = require('./logger-util');
const fs = require('fs');
const path = require('path');
const process = require('process');
const remote = const remote =
process.type === 'renderer' ? require('electron').remote : require('electron'); process.type === 'renderer' ? electron.remote : electron;
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const logger = new Logger({ const logger = new Logger({
file: 'linux-update-util.log', file: 'linux-update-util.log',
@@ -15,10 +16,11 @@ const logger = new Logger({
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
const { dialog, app } = remote; const { dialog, app } = remote;
let instance: null | LinuxUpdateUtil = null;
let instance = null;
class LinuxUpdateUtil { class LinuxUpdateUtil {
db: JsonDB;
constructor() { constructor() {
if (instance) { if (instance) {
return instance; return instance;
@@ -30,7 +32,7 @@ class LinuxUpdateUtil {
return instance; return instance;
} }
getUpdateItem(key, defaultValue = null) { getUpdateItem(key: string, defaultValue: any = null): any {
this.reloadDB(); this.reloadDB();
const value = this.db.getData('/')[key]; const value = this.db.getData('/')[key];
if (value === undefined) { if (value === undefined) {
@@ -41,17 +43,17 @@ class LinuxUpdateUtil {
} }
} }
setUpdateItem(key, value) { setUpdateItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true); this.db.push(`/${key}`, value, true);
this.reloadDB(); this.reloadDB();
} }
removeUpdateItem(key) { removeUpdateItem(key: string): void {
this.db.delete(`/${key}`); this.db.delete(`/${key}`);
this.reloadDB(); this.reloadDB();
} }
reloadDB() { reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json'); const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try { try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8'); const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
@@ -71,4 +73,4 @@ class LinuxUpdateUtil {
} }
} }
module.exports = new LinuxUpdateUtil(); export = new LinuxUpdateUtil();

View File

@@ -1,37 +1,57 @@
const NodeConsole = require('console').Console; import { Console as NodeConsole } from 'console'; // eslint-disable-line node/prefer-global/console
const fs = require('fs'); import { initSetUp } from './default-util';
const os = require('os'); import { sentryInit, captureException } from './sentry-util';
const isDev = require('electron-is-dev');
const { initSetUp } = require('./default-util'); import fs = require('fs');
const { sentryInit, captureException } = require('./sentry-util'); import os = require('os');
import isDev = require('electron-is-dev');
import electron = require('electron');
// this interface adds [key: string]: any so
// we can do console[type] later on in the code
interface PatchedConsole extends Console {
[key: string]: any;
}
interface LoggerOptions {
timestamp?: any;
file?: string;
level?: boolean;
logInDevMode?: boolean;
}
initSetUp(); initSetUp();
let app = null; let app: Electron.App = null;
let reportErrors = true; let reportErrors = true;
if (process.type === 'renderer') { if (process.type === 'renderer') {
app = require('electron').remote.app; app = electron.remote.app;
// Report Errors to Sentry only if it is enabled in settings // Report Errors to Sentry only if it is enabled in settings
// Gets the value of reportErrors from config-util for renderer process // Gets the value of reportErrors from config-util for renderer process
// For main process, sentryInit() is handled in index.js // For main process, sentryInit() is handled in index.js
const { ipcRenderer } = require('electron'); const { ipcRenderer } = electron;
ipcRenderer.send('error-reporting'); ipcRenderer.send('error-reporting');
ipcRenderer.on('error-reporting-val', (event, errorReporting) => { ipcRenderer.on('error-reporting-val', (_event: any, errorReporting: boolean) => {
reportErrors = errorReporting; reportErrors = errorReporting;
if (reportErrors) { if (reportErrors) {
sentryInit(); sentryInit();
} }
}); });
} else { } else {
app = require('electron').app; app = electron.app;
} }
const browserConsole = console; const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`; const logDir = `${app.getPath('userData')}/Logs`;
class Logger { class Logger {
constructor(opts = {}) { nodeConsole: PatchedConsole;
timestamp: any; // TODO: TypeScript - Figure out how to make this work with string | Function.
level: boolean;
logInDevMode: boolean;
[key: string]: any;
constructor(opts: LoggerOptions = {}) {
let { let {
timestamp = true, timestamp = true,
file = 'console.log', file = 'console.log',
@@ -61,7 +81,7 @@ class Logger {
this.setUpConsole(); this.setUpConsole();
} }
_log(type, ...args) { _log(type: string, ...args: any[]): void {
const { const {
nodeConsole, timestamp, level, logInDevMode nodeConsole, timestamp, level, logInDevMode
} = this; } = this;
@@ -77,7 +97,7 @@ class Logger {
case isDev || logInDevMode: case isDev || logInDevMode:
nodeConsoleLog = nodeConsole[type] || nodeConsole.log; nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
nodeConsoleLog.apply(null, args); nodeConsoleLog.apply(null, args); // eslint-disable-line prefer-spread
default: break; default: break;
} }
@@ -86,19 +106,20 @@ class Logger {
browserConsole[type].apply(null, args); browserConsole[type].apply(null, args);
} }
setUpConsole() { setUpConsole(): void {
for (const type in browserConsole) { for (const type in browserConsole) {
this.setupConsoleMethod(type); this.setupConsoleMethod(type);
} }
} }
setupConsoleMethod(type) { setupConsoleMethod(type: string): void {
this[type] = (...args) => { this[type] = (...args: any[]) => {
this._log(type, ...args); const log = this._log.bind(this, type, ...args);
log();
}; };
} }
getTimestamp() { getTimestamp(): string {
const date = new Date(); const date = new Date();
const timestamp = const timestamp =
`${date.getMonth()}/${date.getDate()} ` + `${date.getMonth()}/${date.getDate()} ` +
@@ -106,13 +127,13 @@ class Logger {
return timestamp; return timestamp;
} }
reportSentry(err) { reportSentry(err: string): void {
if (reportErrors) { if (reportErrors) {
captureException(err); captureException(err);
} }
} }
trimLog(file) { trimLog(file: string): void{
fs.readFile(file, 'utf8', (err, data) => { fs.readFile(file, 'utf8', (err, data) => {
if (err) { if (err) {
throw err; throw err;
@@ -131,4 +152,4 @@ class Logger {
} }
} }
module.exports = Logger; export = Logger;

View File

@@ -1,15 +1,11 @@
// This util function returns the page params if they're present else returns null // This util function returns the page params if they're present else returns null
function isPageParams() { export function isPageParams(): null | object {
let webpageParams = null; let webpageParams = null;
try { try {
// eslint-disable-next-line no-undef, camelcase // eslint-disable-next-line no-undef, @typescript-eslint/camelcase
webpageParams = page_params; webpageParams = page_params;
} catch (err) { } catch (_) {
webpageParams = null; webpageParams = null;
} }
return webpageParams; return webpageParams;
} }
module.exports = {
isPageParams
};

View File

@@ -1,9 +1,15 @@
'use strict'; 'use strict';
const url = require('url'); import url = require('url');
const ConfigUtil = require('./config-util.js');
let instance = null; import ConfigUtil = require('./config-util');
let instance: null | ProxyUtil = null;
interface ProxyRule {
hostname?: string;
port?: number;
}
class ProxyUtil { class ProxyUtil {
constructor() { constructor() {
@@ -17,8 +23,13 @@ class ProxyUtil {
} }
// Return proxy to be used for a particular uri, to be used for request // Return proxy to be used for a particular uri, to be used for request
getProxy(uri) { getProxy(_uri: string): ProxyRule | void {
uri = url.parse(uri); const parsedUri = url.parse(_uri);
if (parsedUri === null) {
return;
}
const uri = parsedUri;
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';'); const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
// If SPS is on and system uses no proxy then request should not try to use proxy from // If SPS is on and system uses no proxy then request should not try to use proxy from
// environment. NO_PROXY = '*' makes request ignore all environment proxy variables. // environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
@@ -27,9 +38,9 @@ class ProxyUtil {
return; return;
} }
const proxyRule = {}; const proxyRule: any = {};
if (uri.protocol === 'http:') { if (uri.protocol === 'http:') {
proxyRules.forEach(proxy => { proxyRules.forEach((proxy: string) => {
if (proxy.includes('http=')) { if (proxy.includes('http=')) {
proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0]; proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1]; proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
@@ -39,7 +50,7 @@ class ProxyUtil {
} }
if (uri.protocol === 'https:') { if (uri.protocol === 'https:') {
proxyRules.forEach(proxy => { proxyRules.forEach((proxy: string) => {
if (proxy.includes('https=')) { if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0]; proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1]; proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
@@ -49,14 +60,15 @@ class ProxyUtil {
} }
} }
resolveSystemProxy(mainWindow) { // TODO: Refactor to async function
resolveSystemProxy(mainWindow: Electron.BrowserWindow): void {
const page = mainWindow.webContents; const page = mainWindow.webContents;
const ses = page.session; const ses = page.session;
const resolveProxyUrl = 'www.example.com'; const resolveProxyUrl = 'www.example.com';
// Check HTTP Proxy // Check HTTP Proxy
const httpProxy = new Promise(resolve => { const httpProxy = new Promise(resolve => {
ses.resolveProxy('http://' + resolveProxyUrl, proxy => { ses.resolveProxy('http://' + resolveProxyUrl, (proxy: string) => {
let httpString = ''; let httpString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
@@ -70,12 +82,12 @@ class ProxyUtil {
}); });
// Check HTTPS Proxy // Check HTTPS Proxy
const httpsProxy = new Promise(resolve => { const httpsProxy = new Promise(resolve => {
ses.resolveProxy('https://' + resolveProxyUrl, proxy => { ses.resolveProxy('https://' + resolveProxyUrl, (proxy: string) => {
let httpsString = ''; let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) { if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // 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 // for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY' || proxy.includes('HTTPS'))) { if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';'; httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
} }
} }
@@ -85,7 +97,7 @@ class ProxyUtil {
// Check FTP Proxy // Check FTP Proxy
const ftpProxy = new Promise(resolve => { const ftpProxy = new Promise(resolve => {
ses.resolveProxy('ftp://' + resolveProxyUrl, proxy => { ses.resolveProxy('ftp://' + resolveProxyUrl, (proxy: string) => {
let ftpString = ''; let ftpString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) { if (proxy.includes('PROXY')) {
@@ -98,7 +110,7 @@ class ProxyUtil {
// Check SOCKS Proxy // Check SOCKS Proxy
const socksProxy = new Promise(resolve => { const socksProxy = new Promise(resolve => {
ses.resolveProxy('socks4://' + resolveProxyUrl, proxy => { ses.resolveProxy('socks4://' + resolveProxyUrl, (proxy: string) => {
let socksString = ''; let socksString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
if (proxy.includes('SOCKS5')) { if (proxy.includes('SOCKS5')) {
@@ -127,4 +139,4 @@ class ProxyUtil {
} }
} }
module.exports = new ProxyUtil(); export = new ProxyUtil();

View File

@@ -1,5 +1,5 @@
const isOnline = require('is-online'); import isOnline = require('is-online');
const Logger = require('./logger-util'); import Logger = require('./logger-util');
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: `domain-util.log`,
@@ -7,16 +7,22 @@ const logger = new Logger({
}); });
class ReconnectUtil { class ReconnectUtil {
constructor(serverManagerView) { // TODO: TypeScript - Figure out how to annotate serverManagerView
// it should be ServerManagerView; maybe make it a generic so we can
// pass the class from main.js
serverManagerView: any;
alreadyReloaded: boolean;
constructor(serverManagerView: any) {
this.serverManagerView = serverManagerView; this.serverManagerView = serverManagerView;
this.alreadyReloaded = false; this.alreadyReloaded = false;
} }
clearState() { clearState(): void {
this.alreadyReloaded = false; this.alreadyReloaded = false;
} }
pollInternetAndReload() { pollInternetAndReload(): void {
const pollInterval = setInterval(() => { const pollInterval = setInterval(() => {
this._checkAndReload() this._checkAndReload()
.then(status => { .then(status => {
@@ -28,14 +34,15 @@ class ReconnectUtil {
}, 1500); }, 1500);
} }
_checkAndReload() { // TODO: Make this a async function
_checkAndReload(): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
isOnline() isOnline()
.then(online => { .then((online: boolean) => {
if (online) { if (online) {
if (!this.alreadyReloaded) { if (!this.alreadyReloaded) {
this.serverManagerView.reloadView(); this.serverManagerView.reloadCurrentView();
} }
logger.log('You\'re back online.'); logger.log('You\'re back online.');
return resolve(true); return resolve(true);
@@ -57,4 +64,4 @@ class ReconnectUtil {
} }
} }
module.exports = ReconnectUtil; export = ReconnectUtil;

View File

@@ -1,29 +1,39 @@
const fs = require('fs'); import fs = require('fs');
const Logger = require('./logger-util'); import ConfigUtil = require('./config-util');
import Logger = require('./logger-util');
const CertificateUtil = require(__dirname + '/certificate-util.js'); import ProxyUtil = require('./proxy-util');
const ProxyUtil = require(__dirname + '/proxy-util.js'); import CertificateUtil = require('./certificate-util');
const ConfigUtil = require(__dirname + '/config-util.js'); import SystemUtil = require('./system-util');
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const logger = new Logger({ const logger = new Logger({
file: `request-util.log`, file: `request-util.log`,
timestamp: true timestamp: true
}); });
let instance = null; let instance: null | RequestUtil = null;
// TODO: TypeScript - Use ProxyRule for the proxy property
// we can do this now since we use export = ProxyUtil syntax
interface RequestUtilResponse {
ca: string;
proxy: string | void | object;
ecdhCurve: 'auto';
headers: { 'User-Agent': string };
rejectUnauthorized: boolean;
}
class RequestUtil { class RequestUtil {
constructor() { constructor() {
if (!instance) { if (!instance) {
instance = this; instance = this;
} }
return instance; return instance;
} }
// ignoreCerts parameter helps in fetching server icon and // ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings // other server details when user chooses to ignore certificate warnings
requestOptions(domain, ignoreCerts) { requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
domain = this.formatUrl(domain); domain = this.formatUrl(domain);
const certificate = CertificateUtil.getCertificate( const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain) encodeURIComponent(domain)
@@ -32,9 +42,9 @@ class RequestUtil {
if (certificate) { if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json // To handle case where certificate has been moved from the location in certificates.json
try { try {
certificateLocation = fs.readFileSync(certificate); certificateLocation = fs.readFileSync(certificate, 'utf8');
} catch (err) { } catch (err) {
logger.warn('Error while trying to get certificate: ' + err); logger.warn(`Error while trying to get certificate: ${err}`);
} }
} }
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy'); const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
@@ -49,14 +59,15 @@ class RequestUtil {
}; };
} }
formatUrl(domain) { formatUrl(domain: string): string {
const hasPrefix = (domain.indexOf('http') === 0); const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) { if (hasPrefix) {
return domain; return domain;
} else { } else {
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`; return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
} }
} }
} }
module.exports = new RequestUtil(); const requestUtil = new RequestUtil();
export = requestUtil;

View File

@@ -1,20 +0,0 @@
const { init, captureException } = require('@sentry/electron');
const isDev = require('electron-is-dev');
const sentryInit = () => {
if (!isDev) {
init({
dsn: 'SENTRY_DSN',
// 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
});
}
};
module.exports = {
sentryInit,
captureException
};

View File

@@ -0,0 +1,21 @@
import { init } from '@sentry/electron';
import isDev = require('electron-is-dev');
import path = require('path');
import dotenv = require('dotenv');
dotenv.config({ path: path.resolve(__dirname, '/../../../../.env') });
export const sentryInit = (): void => {
if (!isDev) {
init({
dsn: process.env.SENTRY_DSN,
// 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,12 +1,16 @@
'use strict'; 'use strict';
import { remote } from 'electron';
const {app} = require('electron').remote; import os = require('os');
const os = require('os'); const { app } = remote;
let instance: null | SystemUtil = null;
let instance = null;
class SystemUtil { class SystemUtil {
connectivityERR: string[];
userAgent: string | null;
constructor() { constructor() {
if (instance) { if (instance) {
return instance; return instance;
@@ -27,29 +31,30 @@ class SystemUtil {
return instance; return instance;
} }
getOS() { getOS(): string {
if (os.platform() === 'darwin') { const platform = os.platform();
if (platform === 'darwin') {
return 'Mac'; return 'Mac';
} } else if (platform === 'linux') {
if (os.platform() === 'linux') {
return 'Linux'; return 'Linux';
} } else if (platform === 'win32') {
if (os.platform() === 'win32' || os.platform() === 'win64') {
if (parseFloat(os.release()) < 6.2) { if (parseFloat(os.release()) < 6.2) {
return 'Windows 7'; return 'Windows 7';
} else { } else {
return 'Windows 10'; return 'Windows 10';
} }
} else {
return '';
} }
} }
setUserAgent(webViewUserAgent) { setUserAgent(webViewUserAgent: string): void {
this.userAgent = 'ZulipElectron/' + app.getVersion() + ' ' + webViewUserAgent; this.userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
} }
getUserAgent() { getUserAgent(): string | null {
return this.userAgent; return this.userAgent;
} }
} }
module.exports = new SystemUtil(); export = new SystemUtil();

View File

@@ -55,6 +55,11 @@
<send-feedback></send-feedback> <send-feedback></send-feedback>
</div> </div>
</body> </body>
<script src="js/main.js"></script>
<script>
// we don't use src='./js/main' in the script tag because
// it messes up require module path resolution
require('./js/main');
</script>
<script>require('./js/shared/preventdrag.js')</script> <script>require('./js/shared/preventdrag.js')</script>
</html> </html>

View File

@@ -12,6 +12,8 @@
<div id="settings-container"></div> <div id="settings-container"></div>
</div> </div>
</body> </body>
<script src="js/pages/preference/preference.js"></script> <script>
<script>require('./js/shared/preventdrag.js')</script> require('./js/pages/preference/preference.js');
require('./js/shared/preventdrag.js')
</script>
</html> </html>

View File

@@ -1,5 +1,5 @@
class Messages { class Messages {
invalidZulipServerError(domain) { invalidZulipServerError(domain: string): string {
return `${domain} does not appear to be a valid Zulip server. Make sure that return `${domain} does not appear to be a valid Zulip server. Make sure that
\n You can connect to that URL in a web browser.\ \n You can connect to that URL in a web browser.\
\n If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\ \n If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
@@ -7,16 +7,16 @@ class Messages {
\n The server has a valid certificate. (You can add custom certificates in Settings > Organizations).`; \n The server has a valid certificate. (You can add custom certificates in Settings > Organizations).`;
} }
noOrgsError(domain) { noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\ return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`; \nPlease contact your server administrator.`;
} }
certErrorMessage(domain, error) { certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`; return `Do you trust certificate from ${domain}? \n ${error}`;
} }
certErrorDetail() { certErrorDetail(): string {
return `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way. return `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again. \nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed. \nUnless you have a good reason to believe otherwise, you should not proceed.
@@ -24,4 +24,4 @@ class Messages {
} }
} }
module.exports = new Messages(); export = new Messages();

View File

@@ -2,6 +2,40 @@
All notable changes to the Zulip desktop app are documented in this file. All notable changes to the Zulip desktop app are documented in this file.
### v3.1.0-beta --2019-07-19
**New features**:
* Add option to find accounts by email.
* Add option to hide Menu bar to View menu.
* Show a loading indicator in the sidebar.
* Update Help Center to open help page of the currently active server.
* Improve auto-detection of spellchecker language.
* Disable menu items on non-server pages.
* Support dark mode on macOS.
**Fixes**:
* Updated, more robust server validation logic.
* Fix JSON DB errors observed when switching tabs.
* Remove unused `isLoading` function from `Tab`.
* Remove unused `defaultId` parameter.
* Fix syntax error in `proxy-util.js`.
* Fix issue with creation of large `.node` files in the `Temp` folder on Windows machines.
* Fix issue where drafts were not saved properly.
**Development**:
* Migrate codebase to TypeScript.
* Set the indent_size in `.editconfig` to 4.
* Use `.env` file for reading Sentry DSN.
**Documentation**:
* Improve development guide.
**Module updates**:
* Upgrade xo to v0.24.0.
* Upgrade node-json-db to v0.9.2.
* Upgrade electron to v3.1.10.
* Add missing transitive dependencies.
### v3.0.0 --2019-05-20 ### v3.0.0 --2019-05-20
**New features**: **New features**:

View File

@@ -5,16 +5,37 @@ const electron = require('electron-connect').server.create({
}); });
const tape = require('gulp-tape'); const tape = require('gulp-tape');
const tapColorize = require('tap-colorize'); const tapColorize = require('tap-colorize');
const ts = require("gulp-typescript");
const tsProject = ts.createProject("tsconfig.json");
const glob = require('glob');
const { execSync } = require('child_process');
const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = { cwd: __dirname };
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);
const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions);
if (jsFiles.length !== tsFiles.length) {
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
console.log('Compiling typescript files...');
execSync(`${npx} tsc`);
}
gulp.task("compile", function () {
return tsProject.src()
.pipe(tsProject())
.js.pipe(gulp.dest("app"));
});
gulp.task('dev', () => { gulp.task('dev', () => {
// Start browser process // Start browser process
electron.start(); electron.start();
// Restart browser process // Restart browser process
gulp.watch('app/main/*.js', gulp.series('restart:browser')); gulp.watch('app/main/*.ts', gulp.series('compile', 'restart:browser'));
// Reload renderer process // Reload renderer process
gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer')); gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
gulp.watch('app/renderer/*.html', gulp.series('reload:renderer')); gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
gulp.watch('app/renderer/js/**/*.js', gulp.series('reload:renderer')); gulp.watch('app/renderer/js/**/*.ts', gulp.series('compile', 'reload:renderer'));
}); });
gulp.task('restart:browser', done => { gulp.task('restart:browser', done => {

2154
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "3.0.0", "version": "3.1.0-beta",
"main": "./app/main", "main": "./app/main",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -21,7 +21,9 @@
"node": ">=6.0.0" "node": ">=6.0.0"
}, },
"scripts": { "scripts": {
"start": "electron app --disable-http-cache --no-electron-connect", "start": "node tools/run-dev",
"clean-ts-files": "git clean app/*.js -xf",
"watch-ts": "tsc -w",
"reinstall": "node ./tools/reinstall-node-modules.js", "reinstall": "node ./tools/reinstall-node-modules.js",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"lint-css": "stylelint app/renderer/css/*.css", "lint-css": "stylelint app/renderer/css/*.css",
@@ -29,10 +31,11 @@
"lint-js": "xo", "lint-js": "xo",
"test": "npm run lint-html && npm run lint-css && npm run lint-js", "test": "npm run lint-html && npm run lint-css && npm run lint-js",
"test-e2e": "gulp test-e2e", "test-e2e": "gulp test-e2e",
"dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js", "compile": "gulp compile",
"pack": "electron-builder --dir", "dev": "gulp dev && npm test",
"dist": "electron-builder", "pack": "tsc && electron-builder --dir",
"mas": "electron-builder --mac mas", "dist": "tsc && electron-builder",
"mas": "tsc && electron-builder --mac mas",
"travis": "cd ./scripts && ./travis-build-test.sh", "travis": "cd ./scripts && ./travis-build-test.sh",
"build-locales": "node tools/locale-helper" "build-locales": "node tools/locale-helper"
}, },
@@ -42,6 +45,9 @@
"build": { "build": {
"appId": "org.zulip.zulip-electron", "appId": "org.zulip.zulip-electron",
"asar": true, "asar": true,
"asarUnpack": [
"**/*.node"
],
"files": [ "files": [
"**/*", "**/*",
"!docs${/*}", "!docs${/*}",
@@ -50,6 +56,7 @@
"copyright": "©2019 Kandra Labs, Inc.", "copyright": "©2019 Kandra Labs, Inc.",
"mac": { "mac": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"darkModeSupport": true,
"artifactName": "${productName}-${version}-${arch}.${ext}" "artifactName": "${productName}-${version}-${arch}.${ext}"
}, },
"linux": { "linux": {
@@ -124,6 +131,10 @@
"InstantMessaging" "InstantMessaging"
], ],
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.4.32",
"@types/dotenv": "6.1.1",
"@typescript-eslint/eslint-plugin": "1.10.2",
"@typescript-eslint/parser": "1.10.2",
"assert": "1.4.1", "assert": "1.4.1",
"cp-file": "5.0.0", "cp-file": "5.0.0",
"devtron": "1.4.0", "devtron": "1.4.0",
@@ -131,9 +142,12 @@
"electron-builder": "20.40.2", "electron-builder": "20.40.2",
"electron-connect": "0.6.2", "electron-connect": "0.6.2",
"electron-debug": "1.4.0", "electron-debug": "1.4.0",
"eslint-config-xo-typescript": "0.14.0",
"fs-extra": "8.1.0",
"google-translate-api": "2.3.0", "google-translate-api": "2.3.0",
"gulp": "4.0.0", "gulp": "4.0.0",
"gulp-tape": "0.0.9", "gulp-tape": "0.0.9",
"gulp-typescript": "5.0.1",
"htmlhint": "0.11.0", "htmlhint": "0.11.0",
"is-ci": "1.0.10", "is-ci": "1.0.10",
"nodemon": "1.14.11", "nodemon": "1.14.11",
@@ -142,24 +156,31 @@
"stylelint": "9.10.1", "stylelint": "9.10.1",
"tap-colorize": "1.2.0", "tap-colorize": "1.2.0",
"tape": "4.8.0", "tape": "4.8.0",
"xo": "0.18.2" "typescript": "3.5.2",
"xo": "0.24.0"
}, },
"xo": { "xo": {
"extends": "xo-typescript",
"extensions": [
"ts"
],
"parserOptions": { "parserOptions": {
"sourceType": "script", "ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"globalReturn": true "globalReturn": true,
"modules": true
} }
}, },
"esnext": true, "esnext": true,
"overrides": [ "overrides": [
{ {
"files": "app*/**/*.js", "files": "app/**/*.ts",
"rules": { "rules": {
"max-lines": [ "max-lines": [
"warn", "warn",
{ {
"max": 700, "max": 800,
"skipBlankLines": true, "skipBlankLines": true,
"skipComments": true "skipComments": true
} }
@@ -174,13 +195,26 @@
"prefer-promise-reject-errors": 0, "prefer-promise-reject-errors": 0,
"import/no-unresolved": 0, "import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0, "import/no-extraneous-dependencies": 0,
"no-prototype-builtins": 0 "no-prototype-builtins": 0,
"lines-between-class-members": 0,
"padding-line-between-statements": 0,
"quotes": 0,
"unicorn/catch-error-name": 0,
"unicorn/no-console-spaces": 0,
"node/no-deprecated-api": 0,
"@typescript-eslint/member-ordering": "never",
"@typescript-eslint/restrict-plus-operands": "never",
"import/default": 0,
"@typescript-eslint/no-unused-vars": 0
} }
} }
], ],
"ignore": [ "ignore": [
"tests/*.js", "tests/*.js",
"tools/locale-helper/*.js" "tools/locale-helper/*.js",
"*/**/*.js",
"*.js",
"typings.d.ts"
], ],
"envs": [ "envs": [
"node", "node",

89
tools/run-dev Normal file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env node
const path = require('path');
const glob = require('glob');
const chalk = require('chalk');
const { spawn } = require('child_process');
async function run(task, commandToRun, opts = {}) {
const args = commandToRun.split(' ');
let cmd = args[0];
args.splice(0, 1);
if (process.platform === 'win32' && /np(m|x)/.test(cmd)) {
cmd = cmd + '.cmd';
}
const defaults = {
cwd: path.resolve(__dirname, '..'),
env: process.env,
stdio: 'pipe'
};
opts = { ...defaults, ...opts };
task = ' ' + task + ' ';
const colors = [
{ bg: 'bold.bgGreen', color: 'bold.green' },
{ bg: 'bold.bgMagenta', color: 'bold.magenta' },
{ bg: 'bold.bgBlue', color: 'bold.blue' },
{ bg: 'bold.bgYellow', color: 'bold.yellow' },
{ bg: 'bold.bgCyan', color: 'bold.cyan' },
{ bg: 'bold.bgKeyword("rebeccapurple")', color: 'bold.keyword("rebeccapurple")' },
{ bg: 'bold.bgKeyword("darkslategray")', color: 'bold.keyword("darkslategray")' },
];
const randomColorIndex = Math.floor((Math.random() * (colors.length * 1000)) / 1000);
const randomColor = colors[randomColorIndex];
console.log(chalk`{${randomColor.bg} ${ task }} {${randomColor.color} ${commandToRun}}`);
const proc = spawn(cmd, args, opts);
proc.stderr.on('data', data => console.log(data.toString()));
proc.stdout.on('data', data => console.log(data.toString()));
return new Promise((resolve, reject) => {
function check(code) {
if (code !== 0) {
reject(chalk`{bgRed ERROR!} Running {red ${commandToRun}} failed with exit code: ${code}`);
} else {
resolve();
}
// we don't want to exit after compiling typescript files
// but instead want to do it if the tsc -w or electron app died.
if (commandToRun !== 'npx tsc') {
process.exit(0);
}
}
proc.on('exit', check);
proc.on('close', check);
proc.on('error', () => {
reject();
process.exit(0);
});
});
}
async function main() {
const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = { cwd: path.resolve(__dirname, '..') };
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);
const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions);
// if the are missing compiled js file for typescript files
// run the typescript compiler before starting the app
if (jsFiles.length !== tsFiles.length) {
console.log('Compiling typescript files...');
await run('TypeScript', 'npx tsc');
}
await Promise.all([
run('Electron app', 'npx electron app --disable-http-cache --no-electron-connect'),
run('TypeScript watch mode', 'npx tsc --watch --pretty')
]);
}
main()
.catch(err => console.error(err));

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "commonjs",
"esModuleInterop": true,
"skipLibCheck": true,
/* Strict type-checking */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": false,
"noImplicitThis": true,
"alwaysStrict": true,
/* Additional checks */
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitReturns": true
},
"include":[
"app/**/*",
"typings.d.ts"
],
"exclude":[
"node_modules",
"app/node_modules"
]
}

54
typings.d.ts vendored Normal file
View File

@@ -0,0 +1,54 @@
declare module 'adm-zip';
declare module 'auto-launch';
declare module 'is-online';
declare module 'request';
declare module 'semver';
declare module '@electron-elements/send-feedback';
declare module 'node-mac-notifier';
declare module 'electron-connect';
declare module 'electron-is-dev';
declare module 'electron-spellchecker';
declare module 'escape-html';
declare module 'fs-extra';
declare module 'wurl';
interface PageParamsObject {
realm_uri: string;
default_language: string;
}
declare var page_params: PageParamsObject;
// since requestIdleCallback didn't make it into lib.dom.d.ts yet
declare function requestIdleCallback(callback: Function, options?: object): void;
// Patch Notification object so we can implement our side
// of Notification classes which we export into zulip side through
// preload.js; if we don't do his extending Notification will throw error.
// Relevant code is in app/renderer/js/notification/default-notification.ts
// and the relevant function is requestPermission.
declare var PatchedNotification: {
prototype: Notification;
new(title: string, options?: NotificationOptions): Notification;
readonly maxActions: number;
readonly permission: NotificationPermission;
requestPermission(): void;
}
// This is mostly zulip side of code we access from window
interface Window {
$: any;
narrow: any
Notification: typeof PatchedNotification;
}
// typescript doesn't have up to date NotificationOptions yet
interface NotificationOptions {
silent?: boolean;
}
interface ZulipWebWindow extends Window {
electron_bridge: any;
tray: any;
$: any;
lightbox: any;
}