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/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/store/Services.js b/app/store/Services.js index 81a2f441..62eeae81 100644 --- a/app/store/Services.js +++ b/app/store/Services.js @@ -33,8 +33,10 @@ Ext.define('Rambox.store.Services', { ,icon: service.get('type') !== 'custom' ? 'resources/icons/'+service.get('logo') : ( service.get('logo') === '' ? 'resources/icons/custom.png' : service.get('logo')) ,src: service.get('url') ,type: service.get('type') - ,muted: service.get('muted') - ,enabled: service.get('enabled') + ,muted: service.get('muted'), + includeInGlobalUnreadCounter: service.get('includeInGlobalUnreadCounter'), + displayTabUnreadCounter: service.get('displayTabUnreadCounter'), + enabled: service.get('enabled') ,record: service ,tabConfig: { service: service diff --git a/app/store/ServicesList.js b/app/store/ServicesList.js index 4120d9f5..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: 'xing' + ,logo: 'xing.png' + ,name: 'XING' + ,description: 'Career-oriented social networking' + ,url: 'https://www.xing.com/messages/conversations' + ,type: 'messaging' + ,js_unread: '(function() { let originalTitle = document.title; function checkUnread() { let count = null; let notificationElement = document.querySelector(\'[data-update="unread_conversations"]\'); if (notificationElement && notificationElement.style.display !== \'none\') { count = parseInt(notificationElement.textContent.trim(), 10); } updateBadge(count); } function updateBadge(count) { if (count && count >= 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 11df8302..6583a01b 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; @@ -44,8 +47,7 @@ Ext.define('Rambox.ux.WebView',{ ,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(); @@ -169,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; @@ -342,21 +308,78 @@ Ext.define('Rambox.ux.WebView',{ 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) { + 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); } } @@ -402,7 +425,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..f24d77dc 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' 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..5b2d6f82 100644 --- a/electron/main.js +++ b/electron/main.js @@ -23,10 +23,9 @@ const config = new Config({ always_on_top: false ,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' - ,start_minimized: false + ,auto_launch: !isDev, + window_close_behavior: 'keep_in_tray', + start_minimized: false ,systemtray_indicator: true ,master_password: false ,disable_gpu: process.platform === 'linux' @@ -44,7 +43,7 @@ const config = new Config({ // Configure AutoLaunch const appLauncher = new AutoLaunch({ - name: 'Rambox' + name: 'Rambox.app' ,isHiddenOnLaunch: config.get('start_minimized') }); config.get('auto_launch') && !isDev ? appLauncher.enable() : appLauncher.disable(); @@ -199,11 +198,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/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