diff --git a/Backers.md b/Backers.md index bff2e750..485bd372 100644 --- a/Backers.md +++ b/Backers.md @@ -3,3 +3,4 @@ [Martin Grünbaum](https://github.com/alathon) Ivan Toshkov +[Simon Joda Stößer](https://github.com/SimJoSt) diff --git a/README.md b/README.md index 37509ba1..83278210 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Gmail Inbox HipChat + ChatWork GroupMe Grape @@ -57,6 +58,7 @@ Outlook Outlook 365 TutaNota + Hushmail BearyChat Aol @@ -67,6 +69,7 @@ Missive Yahoo! Mail Ryver + Yandex Mail Dasher DingTalk @@ -77,6 +80,7 @@ Yahoo! Messenger mysms ICQ + TweetDeck Zinc FreeNode @@ -87,6 +91,7 @@ Horde SquirrelMail Zimbra + Hootsuite Amium RainLoop @@ -97,6 +102,7 @@ Crisp Flock Openmailbox + Typetalk Drift mmmelon @@ -107,7 +113,9 @@ Riot Pushbullet Movim + Kaiwa +XING































@@ -152,6 +160,35 @@ Want to report a bug, request a feature, contribute to or translate Rambox? We n If you're comfortable getting up and running from a `git clone`, this method is for you. +## Adding a service + +The available services are stored in the [ServiceList.js](app/store/ServicesList.js). +Structure of a service entry: + +|Name|Description|Required| +|---|---|---| +|id|Unique identifier for the service, e.g. "slack"|yes| +|logo|File name of the service logo located in "/resources/icons/", e.g. "slack.png"|yes| +|name|Visible name for the service, e.g. "Slack"|yes| +|description|A short description of the service, e.g. "Slack brings all your communication together..."|yes| +|url|URL of the service, e.g. "https://\_\_\_.slack.com/". "\_\_\_" may be used as a placeholder, that can be configured when adding a service.|yes| +|type|Defines the type of the service. Must be one of `email` or `messaging`.|yes| +|allow_popups|Set to `true` to allow popup windows for the service.|no| +|note|Additional info to display when adding the service.|no| +|manual_notifications|Set to `true` to let Rambox trigger notifications. Can be used for services that doesn't support browser notifications.|no| +|js_unread|JavaScript code for setting the unread count (see below).|no| +|dont_update_unread_from_title|Set to `true` to prevent updating the unread count from the window title (see below).|no| + +### Setting the unread count + +While there is also a way to set the unread count by adding ` (COUNT)` to the window title, this describes the preferred way of doing it: + +First set `dont_update_unread_from_title` in the service config to `true`. + +Code provided by `js_unread` will be injected into the service website. +You can retrieve the unread count in this JavaScript code e.g. by parsing elements. +Set the unread count by calling `rambox.setUnreadCount(COUNT)` or clear it by calling `rambox.clearUnreadCount()`. + #### Technologies: * Sencha Ext JS 5.1.1.451 diff --git a/app/Application.js b/app/Application.js index 43fdc2b3..806924db 100644 --- a/app/Application.js +++ b/app/Application.js @@ -44,9 +44,14 @@ Ext.define('Rambox.Application', { var tabPanel = Ext.cq1('app-main'); var activeIndex = tabPanel.items.indexOf(tabPanel.getActiveTab()); var i = activeIndex + 1; - if ( i >= tabPanel.items.items.length - 1 ) i = 0; - while ( tabPanel.items.items[i].id === 'tbfill' ) i++; - tabPanel.setActiveTab( i ); + + // "cycle" (go to the start) when the end is reached or the end is the spacer "tbfill" + if (i === tabPanel.items.items.length || i === tabPanel.items.items.length - 1 && tabPanel.items.items[i].id === 'tbfill') i = 0; + + // skip spacer + while (tabPanel.items.items[i].id === 'tbfill') i++; + + tabPanel.setActiveTab(i); } } ,{ diff --git a/app/model/Service.js b/app/model/Service.js index 41eb4de2..fe35526b 100644 --- a/app/model/Service.js +++ b/app/model/Service.js @@ -37,7 +37,18 @@ Ext.define('Rambox.model.Service', { name: 'muted' ,type: 'boolean' ,defaultValue: false - },{ + }, + { + name: 'displayTabUnreadCounter', + type: 'boolean', + defaultValue: true + }, + { + name: 'includeInGlobalUnreadCounter', + type: 'boolean', + defaultValue: true + }, + { name: 'trust' ,type: 'boolean' ,defaultValue: false diff --git a/app/package.json b/app/package.json index 74613555..2b5cc84b 100644 --- a/app/package.json +++ b/app/package.json @@ -33,6 +33,7 @@ "firebase": "^3.0.5", "firebase-token-generator": "^2.0.0", "tmp": "0.0.28", + "electron-spell-check-provider": "^1.0.0", "mime": "^1.3.4", "electron-is-dev": "^0.1.1", "electron-config": "0.2.1" diff --git a/app/store/Services.js b/app/store/Services.js index 81a2f441..6ab34ffe 100644 --- a/app/store/Services.js +++ b/app/store/Services.js @@ -26,6 +26,19 @@ Ext.define('Rambox.store.Services', { var servicesLeft = []; var servicesRight = []; store.each(function(service) { + // Fix some services with bad IDs + // TODO: Remove in next release + switch ( service.get('type') ) { + case 'office365': + service.set('type', 'outlook365'); + break; + case ' irccloud': + service.set('type', 'irccloud'); + break; + default: + break; + } + var cfg = { xtype: 'webview' ,id: 'tab_'+service.get('id') @@ -34,6 +47,8 @@ Ext.define('Rambox.store.Services', { ,src: service.get('url') ,type: service.get('type') ,muted: service.get('muted') + ,includeInGlobalUnreadCounter: service.get('includeInGlobalUnreadCounter') + ,displayTabUnreadCounter: service.get('displayTabUnreadCounter') ,enabled: service.get('enabled') ,record: service ,tabConfig: { diff --git a/app/store/ServicesList.js b/app/store/ServicesList.js index 4047e9b1..4499a500 100644 --- a/app/store/ServicesList.js +++ b/app/store/ServicesList.js @@ -95,7 +95,7 @@ Ext.define('Rambox.store.ServicesList', { ,url: 'https://web.telegram.org/' ,type: 'messaging' ,titleBlink: true - ,js_unread: 'function checkUnread(){var e=document.getElementsByClassName("im_dialog_badge badge"),t=0;for(i=0;i= 1) { rambox.setUnreadCount(count); } else { rambox.clearUnreadCount(); } } setInterval(checkUnread, 3000); checkUnread(); })();' + ,dont_update_unread_from_title: true + }, + { + id: 'Workplace' + ,logo: 'workplace.png' + ,name: 'Workplace by Facebook' + ,description: 'Connect everyone in your company and turn ideas into action. Through group discussion, a personalised News Feed, and voice and video calling, work together and get more done. Workplace is an ad-free space, separate from your personal Facebook account.' + ,url: 'https://___.facebook.com/' + ,type: 'messaging' } - ] + ] }); diff --git a/app/util/Notifier.js b/app/util/Notifier.js new file mode 100644 index 00000000..5d0da406 --- /dev/null +++ b/app/util/Notifier.js @@ -0,0 +1,57 @@ + +/** + * Singleton class for notification dispatching. + */ +Ext.define('Rambox.util.Notifier', { + + singleton: true, + + constructor: function(config) { + + config = config || {}; + + /** + * Returns the notification text depending on the service type. + * + * @param view + * @param count + * @return {*} + */ + function getNotificationText(view, count) { + var text; + switch (Ext.getStore('ServicesList').getById(view.type).get('type')) { + case 'messaging': + text = 'You have ' + Ext.util.Format.plural(count, 'new message', 'new messages') + '.'; + break; + case 'email': + text = 'You have ' + Ext.util.Format.plural(count, 'new email', 'new emails') + '.'; + break; + default: + text = 'You have ' + Ext.util.Format.plural(count, 'new activity', 'new activities') + '.'; + break; + } + return text; + } + + /** + * Dispatches a notification for a specific service. + * + * @param view The view of the service + * @param {number} count The unread count + */ + this.dispatchNotification = function(view, count) { + var text = getNotificationText(view, count); + + var notification = new Notification(view.record.get('name'), { + body: text, + icon: view.tab.icon, + silent: view.record.get('muted') + }); + + notification.onclick = function() { + require('electron').remote.getCurrentWindow().show(); + Ext.cq1('app-main').setActiveTab(view); + }; + } + } +}); diff --git a/app/util/UnreadCounter.js b/app/util/UnreadCounter.js new file mode 100644 index 00000000..46617b0f --- /dev/null +++ b/app/util/UnreadCounter.js @@ -0,0 +1,75 @@ +/** + * Singleton class to handle the global unread counter. + */ +Ext.define('Rambox.util.UnreadCounter', { + + singleton: true, + + constructor: function(config) { + + config = config || {}; + + /** + * Map for storing the global unread count. + * service id -> unread count + * + * @type {Map} + */ + var unreadCountByService = new Map(); + + /** + * Holds the global unread count for internal usage. + * + * @type {number} + */ + var totalUnreadCount = 0; + + /** + * Sets the application's unread count to tracked unread count. + */ + function updateAppUnreadCounter() { + Rambox.app.setTotalNotifications(totalUnreadCount); + } + + /** + * Returns the global unread count. + * + * @return {number} + */ + this.getTotalUnreadCount = function() { + return totalUnreadCount; + }; + + /** + * Sets the global unread count for a specific service. + * + * @param {*} id Id of the service to set the global unread count for. + * @param {number} unreadCount The global unread count for the service. + */ + this.setUnreadCountForService = function(id, unreadCount) { + unreadCount = parseInt(unreadCount, 10); + + if (unreadCountByService.has(id)) { + totalUnreadCount -= unreadCountByService.get(id); + } + totalUnreadCount += unreadCount; + unreadCountByService.set(id, unreadCount); + + updateAppUnreadCounter(); + }; + + /** + * Clears the global unread count for a specific service. + * + * @param {*} id Id of the service to clear the global unread count for. + */ + this.clearUnreadCountForService = function(id) { + if (unreadCountByService.has(id)) { + totalUnreadCount -= unreadCountByService.get(id); + } + unreadCountByService['delete'](id); + + updateAppUnreadCounter(); + } + } +}); diff --git a/app/ux/WebView.js b/app/ux/WebView.js index e681b331..8cdfa066 100644 --- a/app/ux/WebView.js +++ b/app/ux/WebView.js @@ -6,14 +6,17 @@ Ext.define('Rambox.ux.WebView',{ ,xtype: 'webview' ,requires: [ - 'Rambox.util.Format' - ] + 'Rambox.util.Format', + 'Rambox.util.Notifier', + 'Rambox.util.UnreadCounter' + ], // private - ,zoomLevel: 0 + zoomLevel: 0, + currentUnreadCount: 0, // CONFIG - ,hideMode: 'offsets' + hideMode: 'offsets' ,initComponent: function(config) { var me = this; @@ -36,16 +39,15 @@ Ext.define('Rambox.ux.WebView',{ Ext.apply(me, { items: me.webViewConstructor() ,title: me.record.get('name') - ,icon: me.record.get('type') === 'custom' ? (me.record.get('logo') === '' ? 'resources/icons/custom.png' : me.record.get('logo')) : 'resources/icons/'+me.record.get('logo') - ,src: me.record.get('url') - ,type: me.record.get('type') - ,align: me.record.get('align') - ,notifications: me.record.get('notifications') - ,muted: me.record.get('muted') + ,icon: me.record.get('type') === 'custom' ? (me.record.get('logo') === '' ? 'resources/icons/custom.png' : me.record.get('logo')) : 'resources/icons/'+me.record.get('logo') + ,src: me.record.get('url') + ,type: me.record.get('type') + ,align: me.record.get('align') + ,notifications: me.record.get('notifications') + ,muted: me.record.get('muted') ,tabConfig: { listeners: { - badgetextchange: me.onBadgeTextChange - ,afterrender : function( btn ) { + afterrender : function( btn ) { btn.el.on('contextmenu', function(e) { btn.showMenu('contextmenu'); e.stopEvent(); @@ -158,7 +160,8 @@ Ext.define('Rambox.ux.WebView',{ ,autosize: 'on' ,disablewebsecurity: 'on' ,blinkfeatures: 'ApplicationCache,GlobalCacheStorage' - ,useragent: Ext.getStore('ServicesList').getById(me.record.get('type')).get('userAgent') + ,useragent: Ext.getStore('ServicesList').getById(me.record.get('type')).get('userAgent'), + preload: './resources/js/rambox-service-api.js' } }; @@ -168,42 +171,6 @@ Ext.define('Rambox.ux.WebView',{ return cfg; } - ,onBadgeTextChange: function( tab, badgeText, oldBadgeText ) { - var me = this; - if ( oldBadgeText === null ) oldBadgeText = 0; - var actualNotifications = Rambox.app.getTotalNotifications(); - - oldBadgeText = Rambox.util.Format.stripNumber(oldBadgeText); - badgeText = Rambox.util.Format.stripNumber(badgeText); - - Rambox.app.setTotalNotifications(actualNotifications - oldBadgeText + badgeText); - - // Some services dont have Desktop Notifications, so we add that functionality =) - if ( Ext.getStore('ServicesList').getById(me.type).get('manual_notifications') && oldBadgeText < badgeText && me.record.get('notifications') && !JSON.parse(localStorage.getItem('dontDisturb')) ) { - var text; - switch ( Ext.getStore('ServicesList').getById(me.type).get('type') ) { - case 'messaging': - text = 'You have ' + Ext.util.Format.plural(badgeText, 'new message', 'new messages') + '.'; - break; - case 'email': - text = 'You have ' + Ext.util.Format.plural(badgeText, 'new email', 'new emails') + '.'; - break; - default: - text = 'You have ' + Ext.util.Format.plural(badgeText, 'new activity', 'new activities') + '.'; - break; - } - var not = new Notification(me.record.get('name'), { - body: text - ,icon: tab.icon - ,silent: me.record.get('muted') - }); - not.onclick = function() { - require('electron').remote.getCurrentWindow().show(); - Ext.cq1('app-main').setActiveTab(me); - }; - } - } - ,onAfterRender: function() { var me = this; @@ -274,7 +241,7 @@ Ext.define('Rambox.ux.WebView',{ // Injected code to detect new messages if ( me.record ) { - var js_unread = Ext.getStore('ServicesList').getById(me.record.get('type') === 'office365' ? 'outlook365' : me.record.get('type')).get('js_unread'); + var js_unread = Ext.getStore('ServicesList').getById(me.record.get('type')).get('js_unread'); js_unread = js_unread + me.record.get('js_unread'); if ( js_unread !== '' ) { console.groupCollapsed(me.record.get('type').toUpperCase() + ' - JS Injected to Detect New Messages'); @@ -296,26 +263,129 @@ Ext.define('Rambox.ux.WebView',{ webview.executeJavaScript('document.body.scrollTop=0;'); }); - webview.addEventListener("page-title-updated", function(e) { - var count = e.title.match(/\(([^)]+)\)/); // Get text between (...) + webview.addEventListener('ipc-message', function(event) { + var channel = event.channel; + switch (channel) { + case 'rambox.setUnreadCount': + handleSetUnreadCount(event); + break; + case 'rambox.clearUnreadCount': + handleClearUnreadCount(event); + break; + } + + /** + * Handles 'rambox.clearUnreadCount' messages. + * Clears the unread count. + */ + function handleClearUnreadCount() { + me.tab.setBadgeText(''); + } + + /** + * Handles 'rambox.setUnreadCount' messages. + * Sets the badge text if the event contains an integer as first argument. + * + * @param event + */ + function handleSetUnreadCount(event) { + if (Array.isArray(event.args) === true && event.args.length > 0) { + var count = event.args[0]; + if (count === parseInt(count, 10)) { + me.tab.setBadgeText(Rambox.util.Format.formatNumber(count)); + } + } + } + }); + + /** + * Register page title update event listener only for services that don't prevent it by setting 'dont_update_unread_from_title' to true. + */ + if (Ext.getStore('ServicesList').getById(me.record.get('type')).get('dont_update_unread_from_title') !== true) { + webview.addEventListener("page-title-updated", function(e) { + var count = e.title.match(/\(([^)]+)\)/); // Get text between (...) count = count ? count[1] : '0'; count = count === '•' ? count : Ext.isArray(count.match(/\d+/g)) ? count.match(/\d+/g).join("") : count.match(/\d+/g); // Some services have special characters. Example: (•) count = count === null ? '0' : count; - me.tab.setBadgeText(Rambox.util.Format.formatNumber(count)); - }); + me.setUnreadCount(count); + }); + } webview.addEventListener('did-get-redirect-request', function( e ) { if ( e.isMainFrame ) webview.loadURL(e.newURL); }); - } - ,reloadService: function(btn) { + if(ipc.sendSync('getConfig').spellcheck) { + var webFrame = require('electron').webFrame; + var SpellCheckProvider = require('electron-spell-check-provider'); + webFrame.setSpellCheckProvider('en-US', true, new SpellCheckProvider('en-US')); + } + }, + + setUnreadCount: function(newUnreadCount) { + var me = this; + + if (me.record.get('includeInGlobalUnreadCounter') === true) { + Rambox.util.UnreadCounter.setUnreadCountForService(me.record.get('id'), newUnreadCount); + } else { + Rambox.util.UnreadCounter.clearUnreadCountForService(me.record.get('id')); + } + + me.setTabBadgeText(Rambox.util.Format.formatNumber(newUnreadCount)); + + /** + * Dispatch manual notification if + * • service doesn't have notifications, so Rambox does them + * • count increased + * • not in dnd mode + * • notifications enabled + */ + if (Ext.getStore('ServicesList').getById(me.type).get('manual_notifications') && + me.currentUnreadCount < newUnreadCount && + me.record.get('notifications') && + !JSON.parse(localStorage.getItem('dontDisturb'))) { + Rambox.util.Notifier.dispatchNotification(me, newUnreadCount); + } + + me.currentUnreadCount = newUnreadCount; + }, + + refreshUnreadCount: function() { + this.setUnreadCount(this.currentUnreadCount); + }, + + /** + * Sets the tab badge text depending on the service config param "displayTabUnreadCounter". + * + * @param {string} badgeText + */ + setTabBadgeText: function(badgeText) { + var me = this; + if (me.record.get('displayTabUnreadCounter') === true) { + me.tab.setBadgeText(badgeText); + } else { + me.tab.setBadgeText(''); + } + }, + + /** + * Clears the unread counter for this view: + * • clears the badge text + * • clears the global unread counter + */ + clearUnreadCounter: function() { + var me = this; + me.tab.setBadgeText(''); + Rambox.util.UnreadCounter.clearUnreadCountForService(me.record.get('id')); + }, + + reloadService: function(btn) { var me = this; var webview = me.down('component').el.dom; if ( me.record.get('enabled') ) { - me.tab.setBadgeText(''); + me.clearUnreadCounter(); webview.loadURL(me.src); } } @@ -361,7 +431,8 @@ Ext.define('Rambox.ux.WebView',{ ,setEnabled: function(enabled) { var me = this; - me.tab.setBadgeText(''); + me.clearUnreadCounter(); + me.removeAll(); me.add(me.webViewConstructor(enabled)); if ( enabled ) { diff --git a/app/view/add/Add.js b/app/view/add/Add.js index 8e32e81f..26790f9b 100644 --- a/app/view/add/Add.js +++ b/app/view/add/Add.js @@ -162,8 +162,31 @@ Ext.define('Rambox.view.add.Add',{ ,inputValue: true } ] - } - ,{ + }, + { + xtype: 'fieldset', + title: 'Unread counter', + margin: '10 0 0 0', + items: [ + { + xtype: 'checkbox', + boxLabel: 'Display tab unread counter', + name: 'displayTabUnreadCounter', + checked: me.edit ? me.record.get('displayTabUnreadCounter') : true, + uncheckedValue: false, + inputValue: true + }, + { + xtype: 'checkbox', + boxLabel: 'Include in global unread counter', + name: 'includeInGlobalUnreadCounter', + checked: me.edit ? me.record.get('includeInGlobalUnreadCounter') : true, + uncheckedValue: false, + inputValue: true + } + ] + }, + { xtype: 'fieldset' ,title: 'Advanced' ,margin: '10 0 0 0' diff --git a/app/view/add/AddController.js b/app/view/add/AddController.js index 97ce0a73..5c4e6cbb 100644 --- a/app/view/add/AddController.js +++ b/app/view/add/AddController.js @@ -1,8 +1,12 @@ Ext.define('Rambox.view.add.AddController', { - extend: 'Ext.app.ViewController' - ,alias: 'controller.add-add' + extend: 'Ext.app.ViewController', + alias: 'controller.add-add', - ,doCancel: function( btn ) { + requires: [ + 'Rambox.util.UnreadCounter' + ], + + doCancel: function( btn ) { var me = this; me.getView().close(); @@ -29,37 +33,44 @@ Ext.define('Rambox.view.add.AddController', { ,url: formValues.url ,align: formValues.align ,notifications: formValues.notifications - ,muted: formValues.muted - ,trust: formValues.trust + ,muted: formValues.muted, + displayTabUnreadCounter: formValues.displayTabUnreadCounter, + includeInGlobalUnreadCounter: formValues.includeInGlobalUnreadCounter, + trust: formValues.trust ,js_unread: formValues.js_unread }); + + var view = Ext.getCmp('tab_'+win.record.get('id')); + // Change the title of the Tab - Ext.getCmp('tab_'+win.record.get('id')).setTitle(formValues.serviceName); + view.setTitle(formValues.serviceName); // Change sound of the Tab - Ext.getCmp('tab_'+win.record.get('id')).setAudioMuted(formValues.muted); + view.setAudioMuted(formValues.muted); // Change notifications of the Tab - Ext.getCmp('tab_'+win.record.get('id')).setNotifications(formValues.notifications); + view.setNotifications(formValues.notifications); // Change the icon of the Tab if ( win.record.get('type') === 'custom' && oldData.logo !== formValues.logo ) Ext.getCmp('tab_'+win.record.get('id')).setConfig('icon', formValues.logo === '' ? 'resources/icons/custom.png' : formValues.logo); // Change the URL of the Tab - if ( oldData.url !== formValues.url ) Ext.getCmp('tab_'+win.record.get('id')).setURL(formValues.url); + if ( oldData.url !== formValues.url ) view.setURL(formValues.url); // Change the align of the Tab if ( oldData.align !== formValues.align ) { if ( formValues.align === 'left' ) { - Ext.cq1('app-main').moveBefore(Ext.getCmp('tab_'+win.record.get('id')), Ext.getCmp('tbfill')); + Ext.cq1('app-main').moveBefore(view, Ext.getCmp('tbfill')); } else { - Ext.cq1('app-main').moveAfter(Ext.getCmp('tab_'+win.record.get('id')), Ext.getCmp('tbfill')); + Ext.cq1('app-main').moveAfter(view, Ext.getCmp('tbfill')); } } // Apply the JS Code of the Tab if ( win.down('textarea').isDirty() ) { Ext.Msg.confirm('CUSTOM CODE', 'Rambox needs to reload the service to execute the new JavaScript code. Do you want to do it now?', function( btnId ) { - if ( btnId === 'yes' ) Ext.getCmp('tab_'+win.record.get('id')).reloadService(); + if ( btnId === 'yes' ) view.reloadService(); }); } - Ext.getCmp('tab_'+win.record.get('id')).record = win.record; - Ext.getCmp('tab_'+win.record.get('id')).tabConfig.service = win.record; + view.record = win.record; + view.tabConfig.service = win.record; + + view.refreshUnreadCount(); } else { // Format data if ( win.record.get('url').indexOf('___') >= 0 ) { @@ -73,8 +84,10 @@ Ext.define('Rambox.view.add.AddController', { ,url: formValues.url ,align: formValues.align ,notifications: formValues.notifications - ,muted: formValues.muted - ,trust: formValues.trust + ,muted: formValues.muted, + displayTabUnreadCounter: formValues.displayTabUnreadCounter, + includeInGlobalUnreadCounter: formValues.includeInGlobalUnreadCounter, + trust: formValues.trust ,js_unread: formValues.js_unread }); service.save(); @@ -121,5 +134,4 @@ Ext.define('Rambox.view.add.AddController', { // Make focus to the name field win.down('textfield[name="serviceName"]').focus(true, 100); } - }); diff --git a/app/view/preferences/Preferences.js b/app/view/preferences/Preferences.js index 749340a1..42cd0a8e 100644 --- a/app/view/preferences/Preferences.js +++ b/app/view/preferences/Preferences.js @@ -67,17 +67,28 @@ Ext.define('Rambox.view.preferences.Preferences',{ ,boxLabel: 'Show in Taskbar' ,value: config.skip_taskbar ,reference: 'skipTaskbar' - ,hidden: process.platform !== 'win32' - } - ,{ - xtype: 'checkbox' - ,name: 'keep_in_taskbar_on_close' - ,boxLabel: 'Keep Rambox in the Taskbar when close it' - ,value: config.keep_in_taskbar_on_close - ,bind: { disabled: '{!skipTaskbar.checked}' } - ,hidden: process.platform !== 'win32' - } - ,{ + ,hidden: process.platform === 'darwin' + }, + { + xtype: 'combo', + name: 'window_close_behavior', + fieldLabel: 'When closing the main window', + labelAlign: 'top', + value: config.window_close_behavior, + displayField: 'label', + valueField: 'value', + editable: false, + store: Ext.create('Ext.data.Store', { + fields: ['value', 'label'], + data : [ + { 'value': 'keep_in_tray', 'label': 'Keep in tray' }, + { 'value': 'keep_in_tray_and_taskbar', 'label': 'Keep in tray and taskbar' }, + { 'value': 'quit', 'label': 'Quit' } + ] + }), + hidden: process.platform === 'darwin' + }, + { xtype: 'checkbox' ,name: 'always_on_top' ,boxLabel: 'Always on top' @@ -96,6 +107,12 @@ Ext.define('Rambox.view.preferences.Preferences',{ ,boxLabel: 'Disable Hardware Acceleration (needs to relaunch)' ,value: config.disable_gpu } + ,{ + xtype: 'checkbox' + ,name: 'spellcheck' + ,boxLabel: 'Enable spellcheck (en_US)' + ,value: config.spellcheck + } ,{ xtype: 'fieldset' ,title: 'Master Password - Ask for password on startup' diff --git a/app/view/preferences/PreferencesController.js b/app/view/preferences/PreferencesController.js index b635e075..c90e9aec 100644 --- a/app/view/preferences/PreferencesController.js +++ b/app/view/preferences/PreferencesController.js @@ -13,12 +13,28 @@ Ext.define('Rambox.view.preferences.PreferencesController', { var values = me.getView().down('form').getForm().getFieldValues(); - // Master Password - if ( values.master_password && (Ext.isEmpty(values.master_password1) || Ext.isEmpty(values.master_password2)) ) return; - if ( values.master_password && (values.master_password1 !== values.master_password2) ) return; - if ( values.master_password ) values.master_password = Rambox.util.MD5.encypt(values.master_password1); - delete values.master_password1; - delete values.master_password2; + // master password activated and only one of the fields "password" or "password confirmation" filled + if (values.master_password === true && + (Ext.isEmpty(values.master_password1) === false && Ext.isEmpty(values.master_password2) === true || + Ext.isEmpty(values.master_password1) === true && Ext.isEmpty(values.master_password2) === false)) return; + + // password and confirmation don't match + if (values.master_password === true && (values.master_password1 !== values.master_password2)) return; + + // master password activated and changed + if (values.master_password === true && + Ext.isEmpty(values.master_password1) === false && + Ext.isEmpty(values.master_password2) === false) { + + values.master_password = Rambox.util.MD5.encypt(values.master_password1); + delete values.master_password1; + delete values.master_password2; + } + + // prevent overwriting password when unchanged + if (values.master_password === true) { + delete values.master_password; + } // Proxy if ( values.proxy && (Ext.isEmpty(values.proxyHost) || Ext.isEmpty(values.proxyPort)) ) return; diff --git a/electron/main.js b/electron/main.js index a261020d..8d1cfeae 100644 --- a/electron/main.js +++ b/electron/main.js @@ -24,12 +24,12 @@ const config = new Config({ ,hide_menu_bar: false ,skip_taskbar: true ,auto_launch: !isDev - // On Linux false because it's uncommon for apps on linux to stay in the taskbar on close - ,keep_in_taskbar_on_close: process.platform !== 'linux' + ,window_close_behavior: 'keep_in_tray' ,start_minimized: false ,systemtray_indicator: true ,master_password: false ,disable_gpu: process.platform === 'linux' + ,spellcheck: false ,proxy: false ,proxyHost: '' ,proxyPort: '' @@ -44,7 +44,7 @@ const config = new Config({ // Configure AutoLaunch const appLauncher = new AutoLaunch({ - name: 'Rambox' + name: process.platform === 'darwin' ? 'Rambox.app' : 'Rambox' ,isHiddenOnLaunch: config.get('start_minimized') }); config.get('auto_launch') && !isDev ? appLauncher.enable() : appLauncher.disable(); @@ -199,11 +199,19 @@ function createWindow () { app.hide(); break; case 'linux': - config.get('keep_in_taskbar_on_close') ? mainWindow.hide() : app.quit(); - break; case 'win32': default: - config.get('keep_in_taskbar_on_close') ? mainWindow.minimize() : mainWindow.hide(); + switch (config.get('window_close_behavior')) { + case 'keep_in_tray': + mainWindow.hide(); + break; + case 'keep_in_tray_and_taskbar': + mainWindow.minimize(); + break; + case 'quit': + app.quit(); + break; + } break; } } diff --git a/overrides/layout/container/boxOverflow/Scroller.js b/overrides/layout/container/boxOverflow/Scroller.js new file mode 100644 index 00000000..69682125 --- /dev/null +++ b/overrides/layout/container/boxOverflow/Scroller.js @@ -0,0 +1,34 @@ +/** + * Per default scrolling the tab bar moves the tabs 20 pixels. + * To improve the usability of the tab bar this value is increased for Rambox. + * Also animations are enabled, so the user understands what's going on. + */ +Ext.define('Rambox.overrides.layout.container.boxOverflow.Scroller', { + override: 'Ext.layout.container.boxOverflow.Scroller', + + scrollIncrement: 250, + wheelIncrement: 50, + + animateScroll: true, + scrollDuration: 250, + + /** + * In difference to the overridden function this one enables scroll animations. + * + * @private + * Scrolls to the left by the configured amount + */ + scrollLeft: function() { + this.scrollBy(-this.scrollIncrement); + }, + + /** + * In difference to the overridden function this one enables scroll animations. + * + * @private + * Scrolls to the right by the configured amount + */ + scrollRight: function() { + this.scrollBy(this.scrollIncrement); + } +}); diff --git a/package.json b/package.json index 707916a1..48abccde 100644 --- a/package.json +++ b/package.json @@ -88,17 +88,18 @@ "app": "build/production/Rambox/" }, "devDependencies": { - "asar": "^0.12.1", - "chai": "3.5.0", - "electron": "1.4.7", - "electron-builder": "6.5.2", - "electron-squirrel-startup": "^1.0.0", - "mocha": "3.2.0", + "asar": "^0.12.1", + "electron": "1.4.14", + "electron-builder": "11.3.0", + "electron-squirrel-startup": "^1.0.0", + "chai": "3.5.0", + "mocha": "3.2.0", "spectron": "3.4.0" }, "dependencies": { "auto-launch": "4.0.0", "firebase": "^3.0.5", + "electron-spell-check-provider": "^1.0.0", "firebase-token-generator": "^2.0.0", "tmp": "0.0.28", "mime": "^1.3.4", diff --git a/resources/icons/workplace.png b/resources/icons/workplace.png new file mode 100644 index 00000000..8f402050 Binary files /dev/null and b/resources/icons/workplace.png differ diff --git a/resources/icons/xing.png b/resources/icons/xing.png new file mode 100644 index 00000000..070b8eb0 Binary files /dev/null and b/resources/icons/xing.png differ diff --git a/resources/js/rambox-service-api.js b/resources/js/rambox-service-api.js new file mode 100644 index 00000000..c37517dd --- /dev/null +++ b/resources/js/rambox-service-api.js @@ -0,0 +1,28 @@ +/** + * This file is loaded in the service web views to provide a Rambox API. + */ + +const { ipcRenderer } = require('electron'); + +/** + * Make the Rambox API available via a global "rambox" variable. + * + * @type {{}} + */ +window.rambox = {}; + +/** + * Sets the unraed count of the tab. + * + * @param {*} count The unread count + */ +window.rambox.setUnreadCount = function(count) { + ipcRenderer.sendToHost('rambox.setUnreadCount', count); +}; + +/** + * Clears the unread count. + */ +window.rambox.clearUnreadCount = function() { + ipcRenderer.sendToHost('rambox.clearUnreadCount'); +}