diff --git a/app/main/autoupdater.js b/app/main/autoupdater.js index 3bd7d761..35d5a423 100644 --- a/app/main/autoupdater.js +++ b/app/main/autoupdater.js @@ -7,24 +7,10 @@ function appUpdater() { const log = require('electron-log'); log.transports.file.level = 'info'; autoUpdater.logger = log; - /* - AutoUpdater.on('error', err => log.info(err)); - autoUpdater.on('checking-for-update', () => log.info('checking-for-update')); - autoUpdater.on('update-available', () => log.info('update-available')); - autoUpdater.on('update-not-available', () => log.info('update-not-available')); - */ // Ask the user if update is available // eslint-disable-next-line no-unused-vars autoUpdater.on('update-downloaded', (event, info) => { - // Let message = app.getName() + ' ' + info.releaseName + ' is now available. It will be installed the next time you restart the application.'; - // if (info.releaseNotes) { - // const splitNotes = info.releaseNotes.split(/[^\r]\n/); - // message += '\n\nRelease notes:\n'; - // splitNotes.forEach(notes => { - // message += notes + '\n\n'; - // }); - // } // Ask user to update the app dialog.showMessageBox({ type: 'question', diff --git a/app/main/link-helper.js b/app/main/link-helper.js deleted file mode 100644 index 49912b4b..00000000 --- a/app/main/link-helper.js +++ /dev/null @@ -1,16 +0,0 @@ -const wurl = require('wurl'); - -// Check link if it's internal/external -function linkIsInternal(currentUrl, newUrl) { - const currentDomain = wurl('hostname', currentUrl); - const newDomain = wurl('hostname', newUrl); - return currentDomain === newDomain; -} - -// We'll be needing this to open images in default browser -const skipImages = '.jpg|.gif|.png|.jpeg|.JPG|.PNG'; - -module.exports = { - linkIsInternal, - skipImages -}; diff --git a/app/main/menu.js b/app/main/menu.js index ebdcdd49..3e7aefe0 100644 --- a/app/main/menu.js +++ b/app/main/menu.js @@ -9,8 +9,6 @@ const BrowserWindow = electron.BrowserWindow; const shell = electron.shell; const appName = app.getName(); -const {about} = require('./windowmanager'); - function sendAction(action) { const win = BrowserWindow.getAllWindows()[0]; @@ -135,8 +133,10 @@ const darwinTpl = [ submenu: [ { label: 'Zulip desktop', - click() { - about(); + click(item, focusedWindow) { + if (focusedWindow) { + sendAction('open-about'); + } } }, { @@ -270,8 +270,10 @@ const otherTpl = [ submenu: [ { label: 'Zulip desktop', - click() { - about(); + click(item, focusedWindow) { + if (focusedWindow) { + sendAction('open-about'); + } } }, { diff --git a/app/main/windowmanager.js b/app/main/windowmanager.js deleted file mode 100644 index 005d8a6f..00000000 --- a/app/main/windowmanager.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; -const path = require('path'); -const electron = require('electron'); -const ipc = require('electron').ipcMain; - -const APP_ICON = path.join(__dirname, '../resources', 'Icon'); - -const iconPath = () => { - return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png'); -}; -let domainWindow; -let aboutWindow; - -function onClosed() { - // Dereference the window - domainWindow = null; - aboutWindow = null; -} - -// Change Zulip server Window -function createdomainWindow() { - const domainwin = new electron.BrowserWindow({ - title: 'Switch Server', - frame: false, - height: 300, - resizable: false, - width: 400, - show: false, - icon: iconPath() - - }); - const domainURL = 'file://' + path.join(__dirname, '../renderer', 'pref.html'); - domainwin.loadURL(domainURL); - domainwin.on('closed', onClosed); - - return domainwin; -} -// Call this window onClick addDomain in tray -function addDomain() { - domainWindow = createdomainWindow(); - domainWindow.once('ready-to-show', () => { - domainWindow.show(); - }); - setTimeout(() => { - if (domainWindow !== null) { - if (!domainWindow.isDestroyed()) { - domainWindow.destroy(); - } - } - }, 15000); -} -// About window -function createAboutWindow() { - const aboutwin = new electron.BrowserWindow({ - width: 500, - height: 500, - title: 'About Zulip Desktop', - show: false, - center: true, - fullscreen: false, - fullscreenable: false, - resizable: false - }); - const aboutURL = 'file://' + path.join(__dirname, '../renderer', 'about.html'); - aboutwin.loadURL(aboutURL); - aboutwin.on('closed', onClosed); - - // Stop page to update it's title - aboutwin.on('page-title-updated', e => { - e.preventDefault(); - }); - - aboutwin.on('closed', onClosed); - - return aboutwin; -} - -// Call this onClick About in tray -function about() { - aboutWindow = createAboutWindow(); - aboutWindow.once('ready-to-show', () => { - aboutWindow.show(); - }); -} - -ipc.on('trayabout', event => { - if (event) { - about(); - } -}); - -module.exports = { - addDomain, - about -}; diff --git a/app/renderer/css/main.css b/app/renderer/css/main.css index a873e360..b78c2d4a 100644 --- a/app/renderer/css/main.css +++ b/app/renderer/css/main.css @@ -116,11 +116,11 @@ html, body { opacity: 0.8; } -.tab .settings-tab { +.tab .functional-tab { background: #eee; } -.tab .settings-tab i { +.tab .functional-tab i { font-size: 28px; line-height: 36px; } @@ -151,6 +151,17 @@ html, body { .tab .server-tab-badge { display: none; } + +.tab .server-tab-badge.close-button { + width: 16px; + padding: 0 0 0 1px; +} + +.tab .server-tab-badge.close-button i { + font-size: 13px; + line-height: 17px; +} + /******************* * Webview Area * *******************/ diff --git a/app/renderer/img/icon.png b/app/renderer/img/icon.png new file mode 100644 index 00000000..d7fe0205 Binary files /dev/null and b/app/renderer/img/icon.png differ diff --git a/app/renderer/js/components/functional-tab.js b/app/renderer/js/components/functional-tab.js new file mode 100644 index 00000000..9f64dc94 --- /dev/null +++ b/app/renderer/js/components/functional-tab.js @@ -0,0 +1,43 @@ +'use strict'; + +const Tab = require(__dirname + '/../components/tab.js'); + +class FunctionalTab extends Tab { + template() { + return `
+
+ close +
+
+ ${this.props.materialIcon} +
+
`; + } + + init() { + this.$el = this.generateNodeFromTemplate(this.template()); + this.props.$root.appendChild(this.$el); + + this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0]; + this.registerListeners(); + } + + registerListeners() { + super.registerListeners(); + + this.$el.addEventListener('mouseover', () => { + this.$closeButton.classList.add('active'); + }); + + this.$el.addEventListener('mouseout', () => { + this.$closeButton.classList.remove('active'); + }); + + this.$closeButton.addEventListener('click', e => { + this.props.onDestroy(); + e.stopPropagation(); + }); + } +} + +module.exports = FunctionalTab; diff --git a/app/renderer/js/components/server-tab.js b/app/renderer/js/components/server-tab.js new file mode 100644 index 00000000..320a89aa --- /dev/null +++ b/app/renderer/js/components/server-tab.js @@ -0,0 +1,31 @@ +'use strict'; + +const Tab = require(__dirname + '/../components/tab.js'); + +class ServerTab extends Tab { + template() { + return `
+
+
+
`; + } + + init() { + super.init(); + + this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0]; + } + + updateBadge(count) { + if (count > 0) { + const formattedCount = count > 999 ? '1K+' : count; + + this.$badge.innerHTML = formattedCount; + this.$badge.classList.add('active'); + } else { + this.$badge.classList.remove('active'); + } + } +} + +module.exports = ServerTab; diff --git a/app/renderer/js/components/tab.js b/app/renderer/js/components/tab.js index c406d95a..42a61222 100644 --- a/app/renderer/js/components/tab.js +++ b/app/renderer/js/components/tab.js @@ -3,65 +3,43 @@ const BaseComponent = require(__dirname + '/../components/base.js'); class Tab extends BaseComponent { - constructor(params) { + constructor(props) { super(); - const {url, icon, name, type, $root, onClick} = params; - this.url = url; - this.name = name; - this.icon = icon; - this.type = type; - this.$root = $root; - this.onClick = onClick; + this.props = props; + this.webview = this.props.webview; this.init(); } - template() { - if (this.type === Tab.SERVER_TAB) { - return `
-
-
-
`; - } else { - return `
-
-
- settings -
-
`; - } - } - init() { this.$el = this.generateNodeFromTemplate(this.template()); - this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0]; - this.$root.appendChild(this.$el); + this.props.$root.appendChild(this.$el); this.registerListeners(); } - updateBadge(count) { - if (count > 0) { - const formattedCount = count > 999 ? '1K+' : count; - - this.$badge.innerHTML = formattedCount; - this.$badge.classList.add('active'); - } else { - this.$badge.classList.remove('active'); - } + registerListeners() { + this.$el.addEventListener('click', this.props.onClick); } - registerListeners() { - this.$el.addEventListener('click', this.onClick); + isLoading() { + return this.webview.isLoading; } activate() { this.$el.classList.add('active'); + this.webview.load(); } deactivate() { this.$el.classList.remove('active'); + this.webview.hide(); + } + + destroy() { + this.$el.parentNode.removeChild(this.$el); + this.webview.$el.parentNode.removeChild(this.webview.$el); } } diff --git a/app/renderer/js/components/webview.js b/app/renderer/js/components/webview.js index e03da492..35ee5bab 100644 --- a/app/renderer/js/components/webview.js +++ b/app/renderer/js/components/webview.js @@ -2,38 +2,28 @@ const DomainUtil = require(__dirname + '/../utils/domain-util.js'); const SystemUtil = require(__dirname + '/../utils/system-util.js'); -const {linkIsInternal, skipImages} = require(__dirname + '/../../../main/link-helper'); +const LinkUtil = require(__dirname + '/../utils/link-util.js'); const {app, dialog, shell} = require('electron').remote; const {ipcRenderer} = require('electron'); const BaseComponent = require(__dirname + '/../components/base.js'); class WebView extends BaseComponent { - constructor(params) { + constructor(props) { super(); - const {$root, url, index, name, isActive, onTitleChange, nodeIntegration} = params; - this.$root = $root; - this.index = index; - this.name = name; - this.url = url; - this.nodeIntegration = nodeIntegration; + this.props = props; - this.onTitleChange = onTitleChange; this.zoomFactor = 1.0; this.loading = false; - this.isActive = isActive; - this.domainUtil = new DomainUtil(); - this.systemUtil = new SystemUtil(); this.badgeCount = 0; } template() { return ` @@ -42,7 +32,7 @@ class WebView extends BaseComponent { init() { this.$el = this.generateNodeFromTemplate(this.template()); - this.$root.appendChild(this.$el); + this.props.$root.appendChild(this.$el); this.registerListeners(); } @@ -50,9 +40,9 @@ class WebView extends BaseComponent { registerListeners() { this.$el.addEventListener('new-window', event => { const {url} = event; - const domainPrefix = this.domainUtil.getDomain(this.index).url; + const domainPrefix = DomainUtil.getDomain(this.props.index).url; - if (linkIsInternal(domainPrefix, url) && url.match(skipImages) === null) { + if (LinkUtil.isInternal(domainPrefix, url)) { event.preventDefault(); this.$el.loadURL(url); } else { @@ -64,14 +54,14 @@ class WebView extends BaseComponent { this.$el.addEventListener('page-title-updated', event => { const {title} = event; this.badgeCount = this.getBadgeCount(title); - this.onTitleChange(); + this.props.onTitleChange(); }); this.$el.addEventListener('dom-ready', this.show.bind(this)); this.$el.addEventListener('did-fail-load', event => { const {errorDescription} = event; - const hasConnectivityErr = (this.systemUtil.connectivityERR.indexOf(errorDescription) >= 0); + const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0); if (hasConnectivityErr) { console.error('error', errorDescription); this.checkConnectivity(); @@ -79,10 +69,10 @@ class WebView extends BaseComponent { }); this.$el.addEventListener('did-start-loading', () => { - let userAgent = this.systemUtil.getUserAgent(); + let userAgent = SystemUtil.getUserAgent(); if (!userAgent) { - this.systemUtil.setUserAgent(this.$el.getUserAgent()); - userAgent = this.systemUtil.getUserAgent(); + SystemUtil.setUserAgent(this.$el.getUserAgent()); + userAgent = SystemUtil.getUserAgent(); } this.$el.setUserAgent(userAgent); }); @@ -95,14 +85,14 @@ class WebView extends BaseComponent { show() { // Do not show WebView if another tab was selected and this tab should be in background. - if (!this.isActive()) { + if (!this.props.isActive()) { return; } this.$el.classList.remove('disabled'); this.focus(); this.loading = false; - this.onTitleChange(this.$el.getTitle()); + this.props.onTitleChange(this.$el.getTitle()); } focus() { diff --git a/app/renderer/js/main.js b/app/renderer/js/main.js index 3b852a93..8528f6b0 100644 --- a/app/renderer/js/main.js +++ b/app/renderer/js/main.js @@ -5,7 +5,8 @@ const {ipcRenderer} = require('electron'); const DomainUtil = require(__dirname + '/js/utils/domain-util.js'); const WebView = require(__dirname + '/js/components/webview.js'); -const Tab = require(__dirname + '/js/components/tab.js'); +const ServerTab = require(__dirname + '/js/components/server-tab.js'); +const FunctionalTab = require(__dirname + '/js/components/functional-tab.js'); class ServerManagerView { constructor() { @@ -17,21 +18,19 @@ class ServerManagerView { this.$settingsButton = $actionsContainer.querySelector('#settings-action'); this.$content = document.getElementById('content'); - this.settingsTabIndex = -1; this.activeTabIndex = -1; - this.webviews = []; this.tabs = []; + this.functionalTabs = {}; } init() { - this.domainUtil = new DomainUtil(); this.initTabs(); this.initActions(); this.registerIpcs(); } initTabs() { - const servers = this.domainUtil.getDomains(); + const servers = DomainUtil.getDomains(); if (servers.length > 0) { for (let i = 0; i < servers.length; i++) { this.initServer(servers[i], i); @@ -43,94 +42,115 @@ class ServerManagerView { } initServer(server, index) { - this.tabs.push(new Tab({ - url: server.url, - name: server.alias, + this.tabs.push(new ServerTab({ icon: server.icon, - type: Tab.SERVER_TAB, $root: this.$tabsContainer, - onClick: this.activateTab.bind(this, index) - })); - this.webviews.push(new WebView({ - $root: this.$content, - index, - url: server.url, - name: server.alias, - isActive: () => { - return index === this.activeTabIndex; - }, - onTitleChange: this.updateBadge.bind(this), - nodeIntegration: false + onClick: this.activateTab.bind(this, index), + webview: new WebView({ + $root: this.$content, + index, + url: server.url, + name: server.alias, + isActive: () => { + return index === this.activeTabIndex; + }, + onTitleChange: this.updateBadge.bind(this), + nodeIntegration: false + }) })); } initActions() { this.$reloadButton.addEventListener('click', () => { - this.webviews[this.activeTabIndex].reload(); + this.tabs[this.activeTabIndex].webview.reload(); }); 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); + openFunctionalTab(tabProps) { + if (this.functionalTabs[tabProps.name]) { + this.activateTab(this.functionalTabs[tabProps.name]); return; } - const url = 'file://' + __dirname + '/preference.html'; - this.settingsTabIndex = this.webviews.length; + this.functionalTabs[tabProps.name] = this.tabs.length; - this.tabs.push(new Tab({ - url, - name: 'Settings', - type: Tab.SETTINGS_TAB, + this.tabs.push(new FunctionalTab({ + materialIcon: tabProps.materialIcon, $root: this.$tabsContainer, - onClick: this.activateTab.bind(this, this.settingsTabIndex) + onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]), + onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]), + webview: new WebView({ + $root: this.$content, + index: this.functionalTabs[tabProps.name], + url: tabProps.url, + name: tabProps.name, + isActive: () => { + return this.functionalTabs[tabProps.name] === this.activeTabIndex; + }, + onTitleChange: this.updateBadge.bind(this), + nodeIntegration: true + }) })); - this.webviews.push(new WebView({ - $root: this.$content, - index: this.settingsTabIndex, - url, - name: 'Settings', - isActive: () => { - return this.settingsTabIndex === this.activeTabIndex; - }, - onTitleChange: this.updateBadge.bind(this), - nodeIntegration: true - })); - - this.activateTab(this.settingsTabIndex); + this.activateTab(this.functionalTabs[tabProps.name]); } - activateTab(index) { - if (this.webviews[index].loading) { + openSettings() { + this.openFunctionalTab({ + name: 'Settings', + materialIcon: 'settings', + url: `file://${__dirname}/preference.html` + }); + } + + openAbout() { + this.openFunctionalTab({ + name: 'About', + materialIcon: 'sentiment_very_satisfied', + url: `file://${__dirname}/about.html` + }); + } + + activateTab(index, hideOldTab = true) { + if (this.tabs[index].loading) { return; } if (this.activeTabIndex !== -1) { if (this.activeTabIndex === index) { return; - } else { + } else if (hideOldTab) { this.tabs[this.activeTabIndex].deactivate(); - this.webviews[this.activeTabIndex].hide(); } } - this.tabs[index].activate(); - this.activeTabIndex = index; - this.webviews[index].load(); + this.tabs[index].activate(); + } + + destroyTab(name, index) { + if (this.tabs[index].loading) { + return; + } + + this.tabs[index].destroy(); + + delete this.tabs[index]; + delete this.functionalTabs[name]; + + this.activateTab(0, false); } updateBadge() { let messageCountAll = 0; - for (let i = 0; i < this.webviews.length; i++) { - const count = this.webviews[i].badgeCount; - messageCountAll += count; - - this.tabs[i].updateBadge(count); + for (let i = 0; i < this.tabs.length; i++) { + if (this.tabs[i] && this.tabs[i].updateBadge) { + const count = this.tabs[i].webview.badgeCount; + messageCountAll += count; + this.tabs[i].updateBadge(count); + } } ipcRenderer.send('update-badge', messageCountAll); @@ -152,7 +172,7 @@ class ServerManagerView { for (const key in webviewListeners) { ipcRenderer.on(key, () => { - const activeWebview = this.webviews[this.activeTabIndex]; + const activeWebview = this.tabs[this.activeTabIndex].webview; if (activeWebview) { activeWebview[webviewListeners[key]](); } @@ -160,6 +180,7 @@ class ServerManagerView { } ipcRenderer.on('open-settings', this.openSettings.bind(this)); + ipcRenderer.on('open-about', this.openAbout.bind(this)); } } diff --git a/app/renderer/js/preference.js b/app/renderer/js/pages/preference.js similarity index 95% rename from app/renderer/js/preference.js rename to app/renderer/js/pages/preference.js index b4073850..f758b3ae 100644 --- a/app/renderer/js/preference.js +++ b/app/renderer/js/pages/preference.js @@ -13,13 +13,12 @@ class PreferenceView { } init() { - this.domainUtil = new DomainUtil(); this.initServers(); this.initActions(); } initServers() { - const servers = this.domainUtil.getDomains(); + const servers = DomainUtil.getDomains(); this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!'; this.initNewServerForm(); @@ -64,7 +63,7 @@ class PreferenceView { `; this.$serverInfoContainer.appendChild(this.insertNode(serverInfoTemplate)); document.getElementById(`delete-server-action-${index}`).addEventListener('click', () => { - this.domainUtil.removeDomain(index); + DomainUtil.removeDomain(index); this.initServers(); // alert('Success. Reload to apply changes.'); ipcRenderer.send('reload-main'); @@ -109,13 +108,13 @@ class PreferenceView { this.$newServerButton.classList.add('hidden'); }); this.$saveServerButton.addEventListener('click', () => { - this.domainUtil.checkDomain(this.$newServerUrl.value).then(domain => { + DomainUtil.checkDomain(this.$newServerUrl.value).then(domain => { const server = { alias: this.$newServerAlias.value, url: domain, icon: this.$newServerIcon.value }; - this.domainUtil.addDomain(server); + DomainUtil.addDomain(server); this.$saveServerButton.classList.add('hidden'); this.$newServerButton.classList.remove('hidden'); this.$newServerForm.classList.add('hidden'); diff --git a/app/renderer/js/tray.js b/app/renderer/js/tray.js index 4a5cc46f..49dec9e7 100644 --- a/app/renderer/js/tray.js +++ b/app/renderer/js/tray.js @@ -117,7 +117,7 @@ const createTray = function () { const contextMenu = Menu.buildFromTemplate([{ label: 'About', click() { - ipcRenderer.send('trayabout'); + sendAction('open-about'); } }, { diff --git a/app/renderer/js/utils/domain-util.js b/app/renderer/js/utils/domain-util.js index a6ca4592..af6154ad 100644 --- a/app/renderer/js/utils/domain-util.js +++ b/app/renderer/js/utils/domain-util.js @@ -4,9 +4,17 @@ 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'; +let instance = null; + +const defaultIconUrl = __dirname + '../../../img/icon.png'; class DomainUtil { constructor() { + if (instance) { + return instance; + } else { + instance = this; + } + this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); // Migrate from old schema if (this.db.getData('/').domain) { @@ -16,6 +24,8 @@ class DomainUtil { }); this.db.delete('/domain'); } + + return instance; } getDomains() { @@ -69,4 +79,4 @@ class DomainUtil { } } -module.exports = DomainUtil; +module.exports = new DomainUtil(); diff --git a/app/renderer/js/utils/link-util.js b/app/renderer/js/utils/link-util.js new file mode 100644 index 00000000..f6ed9dab --- /dev/null +++ b/app/renderer/js/utils/link-util.js @@ -0,0 +1,29 @@ +'use strict'; + +const wurl = require('wurl'); + +let instance = null; + +class LinkUtil { + constructor() { + if (instance) { + return instance; + } else { + instance = this; + } + + return instance; + } + + isInternal(currentUrl, newUrl) { + const currentDomain = wurl('hostname', currentUrl); + const newDomain = wurl('hostname', newUrl); + + const skipImages = '.jpg|.gif|.png|.jpeg|.JPG|.PNG'; + + // We'll be needing this to open images in default browser + return (currentDomain === newDomain) && !newUrl.match(skipImages); + } +} + +module.exports = new LinkUtil(); diff --git a/app/renderer/js/utils/system-util.js b/app/renderer/js/utils/system-util.js index 57869e8d..e2b97bc7 100644 --- a/app/renderer/js/utils/system-util.js +++ b/app/renderer/js/utils/system-util.js @@ -52,4 +52,4 @@ class SystemUtil { } } -module.exports = SystemUtil; +module.exports = new SystemUtil(); diff --git a/app/renderer/preference.html b/app/renderer/preference.html index dd2588c8..a6c19183 100644 --- a/app/renderer/preference.html +++ b/app/renderer/preference.html @@ -41,5 +41,5 @@ - +