1
0
mirror of https://github.com/moparisthebest/mail synced 2024-12-01 13:22:16 -05:00

[WO-338] add notification for incoming unread mails

This commit is contained in:
Felix Hammerl 2014-04-28 18:09:51 +02:00
parent f5f8781a8c
commit 49cadecd2d
10 changed files with 206 additions and 148 deletions

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1", "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.2.4", "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/wo-338",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.2.2", "mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.2.2",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.2.2", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.2.2",
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.2.3", "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.2.3",

View File

@ -72,7 +72,7 @@ define(function(require) {
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
self._crypto = pgp = new PGP(); self._crypto = pgp = new PGP();
self._pgpbuilder = pgpbuilder = new PgpBuilder(); self._pgpbuilder = pgpbuilder = new PgpBuilder();
emailSync = new EmailSync(keychain, userStorage); self._emailSync = emailSync = new EmailSync(keychain, userStorage);
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync); self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
self._updateHandler = new UpdateHandler(appConfigStore, userStorage); self._updateHandler = new UpdateHandler(appConfigStore, userStorage);

View File

@ -5,9 +5,8 @@ define(function(require) {
_ = require('underscore'), _ = require('underscore'),
appController = require('js/app-controller'), appController = require('js/app-controller'),
IScroll = require('iscroll'), IScroll = require('iscroll'),
str = require('js/app-config').string,
notification = require('js/util/notification'), notification = require('js/util/notification'),
emailDao, outboxBo; emailDao, outboxBo, emailSync;
var MailListCtrl = function($scope, $timeout) { var MailListCtrl = function($scope, $timeout) {
// //
@ -16,26 +15,66 @@ define(function(require) {
emailDao = appController._emailDao; emailDao = appController._emailDao;
outboxBo = appController._outboxBo; outboxBo = appController._outboxBo;
emailSync = appController._emailSync;
// push handler emailDao.onNeedsSync = function(error, folder) {
if (emailDao) { if (error) {
emailDao.onIncomingMessage = function(email) { $scope.onError(error);
// sync return;
$scope.synchronize(function() { }
// show notification
notificationForEmail(email); $scope.synchronize({
folder: folder
}); });
}; };
notification.setOnClickedListener(notificationClicked);
emailSync.onIncomingMessage = function(msgs) {
var popupId, popupTitle, popupMessage, unreadMsgs;
unreadMsgs = msgs.filter(function(msg) {
return msg.unread;
});
if (unreadMsgs.length === 0) {
return;
} }
popupId = '' + unreadMsgs[0].uid;
if (unreadMsgs.length > 1) {
popupTitle = unreadMsgs.length + ' new messages';
popupMessage = _.pluck(unreadMsgs, 'subject').join('\n');
} else {
popupTitle = unreadMsgs[0].from[0].name || unreadMsgs[0].from[0].address;
popupMessage = unreadMsgs[0].subject;
}
notification.create({
id: popupId,
title: popupTitle,
message: popupMessage
});
};
notification.setOnClickedListener(function(uidString) {
var uid = parseInt(uidString, 10);
if (isNaN(uid)) {
return;
}
$scope.select(_.findWhere(currentFolder().messages, {
uid: uid
}));
});
// //
// scope functions // scope functions
// //
$scope.getBody = function(email) { $scope.getBody = function(email) {
emailDao.getBody({ emailDao.getBody({
folder: getFolder().path, folder: currentFolder().path,
message: email message: email
}, function(err) { }, function(err) {
if (err && err.code !== 42) { if (err && err.code !== 42) {
@ -93,17 +132,20 @@ define(function(require) {
/** /**
* Synchronize the selected imap folder to local storage * Synchronize the selected imap folder to local storage
*/ */
$scope.synchronize = function(callback) { $scope.synchronize = function(options) {
updateStatus('Syncing ...'); updateStatus('Syncing ...');
options = options || {};
options.folder = options.folder || currentFolder().path;
// let email dao handle sync transparently // let email dao handle sync transparently
if ($scope.state.nav.currentFolder.type === 'Outbox') { if (currentFolder().type === 'Outbox') {
emailDao.syncOutbox({ emailDao.syncOutbox({
folder: getFolder().path folder: currentFolder().path
}, done); }, done);
} else { } else {
emailDao.sync({ emailDao.sync({
folder: getFolder().path folder: options.folder || currentFolder().path
}, done); }, done);
} }
@ -127,18 +169,18 @@ define(function(require) {
return; return;
} }
// sort emails
selectFirstMessage();
// display last update // display last update
updateStatus('Last update: ', new Date()); updateStatus('Last update: ', new Date());
// do not change the selection if we just updated another folder in the background
if (currentFolder().path === options.folder) {
selectFirstMessage();
}
$scope.$apply(); $scope.$apply();
// fetch visible bodies at the end of a successful sync // fetch visible bodies at the end of a successful sync
$scope.loadVisibleBodies(); $scope.loadVisibleBodies();
if (callback) {
callback();
}
} }
}; };
@ -150,7 +192,7 @@ define(function(require) {
return; return;
} }
if (getFolder().type === 'Outbox') { if (currentFolder().type === 'Outbox') {
$scope.onError({ $scope.onError({
errMsg: 'Deleting messages from the outbox is not yet supported.' errMsg: 'Deleting messages from the outbox is not yet supported.'
}); });
@ -161,18 +203,18 @@ define(function(require) {
$scope.synchronize(); $scope.synchronize();
function removeAndShowNext() { function removeAndShowNext() {
var index = getFolder().messages.indexOf(email); var index = currentFolder().messages.indexOf(email);
// show the next mail // show the next mail
if (getFolder().messages.length > 1) { if (currentFolder().messages.length > 1) {
// if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list), // if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list),
// otherwise show the next one (i.e. the one above in the list) // otherwise show the next one (i.e. the one above in the list)
$scope.select(_.last(getFolder().messages) === email ? getFolder().messages[index - 1] : getFolder().messages[index + 1]); $scope.select(_.last(currentFolder().messages) === email ? currentFolder().messages[index - 1] : currentFolder().messages[index + 1]);
} else { } else {
// if we have only one email in the array, show nothing // if we have only one email in the array, show nothing
$scope.select(); $scope.select();
$scope.state.mailList.selected = undefined; $scope.state.mailList.selected = undefined;
} }
getFolder().messages.splice(index, 1); currentFolder().messages.splice(index, 1);
} }
}; };
@ -190,14 +232,14 @@ define(function(require) {
* List emails from folder when user changes folder * List emails from folder when user changes folder
*/ */
$scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() {
if (!getFolder()) { if (!currentFolder()) {
return; return;
} }
// development... display dummy mail objects // development... display dummy mail objects
if (!window.chrome || !chrome.identity) { if (!window.chrome || !chrome.identity) {
updateStatus('Last update: ', new Date()); updateStatus('Last update: ', new Date());
getFolder().messages = createDummyMails(); currentFolder().messages = createDummyMails();
selectFirstMessage(); selectFirstMessage();
return; return;
} }
@ -229,30 +271,6 @@ define(function(require) {
// helper functions // helper functions
// //
function notificationClicked(uidString) {
var email, uid = parseInt(uidString, 10);
if (isNaN(uid)) {
return;
}
email = _.findWhere(getFolder().messages, {
uid: uid
});
if (email) {
$scope.select(email);
}
}
function notificationForEmail(email) {
notification.create({
id: '' + email.uid,
title: email.from[0].name || email.from[0].address,
message: email.subject.replace(str.subjectPrefix, '')
}, function() {});
}
function updateStatus(lbl, time) { function updateStatus(lbl, time) {
$scope.lastUpdateLbl = lbl; $scope.lastUpdateLbl = lbl;
$scope.lastUpdate = (time) ? time : ''; $scope.lastUpdate = (time) ? time : '';
@ -272,7 +290,7 @@ define(function(require) {
} }
} }
function getFolder() { function currentFolder() {
return $scope.state.nav.currentFolder; return $scope.state.nav.currentFolder;
} }
}; };

View File

@ -59,6 +59,7 @@ define(function(require) {
// init folders // init folders
initFolders(); initFolders();
// select inbox as the current folder on init // select inbox as the current folder on init
if ($scope.account.folders && $scope.account.folders.length > 0) { if ($scope.account.folders && $scope.account.folders.length > 0) {
$scope.openFolder($scope.account.folders[0]); $scope.openFolder($scope.account.folders[0]);
@ -89,8 +90,7 @@ define(function(require) {
outboxBo.onSent = sentNotification; outboxBo.onSent = sentNotification;
// start checking outbox periodically // start checking outbox periodically
outboxBo.startChecking($scope.onOutboxUpdate); outboxBo.startChecking($scope.onOutboxUpdate);
// make function available globally for write controller
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
return; return;
} }

View File

@ -86,13 +86,6 @@ define(function(require) {
self._imapClient = options.imapClient; self._imapClient = options.imapClient;
self._pgpMailer = options.pgpMailer; self._pgpMailer = options.pgpMailer;
// delegation-esque pattern to mitigate between node-style events and plain js
self._imapClient.onIncomingMessage = function(message) {
if (typeof self.onIncomingMessage === 'function') {
self.onIncomingMessage(message);
}
};
// notify emailSync // notify emailSync
self._emailSync.onConnect({ self._emailSync.onConnect({
imapClient: self._imapClient imapClient: self._imapClient
@ -122,6 +115,20 @@ define(function(require) {
return; return;
} }
var inbox = _.findWhere(folders, {
type: 'Inbox'
});
if (inbox) {
self._imapClient.listenForChanges({
path: inbox.path
},function(error, path) {
if (typeof self.onNeedsSync === 'function') {
self.onNeedsSync(error, path);
}
});
}
self._account.folders = folders; self._account.folders = folders;
callback(); callback();

View File

@ -448,6 +448,7 @@ define(function(require) {
// if persisting worked, add them to the messages array // if persisting worked, add them to the messages array
folder.messages = folder.messages.concat(messages); folder.messages = folder.messages.concat(messages);
self.onIncomingMessage(messages);
doDeltaF4(); doDeltaF4();
}); });
} }

View File

@ -6,6 +6,7 @@ define(function(require) {
var self = {}; var self = {};
self.create = function(options, callback) { self.create = function(options, callback) {
callback = callback || function() {};
if (window.chrome && chrome.notifications) { if (window.chrome && chrome.notifications) {
chrome.notifications.create(options.id, { chrome.notifications.create(options.id, {
type: 'basic', type: 'basic',

View File

@ -159,19 +159,6 @@ define(function(require) {
}); });
}); });
describe('push', function() {
it('should work', function(done) {
var o = {};
dao.onIncomingMessage = function(obj) {
expect(obj).to.equal(o);
done();
};
dao._imapClient.onIncomingMessage(o);
});
});
describe('init', function() { describe('init', function() {
beforeEach(function() { beforeEach(function() {
delete dao._account; delete dao._account;

View File

@ -800,7 +800,7 @@ define(function(require) {
}); });
it('should fetch messages downstream from the remote', function(done) { it('should fetch messages downstream from the remote', function(done) {
var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub, incomingMessagesCalled;
invocations = 0; invocations = 0;
folder = 'FOLDAAAA'; folder = 'FOLDAAAA';
@ -842,6 +842,13 @@ define(function(require) {
emails: [dummyEncryptedMail] emails: [dummyEncryptedMail]
}).yields(); }).yields();
incomingMessagesCalled = false;
emailSync.onIncomingMessage = function(msgs) {
incomingMessagesCalled = true;
expect(msgs).to.not.be.empty;
};
emailSync.sync({ emailSync.sync({
folder: folder folder: folder
}, function(err) { }, function(err) {
@ -860,6 +867,8 @@ define(function(require) {
expect(localListStub.calledOnce).to.be.true; expect(localListStub.calledOnce).to.be.true;
expect(imapSearchStub.calledThrice).to.be.true; expect(imapSearchStub.calledThrice).to.be.true;
expect(localStoreStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true;
expect(incomingMessagesCalled).to.be.true;
done(); done();
}); });
}); });
@ -1076,6 +1085,8 @@ define(function(require) {
emails: [verificationMail] emails: [verificationMail]
}).yields(); }).yields();
emailSync.onIncomingMessage = function() {};
emailSync.sync({ emailSync.sync({
folder: folder folder: folder
}, function(err) { }, function(err) {
@ -1146,6 +1157,8 @@ define(function(require) {
}); });
imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({});
emailSync.onIncomingMessage = function() {};
emailSync.sync({ emailSync.sync({
folder: folder folder: folder
}, function(err) { }, function(err) {

View File

@ -6,35 +6,26 @@ define(function(require) {
mocks = require('angularMocks'), mocks = require('angularMocks'),
MailListCtrl = require('js/controller/mail-list'), MailListCtrl = require('js/controller/mail-list'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
EmailSync = require('js/dao/email-sync'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'),
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
appController = require('js/app-controller'); appController = require('js/app-controller'),
notification = require('js/util/notification');
chai.Assertion.includeStack = true; chai.Assertion.includeStack = true;
describe('Mail List controller unit test', function() { describe('Mail List controller unit test', function() {
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, var scope, ctrl, origEmailDao, origEmailSync, emailDaoMock, emailSyncMock, keychainMock, deviceStorageMock,
emailAddress, notificationClickedHandler, emails, emailAddress, notificationClickedHandler, emails,
hasChrome, hasNotifications, hasSocket, hasRuntime, hasIdentity; hasChrome, hasSocket, hasRuntime, hasIdentity;
beforeEach(function() { beforeEach(function() {
hasChrome = !! window.chrome; hasChrome = !! window.chrome;
hasNotifications = !! window.chrome.notifications;
hasSocket = !! window.chrome.socket; hasSocket = !! window.chrome.socket;
hasIdentity = !! window.chrome.identity; hasIdentity = !! window.chrome.identity;
if (!hasChrome) { if (!hasChrome) {
window.chrome = {}; window.chrome = {};
} }
if (!hasNotifications) {
window.chrome.notifications = {
onClicked: {
addListener: function(handler) {
notificationClickedHandler = handler;
}
},
create: function() {}
};
}
if (!hasSocket) { if (!hasSocket) {
window.chrome.socket = {}; window.chrome.socket = {};
} }
@ -47,6 +38,10 @@ define(function(require) {
window.chrome.identity = {}; window.chrome.identity = {};
} }
sinon.stub(notification, 'setOnClickedListener', function(func) {
notificationClickedHandler = func;
});
emails = [{ emails = [{
unread: true unread: true
}, { }, {
@ -59,8 +54,11 @@ define(function(require) {
}; };
origEmailDao = appController._emailDao; origEmailDao = appController._emailDao;
origEmailSync = appController._emailSync;
emailDaoMock = sinon.createStubInstance(EmailDAO); emailDaoMock = sinon.createStubInstance(EmailDAO);
emailSyncMock = sinon.createStubInstance(EmailSync);
appController._emailDao = emailDaoMock; appController._emailDao = emailDaoMock;
appController._emailSync = emailSyncMock;
emailAddress = 'fred@foo.com'; emailAddress = 'fred@foo.com';
emailDaoMock._account = { emailDaoMock._account = {
emailAddress: emailAddress, emailAddress: emailAddress,
@ -91,9 +89,8 @@ define(function(require) {
}); });
afterEach(function() { afterEach(function() {
if (!hasNotifications) { notification.setOnClickedListener.restore();
delete window.chrome.notifications;
}
if (!hasSocket) { if (!hasSocket) {
delete window.chrome.socket; delete window.chrome.socket;
} }
@ -109,6 +106,7 @@ define(function(require) {
// restore the module // restore the module
appController._emailDao = origEmailDao; appController._emailDao = origEmailDao;
appController._emailSync = origEmailDao;
}); });
describe('scope variables', function() { describe('scope variables', function() {
@ -117,72 +115,105 @@ define(function(require) {
expect(scope.synchronize).to.exist; expect(scope.synchronize).to.exist;
expect(scope.remove).to.exist; expect(scope.remove).to.exist;
expect(scope.state.mailList).to.exist; expect(scope.state.mailList).to.exist;
// expect(emailDaoMock.onIncomingMessage).to.exist;
}); });
}); });
describe('push notification', function() { describe('push notification', function() {
it('should focus mail and not mark it read', function(done) { beforeEach(function() {
var uid, mail, currentFolder;
scope._stopWatchTask(); scope._stopWatchTask();
uid = 123;
mail = {
uid: uid,
from: [{
address: 'asd'
}],
subject: '[whiteout] asdasd',
unread: true
};
currentFolder = 'asd';
scope.state.nav = {
currentFolder: currentFolder
};
scope.state.read = {
toggle: function() {}
};
scope.emails = [mail];
emailDaoMock.sync.yieldsAsync();
window.chrome.notifications.create = function(id, opts) {
expect(id).to.equal('123');
expect(opts.type).to.equal('basic');
expect(opts.message).to.equal('asdasd');
expect(opts.title).to.equal('asd');
done();
};
emailDaoMock.onIncomingMessage(mail);
});
}); });
describe('clicking push notification', function() { it('should succeed for single mail', function(done) {
it('should focus mail', function() { var mail = {
var mail, currentFolder;
scope._stopWatchTask();
mail = {
uid: 123, uid: 123,
from: [{ from: [{
address: 'asd' address: 'asd'
}], }],
subject: '[whiteout] asdasd', subject: 'this is the subject!',
unread: true unread: true
}; };
currentFolder = {
sinon.stub(notification, 'create', function(opts) {
expect(opts.id).to.equal('' + mail.uid);
expect(opts.title).to.equal(mail.from[0].address);
expect(opts.message).to.equal(mail.subject);
notification.create.restore();
done();
});
emailSyncMock.onIncomingMessage([mail]);
});
it('should succeed for multiple mails', function(done) {
var mails = [{
uid: 1,
from: [{
address: 'asd'
}],
subject: 'this is the subject!',
unread: true
}, {
uid: 2,
from: [{
address: 'qwe'
}],
subject: 'this is the other subject!',
unread: true
}, {
uid: 3,
from: [{
address: 'qwe'
}],
subject: 'this is the other subject!',
unread: false
}];
sinon.stub(notification, 'create', function(opts) {
expect(opts.id).to.equal('' + mails[0].uid);
expect(opts.title).to.equal('2 new messages');
expect(opts.message).to.equal(mails[0].subject + '\n' + mails[1].subject);
notification.create.restore();
done();
});
emailSyncMock.onIncomingMessage(mails);
});
it('should focus mail when clicked', function() {
var mail = {
uid: 123,
from: [{
address: 'asd'
}],
subject: 'asdasd',
unread: true
};
scope.state.nav = {
currentFolder: {
type: 'asd', type: 'asd',
messages: [mail] messages: [mail]
}; }
scope.state.nav = {
currentFolder: currentFolder
}; };
notificationClickedHandler('123'); notificationClickedHandler('123');
expect(scope.state.mailList.selected).to.equal(mail); expect(scope.state.mailList.selected).to.equal(mail);
}); });
it('should not change focus mail when popup id is NaN', function() {
scope.state.nav = {
currentFolder: {
type: 'asd',
messages: []
}
};
var focus = scope.state.mailList.selected = {};
notificationClickedHandler('');
expect(scope.state.mailList.selected).to.equal(focus);
});
}); });
describe('synchronize', function() { describe('synchronize', function() {
@ -200,15 +231,15 @@ define(function(require) {
currentFolder: currentFolder currentFolder: currentFolder
}; };
var loadVisibleBodiesStub = sinon.stub(scope, 'loadVisibleBodies'); var loadVisibleBodiesStub = sinon.stub(scope, 'loadVisibleBodies', function() {
scope.synchronize(function() {
expect(scope.state.nav.currentFolder.messages).to.deep.equal(emails); expect(scope.state.nav.currentFolder.messages).to.deep.equal(emails);
expect(loadVisibleBodiesStub.calledOnce).to.be.true; expect(loadVisibleBodiesStub.calledOnce).to.be.true;
loadVisibleBodiesStub.restore(); loadVisibleBodiesStub.restore();
done(); done();
}); });
scope.synchronize();
}); });
}); });