(js) Show loading progress for messages and cards

This commit is contained in:
Francis Lachapelle 2016-05-30 12:29:58 -04:00
parent c46a5f8df7
commit 5c2aedb3c9
11 changed files with 135 additions and 30 deletions

1
NEWS
View file

@ -5,6 +5,7 @@ Enhancements
- [web] expose all email addresses in autocompletion of message editor (#3443) - [web] expose all email addresses in autocompletion of message editor (#3443)
- [web] Gravatar service can now be disabled (#3600) - [web] Gravatar service can now be disabled (#3600)
- [web] collapsable mail accounts (#3493) - [web] collapsable mail accounts (#3493)
- [web] show progress indicator when loading messages and cards
Bug fixes Bug fixes
- [web] fixed creation of chip on blur (sgTransformOnBlur directive) - [web] fixed creation of chip on blur (sgTransformOnBlur directive)

View file

@ -411,17 +411,22 @@
<!-- selected avatar --> <!-- selected avatar -->
<!-- </div> --> <!-- </div> -->
<sg-avatar-image class="md-avatar" <sg-avatar-image class="md-avatar"
ng-if="addressbook.notSelectedComponent(currentCard, 'vcard')" ng-if="currentCard.$isCard()"
sg-email="currentCard.$preferredEmail(addressbook.selectedFolder.constructor.$query.value)" sg-email="currentCard.$preferredEmail(addressbook.selectedFolder.constructor.$query.value)"
sg-src="currentCard.photoURL" sg-src="currentCard.photoURL"
size="40"> size="40">
<!-- contact avatar --> <!-- contact avatar -->
</sg-avatar-image> </sg-avatar-image>
<div class="md-avatar sg-avatar-list" <div class="md-avatar sg-avatar-list"
ng-show="addressbook.notSelectedComponent(currentCard, 'vlist')"> ng-show="currentCard.$isList()">
<!-- list avatar --> <!-- list avatar -->
</div> </div>
</div> </div>
<div class="sg-progress-linear-bottom">
<md-progress-linear class="md-accent"
md-mode="indeterminate"
ng-show="currentCard.$isLoading()"><!-- progress --></md-progress-linear>
</div>
</md-list-item> </md-list-item>
</md-list> </md-list>
</md-virtual-repeat-container> </md-virtual-repeat-container>

View file

@ -302,6 +302,11 @@
<md-icon class="ng-hide" ng-show="currentMessage.isforwarded">forward</md-icon> <md-icon class="ng-hide" ng-show="currentMessage.isforwarded">forward</md-icon>
<md-icon class="ng-hide" ng-show="currentMessage.hasattachment">attach_file</md-icon> <md-icon class="ng-hide" ng-show="currentMessage.hasattachment">attach_file</md-icon>
</div> </div>
<div class="sg-progress-linear-bottom">
<md-progress-linear class="md-accent"
md-mode="indeterminate"
ng-show="currentMessage.$isLoading()"><!-- message loading progress --></md-progress-linear>
</div>
</md-list-item> </md-list-item>
</md-list> </md-list>
</md-virtual-repeat-container> </md-virtual-repeat-container>
@ -309,7 +314,7 @@
ng-show="mailbox.service.selectedFolder.$isLoading"> ng-show="mailbox.service.selectedFolder.$isLoading">
<md-progress-circular class="md-accent" <md-progress-circular class="md-accent"
md-mode="indeterminate" md-mode="indeterminate"
md-diameter="32"><!-- progress --></md-progress-circular> md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
</div> </div>
<md-button class="md-fab md-fab-bottom-right md-accent" <md-button class="md-fab md-fab-bottom-right md-accent"
label:aria-label="Write a new message" label:aria-label="Write a new message"

View file

@ -483,7 +483,7 @@
// Add new cards matching the search query // Add new cards matching the search query
_.forEach(results, function(cardId, index) { _.forEach(results, function(cardId, index) {
if (_.isUndefined(_.find(cards, _.bind(compareIds, cardId)))) { if (_.isUndefined(_.find(cards, _.bind(compareIds, cardId)))) {
var data = { id: cardId }; var data = { pid: addressbookId, id: cardId };
var card = new AddressBook.$Card(data, search); var card = new AddressBook.$Card(data, search);
cards.splice(index, 0, card); cards.splice(index, 0, card);
} }
@ -686,7 +686,7 @@
// Instanciate Card objects // Instanciate Card objects
_.reduce(_this.ids, function(cards, card, i) { _.reduce(_this.ids, function(cards, card, i) {
var data = { id: card }; var data = { pid: _this.id, id: card };
// Build map of ID <=> index // Build map of ID <=> index
_this.idsMap[data.id] = i; _this.idsMap[data.id] = i;

View file

@ -17,7 +17,6 @@
vm.selectCard = selectCard; vm.selectCard = selectCard;
vm.toggleCardSelection = toggleCardSelection; vm.toggleCardSelection = toggleCardSelection;
vm.newComponent = newComponent; vm.newComponent = newComponent;
vm.notSelectedComponent = notSelectedComponent;
vm.unselectCards = unselectCards; vm.unselectCards = unselectCards;
vm.confirmDeleteSelectedCards = confirmDeleteSelectedCards; vm.confirmDeleteSelectedCards = confirmDeleteSelectedCards;
vm.copySelectedCards = copySelectedCards; vm.copySelectedCards = copySelectedCards;
@ -79,10 +78,6 @@
} }
} }
function notSelectedComponent(currentCard, type) {
return (currentCard && currentCard.c_component == type && !currentCard.selected);
}
function unselectCards() { function unselectCards() {
_.forEach(vm.selectedFolder.$cards, function(card) { _.forEach(vm.selectedFolder.$cards, function(card) {
card.selected = false; card.selected = false;
@ -180,13 +175,11 @@
}); });
} }
else { else {
promises.push(vm.selectedFolder.$getCard(card.id).then(function(card) { promises.push(card.$reload().then(function(card) {
return card.$futureCardData.then(function(data) { _.forEach(card.refs, function(ref) {
_.forEach(data.refs, function(ref) {
if (ref.email.length) if (ref.email.length)
recipients.push(ref.$shortFormat()); recipients.push(ref.$shortFormat());
}); });
});
})); }));
} }
} }

View file

@ -71,8 +71,10 @@
angular.module('SOGo.ContactsUI') angular.module('SOGo.ContactsUI')
.constant('sgCard_STATUS', { .constant('sgCard_STATUS', {
NOT_LOADED: 0, NOT_LOADED: 0,
LOADING: 1, DELAYED_LOADING: 1,
LOADED: 2 LOADING: 2,
LOADED: 3,
DELAYED_MS: 300
}) })
.factory('Card', Card.$factory); .factory('Card', Card.$factory);
@ -164,6 +166,33 @@
}); });
}; };
/**
* @function $isLoading
* @memberof Card.prototype
* @returns true if the Card definition is still being retrieved from server after a specific delay
* @see sgCard_STATUS
*/
Card.prototype.$isLoading = function() {
return this.$loaded == Card.STATUS.LOADING;
};
/**
* @function $reload
* @memberof Message.prototype
* @desc Fetch the viewable message body along with other metadata such as the list of attachments.
* @returns a promise of the HTTP operation
*/
Card.prototype.$reload = function() {
var futureCardData;
if (this.$futureCardData)
return this;
futureCardData = Card.$$resource.fetch([this.pid, this.id].join('/'), 'view');
return this.$unwrap(futureCardData);
};
/** /**
* @function $save * @function $save
* @memberof Card.prototype * @memberof Card.prototype
@ -498,7 +527,11 @@
var _this = this; var _this = this;
// Card is not loaded yet // Card is not loaded yet
this.$loaded = Card.STATUS.LOADING; this.$loaded = Card.STATUS.DELAYED_LOADING;
Card.$timeout(function() {
if (_this.$loaded != Card.STATUS.LOADED)
_this.$loaded = Card.STATUS.LOADING;
}, Card.STATUS.DELAYED_MS);
// Expose the promise // Expose the promise
this.$futureCardData = futureCardData.then(function(data) { this.$futureCardData = futureCardData.then(function(data) {
@ -523,6 +556,8 @@
return _this; return _this;
}); });
return this.$futureCardData;
}; };
/** /**

View file

@ -64,7 +64,9 @@
}, },
resolve: { resolve: {
stateCard: stateCard stateCard: stateCard
} },
onEnter: onEnterCard,
onExit: onExitCard
}) })
.state('app.addressbook.card.view', { .state('app.addressbook.card.view', {
url: '/view', url: '/view',
@ -129,10 +131,37 @@
/** /**
* @ngInject * @ngInject
*/ */
stateCard.$inject = ['$stateParams', 'stateAddressbook']; stateCard.$inject = ['$state', '$stateParams', 'stateAddressbook'];
function stateCard($stateParams, stateAddressbook) { function stateCard($state, $stateParams, stateAddressbook) {
var card;
card = _.find(stateAddressbook.$cards, function(cardObject) {
return (cardObject.id == $stateParams.cardId);
});
if (card) {
return card.$reload();
}
else {
// Card not found
$state.go('app.addressbook');
}
}
/**
* @ngInject
*/
onEnterCard.$inject = ['$stateParams', 'stateAddressbook'];
function onEnterCard($stateParams, stateAddressbook) {
stateAddressbook.selectedCard = $stateParams.cardId; stateAddressbook.selectedCard = $stateParams.cardId;
return stateAddressbook.$getCard($stateParams.cardId); }
/**
* @ngInject
*/
onExitCard.$inject = ['stateAddressbook'];
function onExitCard(stateMailbox) {
delete stateAddressbook.selectedCard;
} }
/** /**

View file

@ -600,7 +600,7 @@
* @return the index of the first deleted message * @return the index of the first deleted message
*/ */
Mailbox.prototype.$_deleteMessages = function(uids, messages) { Mailbox.prototype.$_deleteMessages = function(uids, messages) {
var _this = this, selectedMessages, selectedUIDs, unseen, firstIndex = this.$messages.length; var _this = this, selectedUIDs, _$messages, unseen, firstIndex = this.$messages.length;
// Decrement the unseen count // Decrement the unseen count
unseen = _.filter(messages, function(message, i) { return !message.isread; }); unseen = _.filter(messages, function(message, i) { return !message.isread; });

View file

@ -61,13 +61,13 @@
controllerAs: 'viewer' controllerAs: 'viewer'
} }
}, },
onEnter: onEnterMessage,
onExit: onExitMessage,
resolve: { resolve: {
stateMailbox: stateVirtualMailboxOfMessage, stateMailbox: stateVirtualMailboxOfMessage,
stateMessages: stateMessages, stateMessages: stateMessages,
stateMessage: stateMessage stateMessage: stateMessage
} },
onEnter: onEnterMessage,
onExit: onExitMessage
}) })
.state('mail.account.inbox', { .state('mail.account.inbox', {
url: '/inbox', url: '/inbox',
@ -279,7 +279,7 @@
}); });
if (message) { if (message) {
return message.$reload(); return message.$reload({useCache: true});
} }
else { else {
// Message not found // Message not found

View file

@ -39,8 +39,9 @@
* @desc The factory we'll use to register with Angular * @desc The factory we'll use to register with Angular
* @returns the Message constructor * @returns the Message constructor
*/ */
Message.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Gravatar', 'Resource', 'Preferences', function($q, $timeout, $log, Settings, Gravatar, Resource, Preferences) { Message.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'sgMessage_STATUS', 'Gravatar', 'Resource', 'Preferences', function($q, $timeout, $log, Settings, Message_STATUS, Gravatar, Resource, Preferences) {
angular.extend(Message, { angular.extend(Message, {
STATUS: Message_STATUS,
$q: $q, $q: $q,
$timeout: $timeout, $timeout: $timeout,
$log: $log, $log: $log,
@ -72,6 +73,13 @@
angular.module('SOGo.MailerUI', ['SOGo.Common']); angular.module('SOGo.MailerUI', ['SOGo.Common']);
} }
angular.module('SOGo.MailerUI') angular.module('SOGo.MailerUI')
.constant('sgMessage_STATUS', {
NOT_LOADED: 0,
DELAYED_LOADING: 1,
LOADING: 2,
LOADED: 3,
DELAYED_MS: 300
})
.factory('Message', Message.$factory); .factory('Message', Message.$factory);
/** /**
@ -503,15 +511,29 @@
}); });
}; };
/**
* @function $isLoading
* @memberof Message.prototype
* @returns true if the Message content is still being retrieved from server after a specific delay
* @see sgMessage_STATUS
*/
Message.prototype.$isLoading = function() {
return this.$loaded == Message.STATUS.LOADING;
};
/** /**
* @function $reload * @function $reload
* @memberof Message.prototype * @memberof Message.prototype
* @desc Fetch the viewable message body along with other metadata such as the list of attachments. * @desc Fetch the viewable message body along with other metadata such as the list of attachments.
* @param {object} [options] - set {useCache: true} to use already fetched data
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Message.prototype.$reload = function(options) { Message.prototype.$reload = function(options) {
var futureMessageData; var futureMessageData;
if (options && options.useCache && this.$futureMessageData)
return this;
futureMessageData = Message.$$resource.fetch(this.$absolutePath(options), 'view'); futureMessageData = Message.$$resource.fetch(this.$absolutePath(options), 'view');
return this.$unwrap(futureMessageData); return this.$unwrap(futureMessageData);
@ -638,6 +660,13 @@
Message.prototype.$unwrap = function(futureMessageData) { Message.prototype.$unwrap = function(futureMessageData) {
var _this = this; var _this = this;
// Message is not loaded yet
this.$loaded = Message.STATUS.DELAYED_LOADING;
Message.$timeout(function() {
if (_this.$loaded != Message.STATUS.LOADED)
_this.$loaded = Message.STATUS.LOADING;
}, Message.STATUS.DELAYED_MS);
// Resolve and expose the promise // Resolve and expose the promise
this.$futureMessageData = futureMessageData.then(function(data) { this.$futureMessageData = futureMessageData.then(function(data) {
// Calling $timeout will force Angular to refresh the view // Calling $timeout will force Angular to refresh the view
@ -653,6 +682,7 @@
angular.extend(_this, data); angular.extend(_this, data);
_this.$formatFullAddresses(); _this.$formatFullAddresses();
_this.$loadUnsafeContent = false; _this.$loadUnsafeContent = false;
_this.$loaded = Message.STATUS.LOADED;
return _this; return _this;
}); });
}); });

View file

@ -1,2 +1,9 @@
/// progressLinear.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*- /// progressLinear.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
@import 'extends'; @import 'extends';
.sg-progress-linear-bottom {
bottom: 0;
left: 0;
position: absolute;
right: 0;
}