adapt ui to async content fetching

This commit is contained in:
Felix Hammerl 2014-02-17 14:31:14 +01:00 committed by Tankred Hase
parent 0e9f68abee
commit 250aa4b886
6 changed files with 277 additions and 43 deletions

View File

@ -58,6 +58,16 @@ define(function(require) {
// scope functions
//
$scope.getContent = function(email) {
emailDao.getMessageContent({
folder: getFolder().path,
message: email
}, function(error) {
$scope.$apply();
$scope.onError(error);
});
};
/**
* Called when clicking on an email list item
*/
@ -67,6 +77,13 @@ define(function(require) {
return;
}
emailDao.decryptMessageContent({
message: email
}, function(error) {
$scope.$apply();
$scope.onError(error);
});
$scope.state.mailList.selected = email;
$scope.state.read.toggle(true);
@ -279,10 +296,88 @@ define(function(require) {
}]; // list of receivers
if (attachments) {
// body structure with three attachments
this.bodystructure = {"1": {"part": "1","type": "text/plain","parameters": {"charset": "us-ascii"},"encoding": "7bit","size": 9,"lines": 2},"2": {"part": "2","type": "application/octet-stream","parameters": {"name": "a.md"},"encoding": "7bit","size": 123,"disposition": [{"type": "attachment","filename": "a.md"}]},"3": {"part": "3","type": "application/octet-stream","parameters": {"name": "b.md"},"encoding": "7bit","size": 456,"disposition": [{"type": "attachment","filename": "b.md"}]},"4": {"part": "4","type": "application/octet-stream","parameters": {"name": "c.md"},"encoding": "7bit","size": 789,"disposition": [{"type": "attachment","filename": "c.md"}]},"type": "multipart/mixed"};
this.attachments = [{"filename": "a.md","filesize": 123,"mimeType": "text/x-markdown","part": "2","content": null}, {"filename": "b.md","filesize": 456,"mimeType": "text/x-markdown","part": "3","content": null}, {"filename": "c.md","filesize": 789,"mimeType": "text/x-markdown","part": "4","content": null}];
this.bodystructure = {
"1": {
"part": "1",
"type": "text/plain",
"parameters": {
"charset": "us-ascii"
},
"encoding": "7bit",
"size": 9,
"lines": 2
},
"2": {
"part": "2",
"type": "application/octet-stream",
"parameters": {
"name": "a.md"
},
"encoding": "7bit",
"size": 123,
"disposition": [{
"type": "attachment",
"filename": "a.md"
}]
},
"3": {
"part": "3",
"type": "application/octet-stream",
"parameters": {
"name": "b.md"
},
"encoding": "7bit",
"size": 456,
"disposition": [{
"type": "attachment",
"filename": "b.md"
}]
},
"4": {
"part": "4",
"type": "application/octet-stream",
"parameters": {
"name": "c.md"
},
"encoding": "7bit",
"size": 789,
"disposition": [{
"type": "attachment",
"filename": "c.md"
}]
},
"type": "multipart/mixed"
};
this.attachments = [{
"filename": "a.md",
"filesize": 123,
"mimeType": "text/x-markdown",
"part": "2",
"content": null
}, {
"filename": "b.md",
"filesize": 456,
"mimeType": "text/x-markdown",
"part": "3",
"content": null
}, {
"filename": "c.md",
"filesize": 789,
"mimeType": "text/x-markdown",
"part": "4",
"content": null
}];
} else {
this.bodystructure = {"part": "1","type": "text/plain","parameters": {"charset": "us-ascii"},"encoding": "7bit","size": 9,"lines": 2};
this.bodystructure = {
"part": "1",
"type": "text/plain",
"parameters": {
"charset": "us-ascii"
},
"encoding": "7bit",
"size": 9,
"lines": 2
};
this.attachments = [];
}
this.unread = unread;
@ -311,8 +406,37 @@ define(function(require) {
var myScroll;
// activate iscroll
myScroll = new IScroll(elm[0], {
mouseWheel: true
mouseWheel: true,
});
// load the visible message bodies, when the list is re-initialized and when scrolling stopped
loadVisible();
myScroll.on('scrollEnd', loadVisible);
function loadVisible() {
var list = elm[0].getBoundingClientRect(),
footerHeight = elm[0].nextElementSibling.getBoundingClientRect().height,
top = list.top,
bottom = list.bottom - footerHeight,
listItems = elm[0].children[0].children,
i = listItems.length,
listItem, message,
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible;
while (i--) {
listItem = listItems.item(i).getBoundingClientRect();
message = scope.filteredMessages[i];
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) {
scope.getContent(message);
}
}
}
}, true);
}
};

View File

@ -742,12 +742,17 @@ define(function(require) {
message = options.message,
folder = options.folder;
// the message already has a body, so no need to become active here
if (message.body) {
callback(null, message);
if (message.loadingBody) {
return;
}
// the message already has a body, so no need to become active here
if (message.body) {
return;
}
message.loadingBody = true;
// the mail does not have its content in memory
readFromDevice();
@ -760,6 +765,7 @@ define(function(require) {
var localMessage;
if (err) {
message.loadingBody = false;
callback(err);
return;
}
@ -785,10 +791,13 @@ define(function(require) {
message: message
}, function(error) {
if (error) {
message.loadingBody = false;
callback(error);
return;
}
message.loadingBody = false;
self._localStoreMessages({
folder: folder,
emails: [message]
@ -813,6 +822,7 @@ define(function(require) {
message.decrypted = false;
extractCiphertext();
}
message.loadingBody = false;
callback(null, message);
}
@ -834,15 +844,17 @@ define(function(require) {
var self = this,
message = options.message;
// the message is not encrypted or has already been decrypted
if (!message.encrypted || message.decrypted) {
callback(null, message);
// the message has no body, is not encrypted or has already been decrypted
if (message.decryptingBody || !message.body || !message.encrypted || message.decrypted) {
return;
}
message.decryptingBody = true;
// get the sender's public key for signature checking
self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) {
if (err) {
message.decryptingBody = false;
callback(err);
return;
}
@ -850,6 +862,7 @@ define(function(require) {
if (!senderPublicKey) {
// this should only happen if a mail from another channel is in the inbox
message.body = 'Public key for sender not found!';
message.decryptingBody = false;
callback(null, message);
return;
}
@ -863,6 +876,7 @@ define(function(require) {
if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && decrypted.indexOf('Content-Type:') === -1) {
message.body = decrypted;
message.decrypted = true;
message.decryptingBody = false;
callback(null, message);
return;
}
@ -873,6 +887,7 @@ define(function(require) {
block: decrypted
}, function(error) {
if (error) {
message.decryptingBody = false;
callback(error);
return;
}
@ -885,7 +900,8 @@ define(function(require) {
});
// we're done here!
callback(error, message);
message.decryptingBody = false;
callback(null, message);
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@
<div class="list-wrapper" ng-iscroll="state.nav.currentFolder.messages.length">
<ul class="mail-list">
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread, 'mail-list-replied': !email.unread && email.answered}" ng-click="select(email)" ng-repeat="email in state.nav.currentFolder.messages | orderBy:'uid':true | filter:searchText">
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread, 'mail-list-replied': !email.unread && email.answered}" ng-click="select(email)" ng-repeat="email in (filteredMessages = (state.nav.currentFolder.messages | orderBy:'uid':true | filter:searchText))">
<h3>{{email.from[0].name || email.from[0].address}}</h3>
<div class="head">
<div class="flag" data-icon="{{(!email.unread && email.answered) ? '&#xe002;' : ''}}" ng-click="toggleUnread(email); $event.stopPropagation()"></div>

View File

@ -929,19 +929,16 @@ define(function(require) {
});
describe('getMessageContent', function() {
it('should not do anything if the message already has content', function(done) {
it('should not do anything if the message already has content', function() {
var message = {
body: 'bender is great!'
};
dao.getMessageContent({
message: message
}, function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
done();
});
// should do nothing
});
it('should read an unencrypted body from the device', function(done) {
@ -957,7 +954,7 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').withArgs({
folder: folder,
uid: uid
}).yields(null, [{
}).yieldsAsync(null, [{
body: body
}]);
@ -967,13 +964,17 @@ define(function(require) {
folder: folder
}, function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.false;
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
done();
});
expect(message.loadingBody).to.be.true;
});
it('should read an encrypted body from the device', function(done) {
@ -989,7 +990,7 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').withArgs({
folder: folder,
uid: uid
}).yields(null, [{
}).yieldsAsync(null, [{
body: body
}]);
@ -999,14 +1000,18 @@ define(function(require) {
folder: folder
}, function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.true;
expect(msg.decrypted).to.be.false;
expect(message.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
done();
});
expect(message.loadingBody).to.be.true;
});
it('should stream an unencrypted body from imap', function(done) {
@ -1022,12 +1027,12 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').withArgs({
folder: folder,
uid: uid
}).yields(null, [{}]);
}).yieldsAsync(null, [{}]);
localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({
folder: folder,
emails: [message]
}).yields();
}).yieldsAsync();
imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) {
expect(opts).to.deep.equal({
@ -1045,15 +1050,19 @@ define(function(require) {
folder: folder
}, function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.false;
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
done();
});
expect(message.loadingBody).to.be.true;
});
it('should stream an encrypted body from imap', function(done) {
@ -1069,12 +1078,12 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').withArgs({
folder: folder,
uid: uid
}).yields(null, [{}]);
}).yieldsAsync(null, [{}]);
localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({
folder: folder,
emails: [message]
}).yields();
}).yieldsAsync();
imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) {
expect(opts).to.deep.equal({
@ -1092,17 +1101,20 @@ define(function(require) {
folder: folder
}, function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.true;
expect(msg.decrypted).to.be.false;
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
done();
});
expect(message.loadingBody).to.be.true;
});
it('fail to stream from imap due to error when persisting', function(done) {
@ -1139,6 +1151,8 @@ define(function(require) {
expect(imapStreamStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
expect(message.loadingBody).to.be.false;
done();
});
});
@ -1174,28 +1188,27 @@ define(function(require) {
expect(imapStreamStub.calledOnce).to.be.true;
expect(localStoreStub.called).to.be.false;
expect(message.loadingBody).to.be.false;
done();
});
});
});
describe('decryptMessageContent', function() {
it('should not do anything when the message is not encrypted', function(done) {
it('should not do anything when the message is not encrypted', function() {
var message = {
encrypted: false
};
dao.decryptMessageContent({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
done();
});
// should do nothing
});
it('should not do anything when the message is already decrypted', function(done) {
it('should not do anything when the message is already decrypted', function() {
var message = {
encrypted: true,
decrypted: true
@ -1203,12 +1216,9 @@ define(function(require) {
dao.decryptMessageContent({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
done();
});
// should do nothing
});
it('decrypt a pgp/mime message', function(done) {
@ -1224,8 +1234,8 @@ define(function(require) {
mimeBody = 'Content-Transfer-Encoding: Content-Type:';
parsedBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yields(null, mimeBody);
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb){
expect(o.message).to.equal(message);
expect(o.block).to.equal(mimeBody);
@ -1238,15 +1248,20 @@ define(function(require) {
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(parsedBody);
expect(msg.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.calledOnce).to.be.true;
done();
});
expect(message.decryptingBody).to.be.true;
});
it('decrypt a pgp/inline message', function(done) {
@ -1261,23 +1276,27 @@ define(function(require) {
plaintextBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yields(null, plaintextBody);
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(plaintextBody);
expect(msg.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.called).to.be.false;
done();
});
expect(message.decryptingBody).to.be.true;
});
it('should fail during decryption message', function(done) {
@ -1303,9 +1322,12 @@ define(function(require) {
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(errMsg);
expect(msg.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.called).to.be.false;
@ -1331,8 +1353,12 @@ define(function(require) {
message: message
}, function(error, msg) {
expect(error).to.exist;
expect(msg).to.not.exist;
expect(message.decrypted).to.be.false;
expect(message.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.called).to.be.false;
expect(parseStub.called).to.be.false;

View File

@ -10,6 +10,8 @@ define(function(require) {
KeychainDAO = require('js/dao/keychain-dao'),
appController = require('js/app-controller');
chai.Assertion.includeStack = true;
describe('Mail List controller unit test', function() {
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
emailAddress, notificationClickedHandler, emails,
@ -223,6 +225,72 @@ define(function(require) {
});
});
describe('getContent', function() {
it('should get the mail content', function() {
scope.state.nav = {
currentFolder: {
type: 'asd',
}
};
scope.getContent();
expect(emailDaoMock.getMessageContent.calledOnce).to.be.true;
});
});
describe('select', function() {
it('should decrypt, focus mark an unread mail as read', function() {
var mail, synchronizeMock;
mail = {
unread: true
};
synchronizeMock = sinon.stub(scope, 'synchronize');
scope.state = {
nav: {
currentFolder: {
type: 'asd',
}
},
mailList: {},
read: {
toggle: function() {}
}
};
scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true;
expect(synchronizeMock.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail);
scope.synchronize.restore();
});
it('should decrypt and focus a read mail', function() {
var mail, synchronizeMock;
mail = {
unread: false
};
synchronizeMock = sinon.stub(scope, 'synchronize');
scope.state = {
mailList: {},
read: {
toggle: function() {}
}
};
scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true;
expect(synchronizeMock.called).to.be.false;
expect(scope.state.mailList.selected).to.equal(mail);
scope.synchronize.restore();
});
});
describe('remove', function() {
it('should not delete without a selected mail', function() {
scope.remove();