From 6a0ae4d55d610e02e325da0900b911c7ad1c8d46 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 16 Jul 2014 12:47:25 +0200 Subject: [PATCH] Performance improvements and fixes for mail-list * Use same list style in desktop as mobile * Bugfix: don't download all body when list not displayed * Use ng-infinite-scroll to load DOM nodes on demand * Custom search filter for FTS and reomve angular filters * Rubber band scrolling on iOS * Add CSP support in cordova via html meta tag --- Gruntfile.js | 4 +- package.json | 4 +- src/index.html | 3 + src/js/app.js | 6 +- src/js/controller/mail-list.js | 177 +++++++++++++++++++++++++--- src/require-config.js | 12 +- src/sass/components/_mail-list.scss | 12 +- src/sass/views/_mail-list.scss | 14 +-- src/tpl/mail-list.html | 15 ++- test/unit/mail-list-ctrl-test.js | 142 ++++++++++++++++++++++ 10 files changed, 340 insertions(+), 49 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 0ce4392..7266b4c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -142,7 +142,9 @@ module.exports = function(grunt) { 'pgpmailer/node_modules/smtpclient/src/*.js', 'pgpmailer/node_modules/smtpclient/node_modules/stringencoding/dist/stringencoding.js', 'axe/axe.js', - 'dompurify/purify.js' + 'dompurify/purify.js', + 'jquery/dist/jquery.min.js', + 'ng-infinite-scroll/build/ng-infinite-scroll.min.js' ], dest: 'src/lib/' }, diff --git a/package.json b/package.json index 2825adc..7019bef 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.3.5", "requirejs": "2.1.14", "axe": "https://github.com/whiteout-io/axe/tarball/v0.0.2", - "dompurify": "~0.4.2" + "dompurify": "~0.4.2", + "jquery": "~2.1.1", + "ng-infinite-scroll": "https://github.com/sroze/ngInfiniteScroll/tarball/1.1.1" }, "devDependencies": { "angularjs": "https://github.com/angular/angular.js/tarball/v1.2.8", diff --git a/src/index.html b/src/index.html index b0d886a..6a817d2 100644 --- a/src/index.html +++ b/src/index.html @@ -4,6 +4,9 @@ Mail + + + diff --git a/src/js/app.js b/src/js/app.js index 976319e..35ca9e8 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -24,7 +24,8 @@ requirejs([ 'js/util/error', 'fastclick', 'angularRoute', - 'angularAnimate' + 'angularAnimate', + 'ngInfiniteScroll' ], function( angular, DialogCtrl, @@ -63,7 +64,8 @@ requirejs([ 'read', 'contacts', 'login-new-device', - 'popover' + 'popover', + 'infinite-scroll' ]); // set router paths diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index f88831f..1eb83a7 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -5,7 +5,10 @@ define(function(require) { _ = require('underscore'), appController = require('js/app-controller'), notification = require('js/util/notification'), - emailDao, outboxBo, keychainDao; + emailDao, outboxBo, keychainDao, searchTimeout; + + var INIT_DISPLAY_LEN = 20, + SCROLL_DISPLAY_LEN = 10; var MailListCtrl = function($scope, $timeout) { // @@ -154,6 +157,9 @@ define(function(require) { return; } + // reset searchFilter + $scope.searchText = undefined; + // in development, display dummy mail objects if (!window.chrome || !chrome.identity) { updateStatus('Last update: ', new Date()); @@ -165,26 +171,145 @@ define(function(require) { openCurrentFolder(); }); - $scope.$watchCollection('state.nav.currentFolder.messages', selectFirstMessage); - - function selectFirstMessage(messages) { + $scope.watchMessages = $scope.$watchCollection('state.nav.currentFolder.messages', function(messages) { if (!messages) { return; } + // sort message by uid + currentFolder().messages.sort(byUidDescending); + // set display buffer to first messages + $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); + // Shows the next message based on the uid of the currently selected element - if (messages.indexOf(currentMessage()) === -1) { - // wait until after first $digest() so $scope.filteredMessages is set + if (currentFolder().messages.indexOf(currentMessage()) === -1) { $timeout(function() { - $scope.select($scope.filteredMessages ? $scope.filteredMessages[0] : undefined); + $scope.select($scope.displayMessages[0]); }); } - } + }); + + /** + * display more items (for infinite scrolling) + */ + $scope.displayMore = function() { + var len = currentFolder().messages.length, + dLen = $scope.displayMessages.length; + + if (dLen === len || $scope.searchText) { + // all messages are already displayed or we're in search mode + return; + } + + // copy next interval of messages to the end of the display messages array + var next = currentFolder().messages.slice(dLen, dLen + SCROLL_DISPLAY_LEN); + Array.prototype.push.apply($scope.displayMessages, next); + }; + + /** + * This method is called when the user changes the searchText + */ + $scope.displaySearchResults = function(searchText) { + if (searchTimeout) { + // remove timeout to wait for user typing query + clearTimeout(searchTimeout); + } + + if (!searchText) { + // set display buffer to first messages + $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); + $scope.searching = false; + updateStatus('Online'); + return; + } + + // display searching spinner + $scope.searching = true; + updateStatus('Searching ...'); + searchTimeout = setTimeout(function() { + $scope.$apply(function() { + // filter relevant messages + $scope.displayMessages = $scope.search(currentFolder().messages, searchText); + $scope.searching = false; + updateStatus('Matches in this folder'); + }); + }, 500); + }; + + /** + * Do full text search on messages. Parse meta data first + */ + $scope.search = function(messages, searchText) { + // don't filter on empty searchText + if (!searchText) { + return messages; + } + + // escape search string + searchText = searchText.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + // compare all strings (case insensitive) + var regex = new RegExp(searchText, 'i'); + + function contains(input) { + if (!input) { + return false; + } + return regex.test(input); + } + + function checkAddresses(header) { + if (!header || !header.length) { + return false; + } + + for (var i = 0; i < header.length; i++) { + if (contains(header[i].name) || contains(header[i].address)) { + return true; + } + } + + return false; + } + + /** + * Filter meta data first and then only look at plaintext and decrypted message bodies + */ + function matchMetaDataFirst(m) { + // compare subject + if (contains(m.subject)) { + return true; + } + // compares address headers + if (checkAddresses(m.from) || checkAddresses(m.to) || checkAddresses(m.cc) || checkAddresses(m.bcc)) { + return true; + } + // compare plaintext body + if (m.body && !m.encrypted && contains(m.body)) { + return true; + } + // compare decrypted body + if (m.body && m.encrypted && m.decrypted && contains(m.body)) { + return true; + } + // compare plaintex html body + if (m.html && !m.encrypted && contains(m.html)) { + return true; + } + // compare decrypted html body + if (m.html && m.encrypted && m.decrypted && contains(m.html)) { + return true; + } + return false; + } + + // user native js Array.filter + return messages.filter(matchMetaDataFirst); + }; /** * Sync current folder when client comes back online */ - $scope.$watch('account.online', function(isOnline) { + $scope.watchOnline = $scope.$watch('account.online', function(isOnline) { if (isOnline) { updateStatus('Online'); openCurrentFolder(); @@ -315,23 +440,29 @@ define(function(require) { listItems = listEl.children[0].children, inViewport = false, listItem, message, - isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible; + isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible, + displayMessages = scope[model]; + + if (!top && !bottom) { + // list not visible + return; + } for (var i = 0, len = listItems.length; i < len; i++) { // the n-th list item (the dom representation of an email) corresponds to // the n-th message model in the filteredMessages array listItem = listItems.item(i).getBoundingClientRect(); - if (!scope.filteredMessages || scope.filteredMessages.length <= i) { + if (!displayMessages || displayMessages.length <= i) { // stop if i get larger than the size of filtered messages break; } - message = scope.filteredMessages[i]; + message = displayMessages[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 + isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { // we are now iterating over visible elements @@ -364,13 +495,23 @@ define(function(require) { }; }); + function byUidDescending(a, b) { + if (a.uid < b.uid) { + return 1; + } else if (b.uid < a.uid) { + return -1; + } else { + return 0; + } + } + // Helper for development mode function createDummyMails() { - var uid = 0; + var uid = 1000000; var Email = function(unread, attachments, answered) { - this.uid = uid++; + this.uid = uid--; this.from = [{ name: 'Whiteout Support', address: 'support@whiteout.io' @@ -489,7 +630,11 @@ define(function(require) { this.decrypted = true; }; - var dummys = [new Email(true, true), new Email(true, false, true), new Email(false, true, true), new Email(false, true), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false)]; + var dummys = [new Email(true, true), new Email(true, false, true), new Email(false, true, true), new Email(false, true)]; + + for (var i = 0; i < 100; i++) { + dummys.push(new Email(false)); + } return dummys; } diff --git a/src/require-config.js b/src/require-config.js index 1be8dd7..1b1054f 100644 --- a/src/require-config.js +++ b/src/require-config.js @@ -7,6 +7,7 @@ paths: { js: '../js', test: '../../test', + jquery: 'jquery.min', underscore: 'underscore/underscore-min', lawnchair: 'lawnchair/lawnchair-git', lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git', @@ -14,6 +15,7 @@ angular: 'angular/angular.min', angularRoute: 'angular/angular-route.min', angularAnimate: 'angular/angular-animate.min', + ngInfiniteScroll: 'ng-infinite-scroll.min', uuid: 'uuid/uuid', forge: 'forge/forge.min', punycode: 'punycode.min', @@ -24,8 +26,12 @@ forge: { exports: 'forge' }, + jquery: { + exports: '$' + }, angular: { - exports: 'angular' + exports: 'angular', + deps: ['jquery'] }, angularRoute: { exports: 'angular', @@ -35,6 +41,10 @@ exports: 'angular', deps: ['angular'] }, + ngInfiniteScroll: { + exports: 'angular', + deps: ['jquery', 'angular'] + }, lawnchair: { exports: 'Lawnchair' }, diff --git a/src/sass/components/_mail-list.scss b/src/sass/components/_mail-list.scss index 78520d2..c5e327f 100755 --- a/src/sass/components/_mail-list.scss +++ b/src/sass/components/_mail-list.scss @@ -159,16 +159,6 @@ } } } - - @include respond-to(desktop) { - margin-top: 0; - margin-bottom: 7px; - &:before { - display: none; - } - &:last-child { - margin-bottom: 0; - } - } } + } \ No newline at end of file diff --git a/src/sass/views/_mail-list.scss b/src/sass/views/_mail-list.scss index e73f810..0a5ec53 100755 --- a/src/sass/views/_mail-list.scss +++ b/src/sass/views/_mail-list.scss @@ -85,6 +85,9 @@ flex-grow: 1; padding: 0 ($padding-horizontal - $scrollbar-width) 0 $padding-horizontal; overflow-y: scroll; + // allow scrolling on iOS + -webkit-overflow-scrolling: touch; + // put layer on GPU transform: translatez(0); } @@ -127,15 +130,4 @@ } } - @include respond-to(desktop) { - background: $color-grey-lighterer; - background-image: linear-gradient(to right ,$color-grey-lighterer 98%, darken($color-grey-lighterer, 1%) 100%); - - footer { - background: darken($color-grey-lighterer, 1%); - &:before { - display: none; - } - } - } } \ No newline at end of file diff --git a/src/tpl/mail-list.html b/src/tpl/mail-list.html index a313de5..0b0894b 100644 --- a/src/tpl/mail-list.html +++ b/src/tpl/mail-list.html @@ -9,21 +9,24 @@ -
-