[WO-565] Improve notifications

* Introduce 2 sec timeout for sent notifications
* Notify only for new messages in the inbox
* Close pending notes when a msg is marked unread in the inbox
This commit is contained in:
Felix Hammerl 2014-09-16 19:01:49 +02:00 committed by Tankred Hase
parent b69e0951c9
commit 86a87e26b8
8 changed files with 143 additions and 92 deletions

View File

@ -19,6 +19,7 @@
"predef": [
"console",
"Notification",
"importScripts",
"process",
"Event",

View File

@ -8,7 +8,8 @@ define(function(require) {
emailDao, outboxBo, keychainDao, searchTimeout, firstSelect;
var INIT_DISPLAY_LEN = 20,
SCROLL_DISPLAY_LEN = 10;
SCROLL_DISPLAY_LEN = 10,
FOLDER_TYPE_INBOX = 'Inbox';
var MailListCtrl = function($scope, $routeParams) {
//
@ -19,6 +20,11 @@ define(function(require) {
outboxBo = appController._outboxBo;
keychainDao = appController._keychain;
/**
* Gathers unread notifications to be cancelled later
*/
$scope.pendingNotifications = [];
//
// scope functions
//
@ -80,6 +86,13 @@ define(function(require) {
return;
}
// let's close pending notifications for unread messages in the inbox
if (currentFolder().type === FOLDER_TYPE_INBOX) {
while ($scope.pendingNotifications.length) {
notification.close($scope.pendingNotifications.shift());
}
}
$scope.toggleUnread(email);
}
};
@ -364,7 +377,7 @@ define(function(require) {
//
(emailDao || {}).onIncomingMessage = function(msgs) {
var popupId, popupTitle, popupMessage, unreadMsgs;
var note, title, message, unreadMsgs;
unreadMsgs = msgs.filter(function(msg) {
return msg.unread;
@ -374,34 +387,35 @@ define(function(require) {
return;
}
popupId = '' + unreadMsgs[0].uid;
if (unreadMsgs.length > 1) {
popupTitle = unreadMsgs.length + ' new messages';
popupMessage = _.pluck(unreadMsgs, 'subject').join('\n');
if (unreadMsgs.length === 1) {
title = unreadMsgs[0].from[0].name || unreadMsgs[0].from[0].address;
message = unreadMsgs[0].subject;
} else {
popupTitle = unreadMsgs[0].from[0].name || unreadMsgs[0].from[0].address;
popupMessage = unreadMsgs[0].subject;
title = unreadMsgs.length + ' new messages';
message = _.pluck(unreadMsgs, 'subject').join('\n');
}
notification.create({
id: popupId,
title: popupTitle,
message: popupMessage
note = notification.create({
title: title,
message: message,
onClick: function() {
// force toggle into read mode when notification is clicked
firstSelect = false;
// remove from pending notificatiosn
var index = $scope.pendingNotifications.indexOf(note);
if (index !== -1) {
$scope.pendingNotifications.splice(index, 1);
}
// mark message as read
$scope.select(_.findWhere(currentFolder().messages, {
uid: unreadMsgs[0].uid
}));
}
});
$scope.pendingNotifications.push(note);
};
notification.setOnClickedListener(function(uidString) {
var uid = parseInt(uidString, 10);
if (isNaN(uid)) {
return;
}
firstSelect = false;
$scope.select(_.findWhere(currentFolder().messages, {
uid: uid
}));
});
};
//

View File

@ -106,9 +106,9 @@ define(function(require) {
function sentNotification(email) {
notification.create({
id: 'o' + email.id,
title: 'Message sent',
message: email.subject
message: email.subject,
timeout: 2000
}, function() {});
}

View File

@ -408,7 +408,11 @@ define(function(require) {
[].unshift.apply(folder.messages, messages); // add the new messages to the folder
updateUnreadCount(folder); // update the unread count
self.onIncomingMessage(messages); // notify about new messages
// notify about new messages only for the inbox
if (folder.type === FOLDER_TYPE_INBOX) {
self.onIncomingMessage(messages);
}
done();
});
}

View File

@ -5,22 +5,52 @@ define(function(require) {
var self = {};
self.create = function(options, callback) {
callback = callback || function() {};
if (window.chrome && chrome.notifications) {
chrome.notifications.create(options.id, {
type: 'basic',
title: options.title,
message: options.message,
iconUrl: chrome.runtime.getURL(cfg.iconPath)
}, callback);
if (window.Notification) {
self.hasPermission = Notification.permission === "granted";
}
/**
* Creates a notification. Requests permission if not already granted
*
* @param {String} options.title The notification title
* @param {String} options.message The notification message
* @param {Number} options.timeout (optional) Timeout when the notification is closed in milliseconds
* @param {Function} options.onClick (optional) callback when the notification is clicked
* @returns {Notification} A notification instance
*/
self.create = function(options) {
options.onClick = options.onClick || function() {};
if (!window.Notification) {
return;
}
if (!self.hasPermission) {
// don't wait until callback returns
Notification.requestPermission(function(permission) {
if (permission === "granted") {
self.hasPermission = true;
}
});
}
var notification = new Notification(options.title, {
body: options.message,
icon: cfg.iconPath
});
notification.onclick = options.onClick;
if (options.timeout > 0) {
setTimeout(function() {
notification.close();
}, options.timeout);
}
return notification;
};
self.setOnClickedListener = function(listener) {
if (window.chrome && chrome.notifications) {
chrome.notifications.onClicked.addListener(listener);
}
self.close = function(notification) {
notification.close();
};
return self;

View File

@ -367,11 +367,7 @@ define(function(require) {
}, function(err, folder) {
expect(err).to.not.exist;
expect(folder.exists).to.equal(1);
emailDao.onIncomingMessage = function(messages) {
expect(messages.length).to.equal(1);
expect(messages[0].id).to.equal('b');
done();
};
done();
});
});
});

View File

@ -444,6 +444,27 @@ define(function(require) {
});
});
it('should not notify for other folders', function(done) {
opts.folder = sentFolder;
imapListStub.withArgs(opts).yieldsAsync(null, [message]);
localStoreStub.withArgs({
folder: sentFolder,
emails: [message]
}).yieldsAsync();
dao.fetchMessages(opts, function(err) {
expect(err).to.not.exist;
expect(sentFolder.messages).to.contain(message);
expect(notified).to.be.false;
expect(localStoreStub.calledOnce).to.be.true;
expect(imapListStub.calledOnce).to.be.true;
done();
});
});
it('should verify verification mails', function(done) {
message.subject = verificationSubject;

View File

@ -15,7 +15,7 @@ define(function(require) {
describe('Mail List controller unit test', function() {
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
emailAddress, notificationClickedHandler, emails,
emailAddress, emails,
hasChrome, hasSocket, hasRuntime, hasIdentity;
beforeEach(function() {
@ -37,10 +37,6 @@ define(function(require) {
window.chrome.identity = {};
}
sinon.stub(notification, 'setOnClickedListener', function(func) {
notificationClickedHandler = func;
});
emails = [{
unread: true
}, {
@ -86,8 +82,6 @@ define(function(require) {
});
afterEach(function() {
notification.setOnClickedListener.restore();
if (!hasSocket) {
delete window.chrome.socket;
}
@ -260,6 +254,10 @@ define(function(require) {
scope._stopWatchTask();
});
afterEach(function() {
notification.create.restore();
});
it('should succeed for single mail', function(done) {
var mail = {
uid: 123,
@ -271,14 +269,21 @@ define(function(require) {
};
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();
opts.onClick();
expect(scope.state.mailList.selected).to.equal(mail);
done();
});
scope.state.nav = {
currentFolder: {
type: 'asd',
messages: [mail]
}
};
emailDaoMock.onIncomingMessage([mail]);
});
@ -307,50 +312,23 @@ define(function(require) {
}];
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();
opts.onClick();
expect(scope.state.mailList.selected).to.equal(mails[0]);
done();
});
scope.state.nav = {
currentFolder: {
type: 'asd',
messages: mails
}
};
emailDaoMock.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',
messages: [mail]
}
};
notificationClickedHandler('123');
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('getBody', function() {
@ -368,6 +346,9 @@ define(function(require) {
describe('select', function() {
it('should decrypt, focus mark an unread mail as read', function() {
scope.pendingNotifications = ['asd'];
sinon.stub(notification, 'close');
var mail = {
from: [{
address: 'asd'
@ -377,7 +358,7 @@ define(function(require) {
scope.state = {
nav: {
currentFolder: {
type: 'asd'
type: 'Inbox'
}
},
mailList: {},
@ -393,6 +374,10 @@ define(function(require) {
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail);
expect(notification.close.calledWith('asd')).to.be.true;
expect(notification.close.calledOnce).to.be.true;
notification.close.restore();
});
it('should decrypt and focus a read mail', function() {