diff --git a/TODO.md b/TODO.md index abe145d1..b3ebb18f 100644 --- a/TODO.md +++ b/TODO.md @@ -2,9 +2,5 @@ - Change theme. - Deeplink to add new service. -- Auto Updater - Dock Menu (http://electron.atom.io/docs/tutorial/desktop-environment-integration/#custom-dock-menu-os-x) -- Auth0 -- Group services (Personal, Work, etc) - Crush Reporter. -- Add Voxer, Yahoo! Messenger and Dasher. diff --git a/app.js b/app.js index 475f142e..3675f3c2 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,18 @@ +// Initialize Firebase +var firebase = require('firebase/app'); +require('firebase/database'); +require('firebase/auth'); +var config = { + apiKey: "AIzaSyAXedcpudidIUVhvn0jjrMHHWXv7YzWAR0", + authDomain: "rambox-d1326.firebaseapp.com", + databaseURL: "https://rambox-d1326.firebaseio.com", + storageBucket: "rambox-d1326.appspot.com" +}; +var fireRef = firebase.initializeApp(config); // Firebase Ref +var FirebaseTokenGenerator = require('firebase-token-generator'); +var auth0, lock; // Auth0 vars + +// Sencha App Ext.setGlyphFontFamily('FontAwesome'); Ext.application({ name: 'Rambox' @@ -6,3 +21,17 @@ Ext.application({ ,autoCreateViewport: 'Rambox.view.main.Main' }); + +// Syncronize with Firebase +function sync() { + // Is not logged, Skip + if ( !localStorage.getItem('id_token') ) return; + + var services = []; + Ext.getStore('Services').each(function(service) { + services.push(service.data); + }); + fireRef.database().ref('users/' + Ext.decode(localStorage.getItem('profile')).user_id).set({ + services: services + }); +} diff --git a/app/Application.js b/app/Application.js index 21fc3efc..bd670c5c 100644 --- a/app/Application.js +++ b/app/Application.js @@ -14,6 +14,9 @@ Ext.define('Rambox.Application', { } ,launch: function () { + lock = new Auth0Lock('y9am0DVawe2tvlA3ucD7OufpJHZZMjsO', 'rambox.auth0.com'); + auth0 = new Auth0({ domain : 'rambox.auth0.com', clientID: 'y9am0DVawe2tvlA3ucD7OufpJHZZMjsO'}) + // Add shortcuts to switch services using CTRL + Number var map = new Ext.util.KeyMap({ target: document diff --git a/app/store/Services.js b/app/store/Services.js index 9d1f1972..b47b78b8 100644 --- a/app/store/Services.js +++ b/app/store/Services.js @@ -53,5 +53,14 @@ Ext.define('Rambox.store.Services', { Ext.cq1('app-main').add(servicesRight); } } + ,add: function(store, records, index) { + sync(); + } + ,update: function(store, record, operation, data) { + if ( operation === 'edit' ) sync(); + } + ,remove: function(store, records, index, isMove) { + sync(); + } } }); diff --git a/app/store/ServicesList.js b/app/store/ServicesList.js index bef31af2..22208f8f 100644 --- a/app/store/ServicesList.js +++ b/app/store/ServicesList.js @@ -10,7 +10,12 @@ Ext.define('Rambox.store.ServicesList', { ,proxy: { type: 'memory' - } + } + + ,sorters: [{ + property: 'name' + ,direction: 'ASC' + }] ,autoLoad: true ,autoSync: true @@ -215,6 +220,48 @@ Ext.define('Rambox.store.ServicesList', { ,name: 'BearyChat' ,url: 'https://___.bearychat.com/' ,type: 'messaging' + }, + { + id: 'yahoomessenger' + ,logo: 'yahoomessenger.png' + ,name: 'Yahoo! Messenger' + ,url: 'https://messenger.yahoo.com/' + ,type: 'messaging' + }, + { + id: 'voxer' + ,logo: 'voxer.png' + ,name: 'Voxer' + ,url: 'https://web.voxer.com/' + ,type: 'messaging' + }, + { + id: 'dasher' + ,logo: 'dasher.png' + ,name: 'Dasher' + ,url: 'https://dasher.im/' + ,type: 'messaging' + }, + { + id: 'flowdock' + ,logo: 'flowdock.png' + ,name: 'Flowdock' + ,url: 'https://www.flowdock.com/login' + ,type: 'messaging' + }, + { + id: 'mattermost' + ,logo: 'mattermost.png' + ,name: 'Mattermost' + ,url: '___' + ,type: 'messaging' + }, + { + id: 'dingtalk' + ,logo: 'dingtalk.png' + ,name: 'DingTalk' + ,url: 'https://im.dingtalk.com/' + ,type: 'messaging' } ] }); diff --git a/app/ux/WebView.js b/app/ux/WebView.js index 9e9f4cae..4dbd6d77 100644 --- a/app/ux/WebView.js +++ b/app/ux/WebView.js @@ -34,7 +34,7 @@ Ext.define('Rambox.ux.WebView',{ tag: 'webview' ,src: me.src ,style: 'width:100%;height:100%;' - ,partition: 'persist:' + me.type + '_' + me.id.replace('tab_', '') + ,partition: 'persist:' + me.type + '_' + me.id.replace('tab_', '') + (localStorage.getItem('id_token') ? '_' + Ext.decode(localStorage.getItem('profile')).user_id : '') ,plugins: 'true' ,allowtransparency: 'on' ,autosize: 'on' @@ -108,9 +108,10 @@ Ext.define('Rambox.ux.WebView',{ }); webview.addEventListener("page-title-updated", function(e) { - var count = e.title.match(/\(([^)]+)\)/); - count = count ? parseInt(count[1]) : 0; - count = Ext.isNaN(count) ? 0 : count; // Some services have special characters. Example: (•) + var count = e.title.match(/\(([^)]+)\)/); // Get text between (...) + count = count ? count[1] : '0'; + count = count.match(/\d+/g); // Some services have special characters. Example: (•) + count = count ? parseInt(count[0]) : 0; switch ( me.type ) { case 'messenger': diff --git a/app/view/main/Main.js b/app/view/main/Main.js index 5feea04e..fd0265f1 100644 --- a/app/view/main/Main.js +++ b/app/view/main/Main.js @@ -135,6 +135,14 @@ Ext.define('Rambox.view.main.Main', { ,margin: '0 0 0 5' ,flex: 1 ,header: { height: 50 } + ,tools: [ + { + xtype: 'button' + ,glyph: 'xf1f8@FontAwesome' + ,tooltip: 'Remove all Services' + ,handler: 'removeAllServices' + } + ] ,columns: [ { xtype: 'templatecolumn' @@ -190,6 +198,73 @@ Ext.define('Rambox.view.main.Main', { } } ] + ,tbar: { + xtype: 'toolbar' + ,height: 42 + ,ui: 'main' + ,enableOverflow: true + ,overflowHandler: 'menu' + ,items: [ + { + glyph: 'xf1f7@FontAwesome' + ,text: 'Don\'t Disturb: OFF' + ,tooltip: 'Lock this app if you will be away for a period of time.' + ,enableToggle: true + ,handler: 'dontDisturb' + ,reference: 'disturbBtn' + } + ,{ + glyph: 'xf023@FontAwesome' + ,text: 'Lock Rambox' + ,tooltip: 'Lock this app if you will be away for a period of time.' + ,handler: 'lockRambox' + } + ,'->' + ,{ + xtype: 'image' + ,id: 'avatar' + ,bind: { + src: '{avatar}' + ,hidden: '{!avatar}' + } + ,width: 30 + ,height: 30 + ,style: 'border-radius: 50%;border:2px solid #d8d8d8;' + } + ,{ + id: 'usernameBtn' + ,bind: { + text: '{username}' + ,hidden: '{!username}' + } + ,menu: [ + { + text: 'Logout' + ,glyph: 'xf08b@FontAwesome' + ,handler: 'logout' + } + ] + } + ,{ + xtype: 'label' + ,id: 'explanationLabel' + ,html: 'Login to save your configuration (no credentials stored) to sync with all your computers. All current services will be removed.' + ,bind: { + hidden: '{username}' + } + } + ,{ + text: 'Login' + ,icon: 'resources/auth0.png' + ,id: 'loginBtn' + ,tooltip: 'Powered by Auth0 (http://auth0.com)' + ,bind: { + hidden: '{username}' + } + ,handler: 'login' + } + ] + } ,bbar: [ '->' ,{ diff --git a/app/view/main/MainController.js b/app/view/main/MainController.js index 817a6195..0b6d7b82 100644 --- a/app/view/main/MainController.js +++ b/app/view/main/MainController.js @@ -50,7 +50,7 @@ Ext.define('Rambox.view.main.MainController', { ,items: [ { xtype: 'checkbox' - ,boxLabel: 'Separate' + ,boxLabel: 'Align to Right' ,checked: edit ? (record.get('align') === 'right' ? true : false) : false ,name: 'align' ,uncheckedValue: 'left' @@ -90,6 +90,8 @@ Ext.define('Rambox.view.main.MainController', { text: edit ? 'Save' : 'Add service' ,itemId: 'submit' ,handler: function() { + if ( !win.down('form').isValid() ) return false; + var formValues = win.down('form').getValues(); if ( edit ) { @@ -184,6 +186,9 @@ Ext.define('Rambox.view.main.MainController', { ,fieldLabel: record.get('name') + ' team' ,name: 'url' ,allowBlank: false + ,submitEmptyText: false + ,emptyText: record.get('url') === '___' ? 'http://' : '' + ,vtype: record.get('url') === '___' ? 'url' : '' ,listeners: { specialkey: function(field, e) { if(e.getKey() == e.ENTER && field.up('form').isValid()) { @@ -204,7 +209,7 @@ Ext.define('Rambox.view.main.MainController', { ,items: [ { xtype: 'checkbox' - ,boxLabel: 'Separate' + ,boxLabel: 'Align to Right' ,checked: false ,name: 'align' ,uncheckedValue: 'left' @@ -244,6 +249,8 @@ Ext.define('Rambox.view.main.MainController', { text: 'Add service' ,itemId: 'submit' ,handler: function() { + if ( !win.down('form').isValid() ) return false; + var formValues = win.down('form').getValues(); var service = Ext.create('Rambox.model.Service', { @@ -299,22 +306,52 @@ Ext.define('Rambox.view.main.MainController', { } } + ,removeServiceFn: function(serviceId) { + if ( !serviceId ) return false; + + // Get Tab + var tab = Ext.getCmp('tab_'+serviceId); + + // Clear all trash data + tab.down('component').el.dom.getWebContents().session.clearCache(Ext.emptyFn); + tab.down('component').el.dom.getWebContents().session.clearStorageData({}, Ext.emptyFn); + + // Close tab + tab.close(); + + // Remove record from localStorage + Ext.getStore('Services').remove(Ext.getStore('Services').getById(serviceId)); + } + ,removeService: function( gridView, rowIndex, colIndex, col, e, rec, rowEl ) { + var me = this; + Ext.Msg.confirm('Please confirm...', 'Are you sure you want to remove '+rec.get('name')+'?', function(btnId) { - if ( btnId === 'yes' ) { - var tab = Ext.getCmp('tab_'+rec.get('id')); + if ( btnId === 'yes' ) me.removeServiceFn(rec.get('id')); + }); + } - // Remove record from localStorage - gridView.getStore().remove(rec); + ,removeAllServices: function(btn, callback) { + var me = this; - // Clear all trash data - tab.down('component').el.dom.getWebContents().session.clearCache(Ext.emptyFn); - tab.down('component').el.dom.getWebContents().session.clearStorageData({}, Ext.emptyFn); + // Clear counter for unread messaging + document.title = 'Rambox'; - // Close tab - tab.close(); - } - }); + if ( btn ) { + Ext.Msg.confirm('Please confirm...', 'Are you sure you want to remove all services?', function(btnId) { + if ( btnId === 'yes' ) { + Ext.Array.each(Ext.getStore('Services').collect('id'), function(serviceId) { + me.removeServiceFn(serviceId); + }); + if ( Ext.isFunction(callback) ) callback(); + } + }); + } else { + Ext.Array.each(Ext.getStore('Services').collect('id'), function(serviceId) { + me.removeServiceFn(serviceId); + }); + if ( Ext.isFunction(callback) ) callback(); + } } ,configureService: function( gridView, rowIndex, colIndex, col, e, rec, rowEl ) { @@ -385,7 +422,7 @@ Ext.define('Rambox.view.main.MainController', { ,items: [ { xtype: 'checkbox' - ,boxLabel: 'Separate' + ,boxLabel: 'Align to Right' ,checked: false ,name: 'align' ,uncheckedValue: 'left' @@ -443,6 +480,8 @@ Ext.define('Rambox.view.main.MainController', { text: 'Add service' ,itemId: 'submit' ,handler: function() { + if ( !win.down('form').isValid() ) return false; + var formValues = win.down('form').getValues(); var service = Ext.create('Rambox.model.Service', { @@ -547,4 +586,202 @@ Ext.define('Rambox.view.main.MainController', { Ext.getStore('ServicesList').getFilters().removeAll(); me.doTypeFilter(cg); } + + ,dontDisturb: function(btn) { + console.info('Dont Disturb:', btn.pressed ? 'Enabled' : 'Disabled'); + Ext.Array.each(Ext.getStore('Services').collect('id'), function(serviceId) { + // Get Tab + var tab = Ext.getCmp('tab_'+serviceId); + + // Mute sounds + tab.down('component').el.dom.getWebContents().setAudioMuted(btn.pressed); + + // Prevent Notifications + if ( btn.pressed ) { + tab.down('component').el.dom.getWebContents().executeJavaScript('var originalNotification = Notification; (function() { Notification = function() { } })();'); + } else { + tab.down('component').el.dom.getWebContents().executeJavaScript('(function() { Notification = originalNotification })();'); + } + }); + + btn.setText('Don\'t Disturb: ' + ( btn.pressed ? 'ON' : 'OFF' )); + } + + ,lockRambox: function(btn) { + var me = this; + + var msgbox = Ext.Msg.prompt('Lock Rambox', 'Enter a temporal password to unlock it later', function(btnId, text) { + if ( btnId === 'ok' ) { + me.lookupReference('disturbBtn').setPressed(true); + me.dontDisturb(me.lookupReference('disturbBtn')); + var winLock = Ext.create('Ext.window.Window', { + width: '100%' + ,height: '100%' + ,closable: false + ,minimizable: false + ,maximizable: false + ,draggable: false + ,onEsc: Ext.emptyFn + ,layout: 'center' + ,bodyStyle: 'background-color:#2e658e;' + ,items: [ + { + xtype: 'container' + ,layout: 'vbox' + ,items: [ + { + xtype: 'image' + ,src: 'resources/Icon.png' + ,width: 256 + ,height: 256 + } + ,{ + xtype: 'component' + ,autoEl: { + tag: 'h1' + ,html: 'Rambox is locked' + ,style: 'text-align:center;width:256px;' + } + } + ,{ + xtype: 'textfield' + ,inputType: 'password' + ,width: 256 + } + ,{ + xtype: 'button' + ,text: 'UNLOCK' + ,glyph: 'xf13e@FontAwesome' + ,width: 256 + ,scale: 'large' + ,handler: function() { + if ( text === winLock.down('textfield').getValue() ) { + winLock.close(); + me.lookupReference('disturbBtn').setPressed(false); + me.dontDisturb(me.lookupReference('disturbBtn')); + } else { + winLock.down('textfield').markInvalid('Unlock password is invalid'); + } + } + } + ] + } + ] + }).show(); + } + }); + msgbox.textField.inputEl.dom.type = 'password'; + } + + ,login: function(btn) { + var me = this; + + lock.show({ + icon: 'resources/Icon.png' + }, function(err, profile, id_token) { + // There was an error logging the user in + if (err) return console.error(err); + + // Display a spinner while waiting + Ext.Msg.wait('Please wait until we get your configuration.', 'Connecting...'); + + // Set the options to retreive a firebase delegation token + var options = { + id_token : id_token, + api : 'firebase', + scope : 'openid name email displayName', + target: 'y9am0DVawe2tvlA3ucD7OufpJHZZMjsO' + }; + + // Make a call to the Auth0 '/delegate' + auth0.getDelegationToken(options, function(err, result) { + if ( !err ) { + // Exchange the delegate token for a Firebase auth token + firebase.auth().signInWithCustomToken(result.id_token).then(function(snapshot) { + fireRef.database().ref('users/' + profile.user_id).once('value').then(function(snapshot) { + me.removeAllServices(false, function() { + if ( snapshot.val() === null || Ext.isEmpty(snapshot.val().services) ) return; + + Ext.each(snapshot.val().services, function(s) { + var service = Ext.create('Rambox.model.Service', { + id: s.id + ,position: s.position + ,type: s.type + ,logo: s.logo + ,name: s.name + ,url: s.url + ,align: s.align + ,notifications: s.notifications + ,muted: s.muted + ,js_unread: s.js_unread + }); + service.save(); + Ext.getStore('Services').add(service); + + var tabData = { + xtype: 'webview' + ,id: 'tab_'+service.get('id') + ,title: service.get('name') + ,icon: 'resources/icons/' + s.logo + ,src: service.get('url') + ,type: service.get('type') + ,align: s.align + ,notifications: s.notifications + ,muted: s.muted + ,record: service + ,tabConfig: { + service: service + } + }; + + if ( s.align === 'left' ) { + var tbfill = Ext.cq1('app-main').getTabBar().down('tbfill'); + Ext.cq1('app-main').insert(Ext.cq1('app-main').getTabBar().items.indexOf(tbfill), tabData); + } else { + Ext.cq1('app-main').add(tabData); + } + }); + + Ext.Msg.hide(); + }); + }); + }).catch(function(error) { + console.error(error); + }); + } + }); + + // User is logged in + // Save the profile and JWT. + localStorage.setItem('profile', JSON.stringify(profile)); + localStorage.setItem('id_token', id_token); + + Ext.cq1('app-main').getViewModel().set('username', profile.name); + Ext.cq1('app-main').getViewModel().set('avatar', profile.picture); + }, function() { + // Error callback + }); + } + + ,logout: function(btn) { + var me = this; + + Ext.Msg.confirm('Logout', 'Are you sure you want to logout?', function(btnId) { + if ( btnId === 'yes' ) { + localStorage.removeItem('profile'); + localStorage.removeItem('id_token'); + + Ext.cq1('app-main').getViewModel().set('username', ''); + Ext.cq1('app-main').getViewModel().set('avatar', ''); + + firebase.auth().signOut().then(function() { + Ext.Array.each(Ext.getStore('Services').collect('id'), function(serviceId) { + me.removeServiceFn(serviceId); + }); + }, function(error) { + console.error(error); + }); + } + }) + } }); diff --git a/app/view/main/MainModel.js b/app/view/main/MainModel.js index a0f85943..50a08b90 100644 --- a/app/view/main/MainModel.js +++ b/app/view/main/MainModel.js @@ -5,6 +5,8 @@ Ext.define('Rambox.view.main.MainModel', { ,alias: 'viewmodel.main' ,data: { - name: 'Rambox' + name: 'Rambox' + ,username: localStorage.getItem('profile') ? JSON.parse(localStorage.getItem('profile')).name : '' + ,avatar: localStorage.getItem('profile') ? JSON.parse(localStorage.getItem('profile')).picture : '' } }); diff --git a/electron/main.js b/electron/main.js index 96718839..842218f1 100644 --- a/electron/main.js +++ b/electron/main.js @@ -14,6 +14,7 @@ const tray = require('./tray'); // Require for autpUpdate file const autoupdater = require('./autoupdater'); + const MenuItem = electron.MenuItem; // this should be placed at top of main.js to handle setup events quickly @@ -103,6 +104,8 @@ function createWindow () { } }); + process.setMaxListeners(100); + // Start maximize mainWindow.maximize(); @@ -118,7 +121,7 @@ function createWindow () { mainWindow.on('page-title-updated', (e, title) => updateBadge(title)); mainWindow.webContents.on('will-navigate', function(event, url) { - event.preventDefault(); + //event.preventDefault(); }); // Emitted when the window is closed. diff --git a/electron/tray.js b/electron/tray.js index 48440ac4..75a524ae 100644 --- a/electron/tray.js +++ b/electron/tray.js @@ -39,9 +39,6 @@ exports.create = win => { const contextMenu = electron.Menu.buildFromTemplate([ showMB, hideMB, - { - label: 'Preferences' - }, { type: 'separator' }, diff --git a/index.html b/index.html index ce660ba0..8df8a6b6 100644 --- a/index.html +++ b/index.html @@ -9,11 +9,13 @@ - + + +