diff --git a/css/main.css b/css/main.css index 9321a5d7f..f44f11a5e 100644 --- a/css/main.css +++ b/css/main.css @@ -496,6 +496,142 @@ a.loading { vertical-align:middle } -input.ng-invalid-wallet-secret { - background: #FFB6C1; +/* notifications */ + +.dr-notification-container { + position: absolute; + z-index: 10000; +} + +.dr-notification-container.bottom { + bottom: 20px; +} + +.dr-notification-container.right { + right: 20px; +} + +.dr-notification-container.left { + left: 20px; +} + +.dr-notification-container.top { + top: 20px; +} + +.dr-notification-container.center { + left: 50%; + margin-left: -190px; +} + +.dr-notification-wrapper { + width: 380px; + position: relative; + margin: 10px 0; +} + +.dr-notification { + width: 380px; + background-color: #2C3E50; + clear: both; + min-height: 80px; + max-height: 90px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + border-radius: 5px; + color: #bfe2de; + border: 1px solid rgba(4, 94, 123, 0.85); + overflow: hidden; +} + +.dr-notification-close-btn { + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + -ms-border-radius: 20px; + border-radius: 20px; + display: inline-block; + padding: 3px; + background-color: #2C3E50; + font-size: 14px; + color: #adfaff; + border: 1px solid rgba(4, 94, 123, 0.85); + position: absolute; + right: -11px; + top: 5px; + -webkit-transition: all 0.35s cubic-bezier(0.31, 0.39, 0.21, 1.65); + -moz-transition: all 0.35s cubic-bezier(0.31, 0.39, 0.21, 1.65); + transition: all 0.35s cubic-bezier(0.31, 0.39, 0.21, 1.65); + cursor: pointer; +} +.dr-notification-close-btn i { + padding-left: 3px; +} +.dr-notification-close-btn:hover { + -webkit-transform: scale3d(1.25, 1.25, 1); + -moz-transform: scale3d(1.25, 1.25, 1); + -ms-transform: scale3d(1.25, 1.25, 1); + transform: scale3d(1.25, 1.25, 1); +} + +.dr-notification-image { + width: 80px; + height: 80px; + border-right: 1px solid rgba(4, 94, 123, 0.85); + float: left; + display: block; + font-size: 40px; + color: white; + text-align: center; +} +.dr-notification-image i { + display: block; + width: 100%; + padding-top: 25px; +} +.dr-notification-image img { + margin: 15px; + max-width: 70px; + min-width: 48px; +} + +.dr-notification-image.dr-notification-type-info { + color: #FFF; +} + +.dr-notification-image.dr-notification-type-warning { + color: #FFA226; +} + +.dr-notification-image.dr-notification-type-error { + color: #FF4B4F; +} + +.dr-notification-image.dr-notification-type-success { + color: #B4D455; +} + +.dr-notification-image.success { + color: #B4D455; +} + +.dr-notification-content { + padding-left: 100px; + padding-right: 15px; + padding-top: 10px; +} + +.dr-notification-title { + color: white; + padding: 0px; + font-size: 20px; +} + +p.dr-notification-text { + margin-top: -5px; + font-size: 12px; +} + +input.ng-invalid-wallet-secret { + background: #FFB6C1; } diff --git a/index.html b/index.html index 6fd3248a3..db702cd03 100644 --- a/index.html +++ b/index.html @@ -112,6 +112,8 @@ +
+
@@ -340,7 +342,7 @@
-

Transactions proposals ({{txs.length}})

+

Transaction proposals ({{txs.length}})

@@ -362,7 +364,7 @@
- cId + {{cId}}
@@ -682,6 +684,7 @@ on supported browsers please check http://www.w + diff --git a/js/app.js b/js/app.js index 30a55782e..d048c71b2 100644 --- a/js/app.js +++ b/js/app.js @@ -7,6 +7,7 @@ var copayApp = window.copayApp = angular.module('copay',[ 'ngRoute', 'mm.foundation', 'monospaced.qrcode', + 'notifications', 'copay.header', 'copay.footer', 'copay.addresses', diff --git a/js/controllers/header.js b/js/controllers/header.js index 3ceb1d865..331fa668d 100644 --- a/js/controllers/header.js +++ b/js/controllers/header.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copay.header').controller('HeaderController', - function($scope, $rootScope, $location, walletFactory, controllerUtils) { + function($scope, $rootScope, $location, $notification, walletFactory, controllerUtils) { $scope.menu = [ { 'title': 'Addresses', @@ -27,6 +27,15 @@ angular.module('copay.header').controller('HeaderController', } }); + // Initialize alert notification (not show when init wallet) + $rootScope.txAlertCount = 0; + $notification.enableHtml5Mode(); // for chrome: if support, enable it + $rootScope.$watch('txAlertCount', function(txAlertCount) { + if (txAlertCount && txAlertCount > 0) { + $notification.info('New Transaction', ($rootScope.txAlertCount == 1) ? 'You have a pending transaction proposal' : 'You have ' + $rootScope.txAlertCount + ' pending transaction proposals', txAlertCount); + } + }); + $scope.isActive = function(item) { if (item.link && item.link.replace('#','') == $location.path()) { return true; diff --git a/js/controllers/signin.js b/js/controllers/signin.js index 5971a1ce2..685e176d5 100644 --- a/js/controllers/signin.js +++ b/js/controllers/signin.js @@ -2,8 +2,13 @@ angular.module('copay.signin').controller('SigninController', function($scope, $rootScope, $location, walletFactory, controllerUtils, Passphrase) { + var cmp = function(o1, o2){ + var v1 = o1.show.toLowerCase(), v2 = o2.show.toLowerCase(); + return v1 > v2 ? 1 : ( v1 < v2 ) ? -1 : 0; + }; + $scope.loading = false; - $scope.wallets = walletFactory.getWallets(); + $scope.wallets = walletFactory.getWallets().sort(cmp); $scope.selectedWalletId = $scope.wallets.length ? $scope.wallets[0].id : null; $scope.openPassword = ''; diff --git a/js/controllers/transactions.js b/js/controllers/transactions.js index 26f948cbc..0a66e1dec 100644 --- a/js/controllers/transactions.js +++ b/js/controllers/transactions.js @@ -8,6 +8,7 @@ angular.module('copay.transactions').controller('TransactionsController', $scope.send = function (ntxid) { $scope.loading = true; + $rootScope.txAlertCount = 0; var w = $rootScope.wallet; w.sendTx(ntxid, function(txid) { console.log('[transactions.js.68:txid:] SENTTX CALLBACK',txid); //TODO @@ -65,6 +66,7 @@ angular.module('copay.transactions').controller('TransactionsController', $scope.reject = function (ntxid) { $scope.loading = true; + $rootScope.txAlertCount = 0; var w = $rootScope.wallet; w.reject(ntxid); $rootScope.flashMessage = {type:'warning', message: 'Transaction rejected by you'}; diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index bbef94908..8a59223a2 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -180,8 +180,9 @@ TxProposals.prototype._mergeBuilder = function(myTxps, theirTxps, mergeInfo) { }; TxProposals.prototype.add = function(data) { - var id = data.builder.build().getNormalizedHash().toString('hex'); - this.txps[id] = new TxProposal(data); + var ntxid = data.builder.build().getNormalizedHash().toString('hex'); + this.txps[ntxid] = new TxProposal(data); + return ntxid; }; @@ -191,12 +192,20 @@ TxProposals.prototype.setSent = function(ntxid,txid) { }; -TxProposals.prototype.getTxProposal = function(ntxid) { +TxProposals.prototype.getTxProposal = function(ntxid, copayers) { var txp = this.txps[ntxid]; var i = JSON.parse(JSON.stringify(txp)); i.builder = txp.builder; i.ntxid = ntxid; i.peerActions = {}; + + if (copayers) { + for(var j=0; j < copayers.length; j++) { + var p = copayers[j]; + i.peerActions[p] = {}; + } + } + for(var p in txp.seenBy){ i.peerActions[p]={seen: txp.seenBy[p]}; } diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 358ab197c..a30e63e24 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -94,6 +94,7 @@ Wallet.prototype._handleTxProposals = function(senderId, data, isInbound) { if (mergeInfo.hasChanged || addSeen) { this.log('### BROADCASTING txProposals. '); recipients = null; + TODO: only diff this.sendTxProposals(recipients); } this.emit('txProposalsUpdated', this.txProposals); @@ -114,7 +115,7 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) { break; case 'walletReady': this.sendPublicKeyRing(senderId); - this.sendTxProposals(senderId); + this.sendTxProposals(senderId); // send old break; case 'publicKeyRing': this._handlePublicKeyRing(senderId, data, isInbound); @@ -223,12 +224,23 @@ Wallet.prototype.getOnlinePeerIDs = function() { return this.network.getOnlinePeerIDs(); }; +Wallet.prototype.getRegisteredCopayerIds = function() { + var l = this.publicKeyRing.registeredCopayers(); + var copayers = []; + for (var i = 0; i < l; i++) { + var cid = this.getCopayerId(i); + copayers.push(cid); + } + return copayers; +}; + Wallet.prototype.getRegisteredPeerIds = function() { var l = this.publicKeyRing.registeredCopayers(); if (this.registeredPeerIds.length !== l) { this.registeredPeerIds = []; + var copayers = this.getRegisteredCopayerIds(); for (var i = 0; i < l; i++) { - var cid = this.getCopayerId(i); + var cid = copayers[i]; var pid = this.network.peerFromCopayer(cid); this.registeredPeerIds.push({ peerId: pid, @@ -275,14 +287,18 @@ Wallet.prototype.toEncryptedObj = function() { return this.storage.export(walletObj); }; -Wallet.prototype.sendTxProposals = function(recipients) { +Wallet.prototype.sendTxProposals = function(recipients, ntxid) { this.log('### SENDING txProposals TO:', recipients || 'All', this.txProposals); - this.network.send(recipients, { - type: 'txProposals', - txProposals: this.txProposals.toObj(), - walletId: this.id, - }); + var toSend = ntxid ? [ntxid] : this.getTxProposals.getNtxids(); + for(var i in toSend) { + var id = toSend[i]; + this.network.send(recipients, { + type: 'txProposals', + txProposals: this.txProposals.toObj(id), + walletId: this.id, + }); + } }; Wallet.prototype.sendWalletReady = function(recipients) { @@ -326,8 +342,9 @@ Wallet.prototype.generateAddress = function(isChange) { Wallet.prototype.getTxProposals = function() { var ret = []; + var copayers = this.getRegisteredCopayerIds(); for (var k in this.txProposals.txps) { - var i = this.txProposals.getTxProposal(k); + var i = this.txProposals.getTxProposal(k, copayers); i.signedByUs = i.signedBy[this.getMyCopayerId()] ? true : false; i.rejectedByUs = i.rejectedBy[this.getMyCopayerId()] ? true : false; if (this.totalCopayers - i.rejectCount < this.requiredCopayers) @@ -345,7 +362,7 @@ Wallet.prototype.reject = function(ntxid) { if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) return; txp.rejectedBy[myId] = Date.now(); - this.sendTxProposals(); + this.sendTxProposals(null, ntxid); this.store(); this.emit('txProposalsUpdated'); }; @@ -366,7 +383,7 @@ Wallet.prototype.sign = function(ntxid) { if (b.signaturesAdded > before) { txp.signedBy[myId] = Date.now(); - this.sendTxProposals(); + this.sendTxProposals(null, ntxid); this.store(); this.emit('txProposalsUpdated'); return true; @@ -392,7 +409,7 @@ Wallet.prototype.sendTx = function(ntxid, cb) { if (txid) { self.txProposals.setSent(ntxid, txid); } - self.sendTxProposals(); + self.sendTxProposals(null, ntxid); self.store(); return cb(txid); }); @@ -502,9 +519,10 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, opts, cb) { } self.getSafeUnspent(function(unspentList) { - if (self.createTxSync(toAddress, amountSatStr, unspentList, opts)) { - self.sendPublicKeyRing(); // Change Address - self.sendTxProposals(); + var ntxid = self.createTxSync(toAddress, amountSatStr, unspentList, opts); + if (ntxid) { + self.sendPublicKeyRing(); // For the new change Address + self.sendTxProposals(null, ntxid); self.store(); self.emit('txProposalsUpdated'); } @@ -558,8 +576,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, utxos, opts) { builder: b, }; - this.txProposals.add(data); - return true; + return this.txProposals.add(data); }; Wallet.prototype.disconnect = function() { diff --git a/js/models/storage/LocalEncrypted.js b/js/models/storage/LocalEncrypted.js index 42509ab6b..18fc83dee 100644 --- a/js/models/storage/LocalEncrypted.js +++ b/js/models/storage/LocalEncrypted.js @@ -73,7 +73,8 @@ Storage.prototype._write = function(k,v) { // get value by key Storage.prototype.getGlobal = function(k) { - return localStorage.getItem(k); + var item = localStorage.getItem(k); + return item == 'undefined' ? undefined : item; }; // set value for key diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 0b0f7361f..a4abebd72 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -128,6 +128,8 @@ angular.module('copay.controllerUtils') var w = $rootScope.wallet; if (!w) return; + var myCopayerId = w.getMyCopayerId(); + var pending = 0; var inT = w.getTxProposals(); var txs = []; @@ -149,13 +151,16 @@ angular.module('copay.controllerUtils') i.fee = i.builder.feeSat/bitcore.util.COIN; i.missingSignatures = tx.countInputMissingSignatures(0); txs.push(i); - }); - $rootScope.txs = txs; - var pending = 0; - for(var i=0; i'; + return this.makeNotification(type, false, icon, title, content, userData); + }, + + notify: function(image, title, content, userData){ + // Wraps the makeNotification method for displaying notifications with images + // rather than icons + return this.makeNotification('custom', image, true, title, content, userData); + }, + + makeNotification: function(type, image, icon, title, content, userData){ + var notification = { + 'type': type, + 'image': image, + 'icon': icon, + 'title': title, + 'content': content, + 'timestamp': +new Date(), + 'userData': userData + }; + notifications.push(notification); + + if(settings.html5Mode){ + html5Notify(image, title, content, function(){ + console.log("inner on display function"); + }, function(){ + console.log("inner on close function"); + }); + } + else{ + queue.push(notification); + $timeout(function removeFromQueueTimeout(){ + queue.splice(queue.indexOf(notification), 1); + }, settings[type].duration); + + } + + this.save(); + return notification; + }, + + + /* ============ PERSISTENCE METHODS ============ */ + + save: function(){ + // Save all the notifications into localStorage + // console.log(JSON); + if(settings.localStorage){ + localStorage.setItem('$notifications', JSON.stringify(notifications)); + } + // console.log(localStorage.getItem('$notifications')); + }, + + restore: function(){ + // Load all notifications from localStorage + }, + + clear: function(){ + notifications = []; + this.save(); + } + + }; + }]). + directive('notifications', ['$notification', '$compile', function($notification, $compile){ + /** + * + * It should also parse the arguments passed to it that specify + * its position on the screen like "bottom right" and apply those + * positions as a class to the container element + * + * Finally, the directive should have its own controller for + * handling all of the notifications from the notification service + */ +// console.log('this is a new directive'); + var html = + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '

{{noti.title}}

' + + '

{{noti.content}}

' + + '
' + + '
' + + '
'; + + + function link(scope, element, attrs){ + var position = attrs.notifications; + position = position.split(' '); + element.addClass('dr-notification-container'); + for(var i = 0; i < position.length ; i++){ + element.addClass(position[i]); + } + } + + + return { + restrict: 'A', + scope: {}, + template: html, + link: link, + controller: ['$scope', function NotificationsCtrl( $scope ){ + $scope.queue = $notification.getQueue(); + + $scope.removeNotification = function(noti){ + $scope.queue.splice($scope.queue.indexOf(noti), 1); + }; + } + ] + + }; + }]);