diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..c250d84b --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +6.9.4 diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..3e651609 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +2.7.9 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..20af2f68 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +// Place your settings in this file to overwrite default and user settings. +{ +} \ No newline at end of file diff --git a/app/main/index.js b/app/main/index.js index 0707188d..212008ea 100644 --- a/app/main/index.js +++ b/app/main/index.js @@ -1,24 +1,16 @@ 'use strict'; const path = require('path'); -const fs = require('fs'); const os = require('os'); const electron = require('electron'); const {app} = require('electron'); const ipc = require('electron').ipcMain; const {dialog} = require('electron'); -const https = require('https'); -const http = require('http'); const electronLocalshortcut = require('electron-localshortcut'); const Configstore = require('electron-config'); -const JsonDB = require('node-json-db'); const isDev = require('electron-is-dev'); const appMenu = require('./menu'); -const {linkIsInternal, skipImages} = require('./link-helper'); const {appUpdater} = require('./autoupdater'); -const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); -const data = db.getData('/'); - // Adds debug features like hotkeys for triggering dev tools and reload require('electron-debug')(); @@ -45,48 +37,9 @@ const isUserAgent = 'ZulipElectron/' + app.getVersion() + ' ' + userOS(); // Prevent window being garbage collected let mainWindow; -let targetLink; // Load this url in main window -const staticURL = 'file://' + path.join(__dirname, '../renderer', 'index.html'); - -const targetURL = function () { - if (data.domain === undefined) { - return staticURL; - } - return data.domain; -}; - -function serverError(targetURL) { - if (targetURL.indexOf('localhost:') < 0 && data.domain) { - const req = https.request(targetURL + '/static/audio/zulip.ogg', res => { - console.log('Server StatusCode:', res.statusCode); - console.log('You are connected to:', res.req._headers.host); - if (res.statusCode >= 500 && res.statusCode <= 599) { - return dialog.showErrorBox('SERVER IS DOWN!', 'We are getting a ' + res.statusCode + ' error status from the server ' + res.req._headers.host + '. Please try again after some time or you may switch server.'); - } - }); - req.on('error', e => { - if (e.toString().indexOf('Error: self signed certificate') >= 0) { - const url = targetURL.replace(/^https?:\/\//, ''); - console.log('Server StatusCode:', 200); - console.log('You are connected to:', url); - } else { - console.error(e); - } - }); - req.end(); - } else if (data.domain) { - const req = http.request(targetURL + '/static/audio/zulip.ogg', res => { - console.log('Server StatusCode:', res.statusCode); - console.log('You are connected to:', res.req._headers.host); - }); - req.on('error', e => { - console.error(e); - }); - req.end(); - } -} +const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html'); function checkConnectivity() { return dialog.showMessageBox({ @@ -113,6 +66,7 @@ const connectivityERR = [ 'ERR_NAME_NOT_RESOLVED' ]; +// TODO function checkConnection() { // eslint-disable-next-line no-unused-vars mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => { @@ -138,13 +92,6 @@ if (isAlreadyRunning) { app.quit(); } -function checkWindowURL() { - if (data.domain !== undefined) { - return data.domain; - } - return targetLink; -} - function isWindowsOrmacOS() { return process.platform === 'darwin' || process.platform === 'win32'; } @@ -161,20 +108,6 @@ function onClosed() { mainWindow = null; } -function updateDockBadge(title) { - if (title.indexOf('Zulip') === -1) { - return; - } - - let messageCount = (/\(([0-9]+)\)/).exec(title); - messageCount = messageCount ? Number(messageCount[1]) : 0; - - if (process.platform === 'darwin') { - app.setBadgeCount(messageCount); - } - mainWindow.webContents.send('tray', messageCount); -} - function createMainWindow() { const win = new electron.BrowserWindow({ // This settings needs to be saved in config @@ -184,11 +117,11 @@ function createMainWindow() { icon: iconPath(), minWidth: 600, minHeight: 400, + titleBarStyle: 'hidden-inset', webPreferences: { - preload: path.join(__dirname, '../renderer/js/preload.js'), plugins: true, allowDisplayingInsecureContent: true, - nodeIntegration: false + nodeIntegration: true }, show: false }); @@ -197,9 +130,7 @@ function createMainWindow() { win.show(); }); - serverError(targetURL()); - - win.loadURL(targetURL(), { + win.loadURL(mainURL, { userAgent: isUserAgent + ' ' + win.webContents.getUserAgent() }); @@ -241,12 +172,6 @@ function createMainWindow() { }); }); - // Stop page to update it's title - win.on('page-title-updated', (e, title) => { - e.preventDefault(); - updateDockBadge(title); - }); - // To destroy tray icon when navigate to a new URL win.webContents.on('will-navigate', e => { if (e) { @@ -257,30 +182,6 @@ function createMainWindow() { return win; } -// TODO - fix certificate errors - -// app.commandLine.appendSwitch('ignore-certificate-errors', 'true'); - -// For self-signed certificate -ipc.on('certificate-err', (e, domain) => { - const detail = `URL: ${domain} \n Error: Self-Signed Certificate`; - dialog.showMessageBox(mainWindow, { - title: 'Certificate error', - message: `Do you trust certificate from ${domain}?`, - // eslint-disable-next-line object-shorthand - detail: detail, - type: 'warning', - buttons: ['Yes', 'No'], - cancelId: 1 - // eslint-disable-next-line object-shorthand - }, response => { - if (response === 0) { - // eslint-disable-next-line object-shorthand - db.push('/domain', domain); - mainWindow.loadURL(domain); - } - }); -}); // eslint-disable-next-line max-params app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { event.preventDefault(); @@ -310,36 +211,22 @@ app.on('ready', () => { // TODO - use global shortcut instead electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => { - mainWindow.reload(); - mainWindow.webContents.send('destroytray'); + page.send('reload'); + // page.send('destroytray'); }); electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => { - if (page.canGoBack()) { - page.goBack(); - } + page.send('back'); }); electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => { - if (page.canGoForward()) { - page.goForward(); - } + page.send('forward'); }); page.on('dom-ready', () => { - page.insertCSS(fs.readFileSync(path.join(__dirname, '../renderer/css/preload.css'), 'utf8')); mainWindow.show(); }); - page.on('new-window', (event, url) => { - if (linkIsInternal(checkWindowURL(), url) && url.match(skipImages) === null) { - event.preventDefault(); - return mainWindow.loadURL(url); - } - event.preventDefault(); - electron.shell.openExternal(url); - }); - page.once('did-frame-finish-load', () => { const checkOS = isWindowsOrmacOS(); if (checkOS && !isDev) { @@ -352,27 +239,20 @@ app.on('ready', () => { mainWindow.webContents.send('destroytray'); }); checkConnection(); + + ipc.on('reload-main', () => { + page.reload(); + }); + + ipc.on('update-badge', (event, messageCount) => { + if (process.platform === 'darwin') { + app.setBadgeCount(messageCount); + } + page.send('tray', messageCount); + }); }); app.on('will-quit', () => { // Unregister all the shortcuts so that they don't interfare with other apps electronLocalshortcut.unregisterAll(mainWindow); }); - -ipc.on('new-domain', (e, domain) => { - // MainWindow.loadURL(domain); - if (!mainWindow) { - mainWindow = createMainWindow(); - mainWindow.loadURL(domain); - mainWindow.webContents.send('destroytray'); - } else if (mainWindow.isMinimized()) { - mainWindow.webContents.send('destroytray'); - mainWindow.loadURL(domain); - mainWindow.show(); - } else { - mainWindow.webContents.send('destroytray'); - mainWindow.loadURL(domain); - serverError(domain); - } - targetLink = domain; -}); diff --git a/app/main/menu.js b/app/main/menu.js index ddc81051..35710916 100644 --- a/app/main/menu.js +++ b/app/main/menu.js @@ -8,9 +8,8 @@ const app = electron.app; const BrowserWindow = electron.BrowserWindow; const shell = electron.shell; const appName = app.getName(); -// Const tray = require('./tray'); -const {addDomain, about} = require('./windowmanager'); +const {about} = require('./windowmanager'); function sendAction(action) { const win = BrowserWindow.getAllWindows()[0]; @@ -35,8 +34,7 @@ const viewSubmenu = [ label: 'Reload', click(item, focusedWindow) { if (focusedWindow) { - focusedWindow.reload(); - focusedWindow.webContents.send('destroytray'); + sendAction('reload'); } } }, @@ -136,10 +134,12 @@ const darwinTpl = [ type: 'separator' }, { - label: 'Change Zulip Server', + label: 'Manage Zulip Servers', accelerator: 'Cmd+,', - click() { - addDomain(); + click(item, focusedWindow) { + if (focusedWindow) { + sendAction('open-settings'); + } } }, { @@ -269,10 +269,12 @@ const otherTpl = [ type: 'separator' }, { - label: 'Change Zulip Server', + label: 'Manage Zulip Servers', accelerator: 'Ctrl+,', - click() { - addDomain(); + click(item, focusedWindow) { + if (focusedWindow) { + sendAction('open-settings'); + } } }, { diff --git a/app/main/windowmanager.js b/app/main/windowmanager.js index a730aa01..005d8a6f 100644 --- a/app/main/windowmanager.js +++ b/app/main/windowmanager.js @@ -89,11 +89,6 @@ ipc.on('trayabout', event => { } }); -ipc.on('traychangeserver', event => { - if (event) { - addDomain(); - } -}); module.exports = { addDomain, about diff --git a/app/renderer/css/pref.css b/app/renderer/css/pref.css deleted file mode 100644 index fcd67827..00000000 --- a/app/renderer/css/pref.css +++ /dev/null @@ -1,74 +0,0 @@ -body{ - background-color: #6BB6C7; -} - -.form { - position: absolute; - top: 35%; - width: 300px; - left: 9%; -} - -.close { - background: transparent url('../img/close.png') no-repeat 4px 4px; - background-size: 24px 24px; - cursor: pointer; - display: inline-block; - height: 32px; - position: absolute; - right: 6px; - text-indent: -10000px; - top: 6px; - width: 32px; - z-index: 1; - -webkit-app-region: no-drag; -} - -input[type="text"] { - display: block; - margin: 0; - width: 100%; - font-family: sans-serif; - font-size: 18px; - appearance: none; - box-shadow: none; - border-radius: none; - color: #646464; -} -input[type="text"]:focus { - outline: none; -} - -.form input[type="text"] { - padding: 10px; - border: solid 1px #dcdcdc; - transition: box-shadow 0.3s, border 0.3s; -} -.form input[type="text"]:focus, -.form input[type="text"].focus { - border: solid 1px #707070; - box-shadow: 0 0 5px 1px #969696; -} -button { - border: none; - color: #fff; - padding: 12px 32px; - text-align: center; - text-decoration: none; - margin-left: 107px; - margin-top: 24px; - display: inline-block; - font-size: 16px; - background: #137b86; -} -button:focus { - outline: 0; -} -#urladded { - font-size: 20px; - position: absolute; - font-family: 'opensans'; - top: -61%; - left: 25%; - text-align: center; -} \ No newline at end of file diff --git a/app/renderer/css/preference.css b/app/renderer/css/preference.css new file mode 100644 index 00000000..31a34ffb --- /dev/null +++ b/app/renderer/css/preference.css @@ -0,0 +1,162 @@ +html, body { + height: 100%; + margin: 0; + cursor: default; + font-size: 14px; + color: #333; + background: #fff; +} + +#content { + display: flex; + height: 100%; + font-family: sans-serif; +} + +#sidebar { + width: 80px; + padding: 30px; + display: flex; + flex-direction: column; + font-size: 16px; +} + +#tabs-container { + padding: 20px 0; +} + +.tab { + padding: 5px 0; + color: #999; + cursor: pointer; +} + +.tab.active { + color: #464e5a; + cursor: default; + position: relative; +} + +.tab.active::before { + background: #464e5a; + width: 3px; + height: 16px; + position: absolute; + left: -8px; + content: ''; +} + +#settings-header { + font-size: 22px; + color: #5c6166; +} + +#settings-container { + width: 100%; + display: flex; + padding: 30px; + overflow-y: scroll; +} + +.server-info-container { + margin: 20px 0; +} + +.title { + padding: 4px 0 6px 0; + font-size: 18px; + color: #000; +} + +img.server-info-icon { + background: #a4d3c4; + background-size: 100%; + border-radius: 4px; + width: 44px; + height: 44px; +} + +.server-info-left { + margin-right: 20px; +} + +.server-info-right { + flex-grow: 1; +} + +.server-info-row { + display: flex; + line-height: 26px; + height: 40px; +} + +.server-info-key { + width: 40px; + margin-right: 20px; + text-align: right; +} + +.server-info-value { + flex-grow: 1; + font-size: 14px; + height: 24px; + border: none; + border-bottom: #ddd 1px solid; + outline-width: 0; + background: transparent; + max-width: 500px; +} + +.server-info-value:focus { + border-bottom: #b0d8ce 2px solid; +} + +.actions-container { + display: flex; + font-size: 14px; + color: #235d3a; + vertical-align: middle; + margin: 10px 0; + flex-wrap: wrap; +} + +.action { + display: flex; + align-items: center; + margin-right: 20px; +} + +.action i { + margin-right: 5px; + font-size: 18px; +} + +.settings-pane { + flex-grow: 1; +} + +.action:hover { + cursor: pointer; +} + +.action.disabled:hover { + cursor: default; +} + +.action.disabled { + color: #999; +} + +.server-info.active { + background: #ecf4ef; +} + +.server-info { + display: flex; + padding: 10px; + margin: 10px 0 10px 0; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/app/renderer/css/preload.css b/app/renderer/css/preload.css deleted file mode 100644 index 1fa30f28..00000000 --- a/app/renderer/css/preload.css +++ /dev/null @@ -1 +0,0 @@ -/* We'll be overriding default styling so that app look more native * / diff --git a/app/renderer/css/servermanager.css b/app/renderer/css/servermanager.css new file mode 100644 index 00000000..563b19af --- /dev/null +++ b/app/renderer/css/servermanager.css @@ -0,0 +1,115 @@ +html, body { + height: 100%; + margin: 0; + cursor: default; +} + +#content { + display: flex; + height: 100%; + background: #eee url(../img/loading.gif) no-repeat; + background-size: 60px 60px; + background-position: center; +} + +#sidebar { + background: #222c31; + width: 50px; + padding: 40px 0 20px 0; + justify-content: space-between; + display: flex; + flex-direction: column; + -webkit-app-region: drag; +} + +#webview { + flex-grow: 1; +} + +.action-button { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px; +} + +.action-button i { + color: #6c8592; + font-size: 28px; +} + +.action-button:hover i { + color: #98a9b3; +} + +#tabs-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.tab { + position: relative; + margin: 5px 0; +} + +.tab.active::before{ + content: ""; + background: #fff; + border-radius: 0 3px 3px 0; + width: 4px; + position: absolute; + height: 31px; + left: -8px; + top: 5px; +} + +.tab .server-tab{ + background: #a4d3c4; + background-size: 100%; + border-radius: 4px; + width: 31px; + height: 31px; + position: relative; + margin: 5px 0; + z-index: 11; + line-height: 31px; + font-size: 32px; + font-family: sans-serif; + color: #194a2b; + text-align: center; + overflow: hidden; + opacity: 0.6; +} + +.tab .server-tab:hover{ + opacity: 0.8; +} + +.tab .settings-tab{ + background: #eee; +} + +.tab .settings-tab i{ + font-size: 28px; + line-height: 44px; +} + +.tab.active .server-tab{ + opacity: 1; +} + +webview { + opacity: 1; + transition: opacity 0.3s; + flex-grow: 1; +} + +webview.loading { + opacity: 0; + transition: opacity 0.3s; +} + +webview.disabled { + display: none; +} diff --git a/app/renderer/img/loading.gif b/app/renderer/img/loading.gif new file mode 100644 index 00000000..60d63d36 Binary files /dev/null and b/app/renderer/img/loading.gif differ diff --git a/app/renderer/img/topography.png b/app/renderer/img/topography.png deleted file mode 100644 index a009e120..00000000 Binary files a/app/renderer/img/topography.png and /dev/null differ diff --git a/app/renderer/index.html b/app/renderer/index.html deleted file mode 100644 index 8c9c6adb..00000000 --- a/app/renderer/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Login - Zulip - - - -
-
-
- -

Zulip Login

-
-
-
- - - -

-
-
-
-
- - diff --git a/app/renderer/js/domain.js b/app/renderer/js/domain.js deleted file mode 100644 index dd45b6d7..00000000 --- a/app/renderer/js/domain.js +++ /dev/null @@ -1,80 +0,0 @@ -const {app} = require('electron').remote; -const ipcRenderer = require('electron').ipcRenderer; -const JsonDB = require('node-json-db'); -const request = require('request'); - -const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); - -window.addDomain = function () { - const el = sel => { - return document.querySelector(sel); - }; - - const $el = { - error: el('#error'), - main: el('#main'), - section: el('section') - }; - - const event = sel => { - return { - on: (event, callback) => { - document.querySelector(sel).addEventListener(event, callback); - } - }; - }; - - const displayError = msg => { - $el.error.innerText = msg; - $el.error.classList.add('show'); - $el.section.classList.add('shake'); - }; - - let newDomain = document.getElementById('url').value; - newDomain = newDomain.replace(/^https?:\/\//, ''); - if (newDomain === '') { - displayError('Please input a valid URL.'); - } else { - el('#main').innerHTML = 'Checking...'; - if (newDomain.indexOf('localhost:') >= 0) { - const domain = 'http://' + newDomain; - const checkDomain = domain + '/static/audio/zulip.ogg'; - request(checkDomain, (error, response) => { - if (!error && response.statusCode !== 404) { - document.getElementById('main').innerHTML = 'Connect'; - db.push('/domain', domain); - ipcRenderer.send('new-domain', domain); - } else { - $el.main.innerHTML = 'Connect'; - displayError('Not a valid Zulip local server'); - } - }); - // }); - } else { - const domain = 'https://' + newDomain; - const checkDomain = domain + '/static/audio/zulip.ogg'; - - request(checkDomain, (error, response) => { - if (!error && response.statusCode !== 404) { - $el.main.innerHTML = 'Connect'; - db.push('/domain', domain); - ipcRenderer.send('new-domain', domain); - } else if (error.toString().indexOf('Error: self signed certificate') >= 0) { - $el.main.innerHTML = 'Connect'; - ipcRenderer.send('certificate-err', domain); - } else { - $el.main.innerHTML = 'Connect'; - displayError('Not a valid Zulip server'); - } - }); - } - } - - event('#url').on('input', () => { - el('#error').classList.remove('show'); - }); - - event('section').on('animationend', function () { - this.classList.remove('shake'); - }); -}; diff --git a/app/renderer/js/main.js b/app/renderer/js/main.js new file mode 100644 index 00000000..e064103d --- /dev/null +++ b/app/renderer/js/main.js @@ -0,0 +1,258 @@ +'use strict'; + +require(__dirname + '/js/tray.js'); + +const DomainUtil = require(__dirname + '/js/utils/domain-util.js'); +const {linkIsInternal, skipImages} = require(__dirname + '/../main/link-helper'); +const {shell, ipcRenderer} = require('electron'); + +class ServerManagerView { + constructor() { + this.$tabsContainer = document.getElementById('tabs-container'); + + const $actionsContainer = document.getElementById('actions-container'); + this.$addServerButton = $actionsContainer.querySelector('#add-action'); + this.$settingsButton = $actionsContainer.querySelector('#settings-action'); + this.$content = document.getElementById('content'); + + this.isLoading = false; + this.settingsTabIndex = -1; + this.activeTabIndex = -1; + this.zoomFactors = []; + } + + init() { + this.domainUtil = new DomainUtil(); + this.initTabs(); + this.initActions(); + this.registerIpcs(); + } + + initTabs() { + const servers = this.domainUtil.getDomains(); + if (servers.length > 0) { + for (const server of servers) { + this.initTab(server); + } + + this.activateTab(0); + } else { + this.openSettings(); + } + } + + initTab(tab) { + const { + url, + icon + } = tab; + const tabTemplate = tab.template || ` +
+
+
`; + const $tab = this.insertNode(tabTemplate); + const index = this.$tabsContainer.childNodes.length; + this.$tabsContainer.appendChild($tab); + $tab.addEventListener('click', this.activateTab.bind(this, index)); + } + + initWebView(url, index, nodeIntegration = false) { + const webViewTemplate = ` + + + `; + const $webView = this.insertNode(webViewTemplate); + this.$content.appendChild($webView); + this.isLoading = true; + $webView.addEventListener('dom-ready', this.endLoading.bind(this, index)); + + $webView.addEventListener('dom-ready', () => { + // We need to wait until the page title is ready to get badge count + setTimeout(() => this.updateBadge(index), 1000); + }); + $webView.addEventListener('dom-ready', () => { + $webView.focus() + }); + + this.registerListeners($webView, index); + this.zoomFactors[index] = 1; + } + + startLoading(url, index) { + const $activeWebView = document.getElementById(`webview-${this.activeTabIndex}`); + if ($activeWebView) { + $activeWebView.classList.add('disabled'); + } + const $webView = document.getElementById(`webview-${index}`); + if ($webView === null) { + this.initWebView(url, index, this.settingsTabIndex === index); + } else { + this.updateBadge(index); + $webView.classList.remove('disabled'); + $webView.focus() + } + } + + endLoading(index) { + const $webView = document.getElementById(`webview-${index}`); + this.isLoading = false; + $webView.classList.remove('loading'); + } + + initActions() { + this.$addServerButton.addEventListener('click', this.openSettings.bind(this)); + this.$settingsButton.addEventListener('click', this.openSettings.bind(this)); + } + + openSettings() { + if (this.settingsTabIndex !== -1) { + this.activateTab(this.settingsTabIndex); + return; + } + const url = 'file://' + __dirname + '/preference.html'; + + const settingsTabTemplate = ` +
+
+ settings +
+
`; + this.initTab({ + alias: 'Settings', + url, + template: settingsTabTemplate + }); + + this.settingsTabIndex = this.$tabsContainer.childNodes.length - 1; + this.activateTab(this.settingsTabIndex); + } + + activateTab(index) { + if (this.isLoading) { + return; + } + + if (this.activeTabIndex !== -1) { + if (this.activeTabIndex === index) { + return; + } else { + this.getTabAt(this.activeTabIndex).classList.remove('active'); + } + } + + const $tab = this.getTabAt(index); + $tab.classList.add('active'); + + const domain = $tab.getAttribute('domain'); + this.startLoading(domain, index); + this.activeTabIndex = index; + } + + insertNode(html) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + return wrapper.firstElementChild; + } + + getTabAt(index) { + return this.$tabsContainer.childNodes[index]; + } + + updateBadge (index) { + const $activeWebView = document.getElementById(`webview-${index}`); + const title = $activeWebView.getTitle(); + let messageCount = (/\(([0-9]+)\)/).exec(title); + messageCount = messageCount ? Number(messageCount[1]) : 0; + ipcRenderer.send('update-badge', messageCount); + } + + registerListeners($webView, index) { + $webView.addEventListener('new-window', event => { + const {url} = event; + const domainPrefix = this.domainUtil.getDomain(this.activeTabIndex).url; + if (linkIsInternal(domainPrefix, url) && url.match(skipImages) === null) { + event.preventDefault(); + return $webView.loadURL(url); + } + event.preventDefault(); + shell.openExternal(url); + }); + + $webView.addEventListener('page-title-updated', event => { + const {title} = event; + if (title.indexOf('Zulip') === -1) { + return; + } + }); + } + + registerIpcs() { + ipcRenderer.on('reload', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + activeWebview.reload(); + }); + + ipcRenderer.on('back', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + if (activeWebview.canGoBack()) { + activeWebview.goBack(); + } + }); + + ipcRenderer.on('forward', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + if (activeWebview.canGoForward()) { + activeWebview.goForward(); + } + }); + + // Handle zooming functionality + ipcRenderer.on('zoomIn', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + this.zoomFactors[this.activeTabIndex] += 0.1; + activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]); + }); + + ipcRenderer.on('zoomOut', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + this.zoomFactors[this.activeTabIndex] -= 0.1; + activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]); + }); + + ipcRenderer.on('zoomActualSize', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + this.zoomFactors[this.activeTabIndex] = 1; + activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]); + }); + + ipcRenderer.on('log-out', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + activeWebview.executeJavaScript('logout()'); + }); + + ipcRenderer.on('shortcut', () => { + const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`); + activeWebview.executeJavaScript('shortcut()'); + }); + + ipcRenderer.on('open-settings', () => { + if (this.settingsTabIndex === -1) { + this.openSettings(); + } else { + this.activateTab(this.settingsTabIndex); + } + }); + } +} + +window.onload = () => { + const serverManagerView = new ServerManagerView(); + serverManagerView.init(); +}; diff --git a/app/renderer/js/pref.js b/app/renderer/js/pref.js deleted file mode 100644 index 7624ad52..00000000 --- a/app/renderer/js/pref.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; -// eslint-disable-next-line import/no-extraneous-dependencies -const {remote} = require('electron'); - -const prefWindow = remote.getCurrentWindow(); - -document.getElementById('close-button').addEventListener('click', () => { - prefWindow.close(); -}); - -document.addEventListener('keydown', event => { - if (event.key === 'Escape' || event.keyCode === 27) { - prefWindow.close(); - } -}); -// eslint-disable-next-line no-unused-vars -window.prefDomain = function () { - const request = require('request'); - // eslint-disable-next-line import/no-extraneous-dependencies - const ipcRenderer = require('electron').ipcRenderer; - const JsonDB = require('node-json-db'); - // eslint-disable-next-line import/no-extraneous-dependencies - const {app} = require('electron').remote; - - const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); - - let newDomain = document.getElementById('url').value; - newDomain = newDomain.replace(/^https?:\/\//, ''); - newDomain = newDomain.replace(/^http?:\/\//, ''); - - if (newDomain === '') { - document.getElementById('urladded').innerHTML = 'Please input a value'; - } else { - document.getElementById('main').innerHTML = 'Checking...'; - if (newDomain.indexOf('localhost:') >= 0) { - const domain = 'http://' + newDomain; - const checkDomain = domain + '/static/audio/zulip.ogg'; - request(checkDomain, (error, response) => { - if (!error && response.statusCode !== 404) { - document.getElementById('main').innerHTML = 'Switch'; - document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain; - db.push('/domain', domain); - ipcRenderer.send('new-domain', domain); - } else { - document.getElementById('main').innerHTML = 'Switch'; - document.getElementById('urladded').innerHTML = 'Not a valid Zulip Local Server.'; - } - }); - } else { - const domain = 'https://' + newDomain; - const checkDomain = domain + '/static/audio/zulip.ogg'; - request(checkDomain, (error, response) => { - if (!error && response.statusCode !== 404) { - document.getElementById('main').innerHTML = 'Switch'; - document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain; - db.push('/domain', domain); - ipcRenderer.send('new-domain', domain); - } else if (error.toString().indexOf('Error: self signed certificate') >= 0) { - document.getElementById('main').innerHTML = 'Switch'; - ipcRenderer.send('certificate-err', domain); - document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain; - } else { - document.getElementById('main').innerHTML = 'Switch'; - document.getElementById('urladded').innerHTML = 'Not a valid Zulip Server.'; - } - }); - } - } -}; diff --git a/app/renderer/js/preference.js b/app/renderer/js/preference.js new file mode 100644 index 00000000..e35cbb4f --- /dev/null +++ b/app/renderer/js/preference.js @@ -0,0 +1,143 @@ +'use strict'; + +const {ipcRenderer} = require('electron'); + +const DomainUtil = require(__dirname + '/js/utils/domain-util.js'); + +class PreferenceView { + constructor() { + this.$newServerButton = document.getElementById('new-server-action'); + this.$saveServerButton = document.getElementById('save-server-action'); + this.$reloadServerButton = document.getElementById('reload-server-action'); + this.$serverInfoContainer = document.querySelector('.server-info-container'); + } + + init() { + this.domainUtil = new DomainUtil(); + this.initServers(); + this.initActions(); + } + + initServers() { + const servers = this.domainUtil.getDomains(); + this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!'; + + this.initNewServerForm(); + + for (const i in servers) { + this.initServer(servers[i], i); + } + } + + initServer(server, index) { + const { + alias, + url, + icon + } = server; + const serverInfoTemplate = ` +
+
+ +
+
+
+ Name + +
+
+ Url + +
+
+ Icon + +
+
+ Actions +
+ indeterminate_check_box + Delete +
+
+
+
`; + this.$serverInfoContainer.appendChild(this.insertNode(serverInfoTemplate)); + document.getElementById(`delete-server-action-${index}`).addEventListener('click', () => { + this.domainUtil.removeDomain(index); + this.initServers(); + alert('Success. Reload to apply changes.'); + this.$reloadServerButton.classList.remove('hidden'); + }); + } + + initNewServerForm() { + const newServerFormTemplate = ` + + `; + this.$serverInfoContainer.appendChild(this.insertNode(newServerFormTemplate)); + + this.$newServerForm = document.querySelector('.server-info.active'); + this.$newServerAlias = this.$newServerForm.querySelectorAll('input.server-info-value')[0]; + this.$newServerUrl = this.$newServerForm.querySelectorAll('input.server-info-value')[1]; + this.$newServerIcon = this.$newServerForm.querySelectorAll('input.server-info-value')[2]; + } + + initActions() { + this.$newServerButton.addEventListener('click', () => { + this.$newServerForm.classList.remove('hidden'); + this.$saveServerButton.classList.remove('hidden'); + this.$newServerButton.classList.add('hidden'); + }); + this.$saveServerButton.addEventListener('click', () => { + this.domainUtil.checkDomain(this.$newServerUrl.value).then(domain => { + const server = { + alias: this.$newServerAlias.value, + url: domain, + icon: this.$newServerIcon.value + }; + this.domainUtil.addDomain(server); + this.$saveServerButton.classList.add('hidden'); + this.$newServerButton.classList.remove('hidden'); + this.$newServerForm.classList.add('hidden'); + + this.initServers(); + alert('Success. Reload to apply changes.'); + this.$reloadServerButton.classList.remove('hidden'); + }, errorMessage => { + alert(errorMessage); + }); + }); + this.$reloadServerButton.addEventListener('click', () => { + ipcRenderer.send('reload-main'); + }); + } + insertNode(html) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + return wrapper.firstElementChild; + } +} + +window.onload = () => { + const preferenceView = new PreferenceView(); + preferenceView.init(); +}; diff --git a/app/renderer/js/preload.js b/app/renderer/js/preload.js index 355bc41e..794a446b 100644 --- a/app/renderer/js/preload.js +++ b/app/renderer/js/preload.js @@ -1,56 +1,15 @@ 'use strict'; -const ipcRenderer = require('electron').ipcRenderer; -const {webFrame} = require('electron'); const {spellChecker} = require('./spellchecker'); -const _setImmediate = setImmediate; -const _clearImmediate = clearImmediate; -process.once('loaded', () => { - global.setImmediate = _setImmediate; - global.clearImmediate = _clearImmediate; -}); - -// eslint-disable-next-line import/no-unassigned-import -require('./domain'); -// eslint-disable-next-line import/no-unassigned-import -require('./tray.js'); -// Calling Tray.js in renderer process everytime app window loads - -// Handle zooming functionality -const zoomIn = () => { - webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1); -}; - -const zoomOut = () => { - webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1); -}; - -const zoomActualSize = () => { - webFrame.setZoomFactor(1); -}; - -// Get zooming actions from main process -ipcRenderer.on('zoomIn', () => { - zoomIn(); -}); - -ipcRenderer.on('zoomOut', () => { - zoomOut(); -}); - -ipcRenderer.on('zoomActualSize', () => { - zoomActualSize(); -}); - -ipcRenderer.on('log-out', () => { +const logout = () => { // Create the menu for the below document.querySelector('.dropdown-toggle').click(); const nodes = document.querySelectorAll('.dropdown-menu li:last-child a'); nodes[nodes.length - 1].click(); -}); +}; -ipcRenderer.on('shortcut', () => { +const shortcut = () => { // Create the menu for the below const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]'); // Additional check @@ -60,6 +19,11 @@ ipcRenderer.on('shortcut', () => { // Atleast click the dropdown document.querySelector('.dropdown-toggle').click(); } +}; + +process.once('loaded', () => { + global.logout = logout; + global.shortcut = shortcut; }); // To prevent failing this script on linux we need to load it after the document loaded diff --git a/app/renderer/js/tray.js b/app/renderer/js/tray.js index 50311e63..8c7aade7 100644 --- a/app/renderer/js/tray.js +++ b/app/renderer/js/tray.js @@ -5,7 +5,7 @@ const electron = require('electron'); const {ipcRenderer, remote} = electron; -const {Tray, Menu, nativeImage} = remote; +const {Tray, Menu, nativeImage, BrowserWindow} = remote; const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); @@ -102,6 +102,16 @@ const renderNativeImage = function (arg) { }); }; +function sendAction(action) { + const win = BrowserWindow.getAllWindows()[0]; + + if (process.platform === 'darwin') { + win.restore(); + } + + win.webContents.send(action); +} + const createTray = function () { window.tray = new Tray(iconPath()); const contextMenu = Menu.buildFromTemplate([{ @@ -114,9 +124,9 @@ const createTray = function () { type: 'separator' }, { - label: 'Change Zulip server', + label: 'Manage Zulip servers', click() { - ipcRenderer.send('traychangeserver'); + sendAction('open-settings'); } }, { @@ -125,8 +135,7 @@ const createTray = function () { { label: 'Reload', click() { - remote.getCurrentWindow().reload(); - window.tray.destroy(); + sendAction('reload'); } }, { diff --git a/app/renderer/js/utils/domain-util.js b/app/renderer/js/utils/domain-util.js new file mode 100644 index 00000000..a6ca4592 --- /dev/null +++ b/app/renderer/js/utils/domain-util.js @@ -0,0 +1,72 @@ +'use strict'; + +const {app} = require('electron').remote; +const JsonDB = require('node-json-db'); +const request = require('request'); + +const defaultIconUrl = 'https://chat.zulip.org/static/images/logo/zulip-icon-128x128.271d0f6a0ca2.png'; +class DomainUtil { + constructor() { + this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); + // Migrate from old schema + if (this.db.getData('/').domain) { + this.addDomain({ + alias: 'Zulip', + url: this.db.getData('/domain') + }); + this.db.delete('/domain'); + } + } + + getDomains() { + if (this.db.getData('/').domains === undefined) { + return []; + } else { + return this.db.getData('/domains'); + } + } + + getDomain(index) { + return this.db.getData(`/domains[${index}]`); + } + + addDomain(server) { + server.icon = server.icon || defaultIconUrl; + this.db.push('/domains[]', server, true); + } + + removeDomains() { + this.db.delete('/domains'); + } + + removeDomain(index) { + this.db.delete(`/domains[${index}]`); + } + + checkDomain(domain) { + const hasPrefix = (domain.indexOf('http') === 0); + if (!hasPrefix) { + domain = (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`; + } + + const checkDomain = domain + '/static/audio/zulip.ogg'; + + return new Promise((resolve, reject) => { + request(checkDomain, (error, response) => { + if (!error && response.statusCode !== 404) { + resolve(domain); + } else if (error.toString().indexOf('Error: self signed certificate') >= 0) { + if (window.confirm(`Do you trust certificate from ${domain}?`)) { + resolve(domain); + } else { + reject('Untrusted Certificate.'); + } + } else { + reject('Not a valid Zulip server'); + } + }); + }); + } +} + +module.exports = DomainUtil; diff --git a/app/renderer/main.html b/app/renderer/main.html new file mode 100644 index 00000000..f15e2709 --- /dev/null +++ b/app/renderer/main.html @@ -0,0 +1,26 @@ + + + + + + Zulip + + + + +
+ +
+ + + diff --git a/app/renderer/pref.html b/app/renderer/pref.html deleted file mode 100644 index 954c66e8..00000000 --- a/app/renderer/pref.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - -
Close
-
-
- - -
-

-

- - - diff --git a/app/renderer/preference.html b/app/renderer/preference.html new file mode 100644 index 00000000..dd2588c8 --- /dev/null +++ b/app/renderer/preference.html @@ -0,0 +1,45 @@ + + + + + + Zulip - Settings + + + + +
+ +
+
+
Manage Servers
+
+
+ add_box + New Server +
+ + +
+
+ +
+
+
+
+ + + diff --git a/package.json b/package.json index f6a2bdb7..b7d03c08 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/zulip/zulip-electron/issues" }, "scripts": { - "start": "electron ./app/main", + "start": "electron app --disable-http-cache", "postinstall": "install-app-deps", "test": "gulp test && xo", "dev": "gulp dev", @@ -75,7 +75,7 @@ "win": { "target": "nsis", "icon": "build/icon.ico" - }, + }, "nsis": { "perMachine": true, "oneClick": false @@ -104,7 +104,7 @@ "esnext": true, "overrides": [ { - "files": "app/main/*.js", + "files": "app*/**/*.js", "rules": { "max-lines": [ "warn", @@ -113,6 +113,10 @@ "no-warning-comments": 0, "capitalized-comments": 0, "no-else-return": 0, + "no-path-concat": 0, + "no-alert": 0, + "guard-for-in": 0, + "prefer-promise-reject-errors": 0, "import/no-unresolved": 0, "import/no-extraneous-dependencies": 0 } diff --git a/tests/index.js b/tests/index.js index 115672af..9701c524 100644 --- a/tests/index.js +++ b/tests/index.js @@ -7,7 +7,7 @@ describe('application launch', function () { beforeEach(function () { this.app = new Application({ path: require('electron'), - args: [__dirname + '/../app/renderer/index.html'] + args: [__dirname + '/../app/renderer/main.html'] }) return this.app.start() }) @@ -20,7 +20,7 @@ describe('application launch', function () { it('shows an initial window', function () { return this.app.client.getWindowCount().then(function (count) { - assert.equal(count, 1) + assert.equal(count, 2) }) }) })