Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
342 lines
10 KiB
JavaScript
Executable file
342 lines
10 KiB
JavaScript
Executable file
/*global define */
|
|
'use strict';
|
|
/**
|
|
* @fileoverview Bug 918303 - HeaderCursor added to provide MessageListCard and
|
|
* MessageReaderCard the current message and whether there are adjacent
|
|
* messages that can be advanced to. Expect for [other] consumers to add
|
|
* additional data to messagesSlice items after they've left the MailAPI.
|
|
*/
|
|
define(function(require) {
|
|
var array = require('array'),
|
|
evt = require('evt');
|
|
|
|
function makeListener(type, obj) {
|
|
return function() {
|
|
var args = Array.slice(arguments);
|
|
this.emit.apply(this, ['messages_' + type].concat(args));
|
|
}.bind(obj);
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function HeaderCursor(model) {
|
|
// Inherit from evt.Emitter.
|
|
evt.Emitter.call(this);
|
|
|
|
this.model = model;
|
|
|
|
// Local binding since it is passed as an evt listener.
|
|
this.onLatestFolder = this.onLatestFolder.bind(this);
|
|
|
|
// Need to distinguish between search and nonsearch slices,
|
|
// since there can be two cards that both are listening for
|
|
// slice changes, but one is for search output, and one is
|
|
// for nonsearch output. The message_list is an example.
|
|
this.searchMode = 'nonsearch';
|
|
}
|
|
|
|
HeaderCursor.prototype = evt.mix({
|
|
/**
|
|
* @type {CurrentMessage}
|
|
*/
|
|
currentMessage: null,
|
|
|
|
/**
|
|
* @type {HeadersViewSlice}
|
|
*/
|
|
messagesSlice: null,
|
|
|
|
/**
|
|
* @type {String}
|
|
*/
|
|
expectingMessageSuid: null,
|
|
|
|
/**
|
|
* @type {Array}
|
|
*/
|
|
sliceEvents: ['splice', 'change', 'status', 'remove', 'complete'],
|
|
|
|
_inited: false,
|
|
|
|
/**
|
|
* Sets up the event wiring and will trigger the slice creation by listening
|
|
* to model 'folder' changes. Want to wait until there are views that need
|
|
* to use the header_cursor for showing UI, to avoid extra work, like in the
|
|
* background sync case.
|
|
*/
|
|
init: function() {
|
|
this._inited = true;
|
|
|
|
// Listen for some slice events to do some special work.
|
|
this.on('messages_splice', this.onMessagesSplice.bind(this));
|
|
this.on('messages_remove', this.onMessagesSpliceRemove.bind(this));
|
|
this.on('messages_complete', function() {
|
|
// Consumers, like message_list, always want their 'complete' work
|
|
// to fire, but by default the slice removes the complete handler
|
|
// at the end. So rebind on each call here.
|
|
if (this.messagesSlice) {
|
|
this.messagesSlice.oncomplete = makeListener('complete', this);
|
|
}
|
|
}.bind(this));
|
|
|
|
// Listen to model for folder changes.
|
|
this.model.latest('folder', this.onLatestFolder);
|
|
},
|
|
|
|
/**
|
|
* The messageReader told us it wanted to advance, so we should go ahead
|
|
* and update our currentMessage appropriately and then report the new one.
|
|
*
|
|
* @param {string} direction either 'next' or 'previous'.
|
|
*/
|
|
advance: function(direction) {
|
|
var index = this.indexOfMessageById(this.currentMessage.header.id);
|
|
switch (direction) {
|
|
case 'previous':
|
|
index -= 1;
|
|
break;
|
|
case 'next':
|
|
index += 1;
|
|
break;
|
|
}
|
|
|
|
var messages = this.messagesSlice.items;
|
|
if (index < 0 || index >= messages.length) {
|
|
// We can't advance that far!
|
|
return;
|
|
}
|
|
|
|
this.setCurrentMessageByIndex(index);
|
|
},
|
|
|
|
/**
|
|
* Tracks a messageSuid to use in selecting
|
|
* the currentMessage once the slice data loads.
|
|
* @param {String} messageSuid The message suid.
|
|
*/
|
|
setCurrentMessageBySuid: function(messageSuid) {
|
|
this.expectingMessageSuid = messageSuid;
|
|
this.checkExpectingMessageSuid();
|
|
},
|
|
|
|
/**
|
|
* Sets the currentMessage if there are messages now to check
|
|
* against expectingMessageSuid. Only works if current folder
|
|
* is set to an "inbox" type, so only useful for jumps into
|
|
* the email app from an entry point like a notification.
|
|
* @param {Boolean} eventIfNotFound if set to true, an event
|
|
* is emitted if the messageSuid is not found in the set of
|
|
* messages.
|
|
*/
|
|
checkExpectingMessageSuid: function(eventIfNotFound) {
|
|
var messageSuid = this.expectingMessageSuid;
|
|
var model = this.model;
|
|
|
|
if (!messageSuid || !model.folder || model.folder.type !== 'inbox') {
|
|
return;
|
|
}
|
|
|
|
var index = this.indexOfMessageById(messageSuid);
|
|
if (index > -1) {
|
|
this.expectingMessageSuid = null;
|
|
return this.setCurrentMessageByIndex(index);
|
|
}
|
|
|
|
if (eventIfNotFound) {
|
|
console.error('header_cursor could not find messageSuid ' +
|
|
messageSuid + ', emitting messageSuidNotFound');
|
|
this.emit('messageSuidNotFound', messageSuid);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {MailHeader} header message header.
|
|
*/
|
|
setCurrentMessage: function(header) {
|
|
if (!header) {
|
|
return;
|
|
}
|
|
|
|
this.setCurrentMessageByIndex(this.indexOfMessageById(header.id));
|
|
},
|
|
|
|
setCurrentMessageByIndex: function(index) {
|
|
var messages = this.messagesSlice.items;
|
|
|
|
// Do not bother if not a valid index.
|
|
if (index === -1 || index > messages.length - 1) {
|
|
return;
|
|
}
|
|
|
|
var header = messages[index];
|
|
if ('header' in header) {
|
|
header = header.header;
|
|
}
|
|
|
|
var currentMessage = new CurrentMessage(header, {
|
|
hasPrevious: index !== 0, // Can't be first
|
|
hasNext: index !== messages.length - 1 // Can't be last
|
|
});
|
|
|
|
this.emit('currentMessage', currentMessage, index);
|
|
this.currentMessage = currentMessage;
|
|
},
|
|
|
|
/**
|
|
* @param {string} id message id.
|
|
* @return {number} the index of the message cursor's current message
|
|
* in the message slice it has checked out.
|
|
*/
|
|
indexOfMessageById: function(id) {
|
|
var messages = (this.messagesSlice && this.messagesSlice.items) || [];
|
|
return array.indexOfGeneric(messages, function(message) {
|
|
var other = 'header' in message ? message.header.id : message.id;
|
|
return other === id;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @param {Object} folder the folder we switched to.
|
|
*/
|
|
onLatestFolder: function(folder) {
|
|
// It is possible that the notification of latest folder is fired
|
|
// but in the meantime the foldersSlice could be cleared due to
|
|
// a change in the current account, before this listener is called.
|
|
// So skip this work if no foldersSlice, this method will be called
|
|
// again soon.
|
|
if (!this.model.foldersSlice) {
|
|
return;
|
|
}
|
|
|
|
this.freshMessagesSlice();
|
|
},
|
|
|
|
startSearch: function(phrase, whatToSearch) {
|
|
this.searchMode = 'search';
|
|
this.bindToSlice(this.model.api.searchFolderMessages(this.model.folder,
|
|
phrase,
|
|
whatToSearch));
|
|
},
|
|
|
|
endSearch: function() {
|
|
this.resetMessageSlice();
|
|
this.searchMode = 'nonsearch';
|
|
this.freshMessagesSlice();
|
|
},
|
|
|
|
freshMessagesSlice: function() {
|
|
this.bindToSlice(this.model.api.viewFolderMessages(this.model.folder));
|
|
},
|
|
|
|
/**
|
|
* holds on to messagesSlice and binds some events to it.
|
|
* @param {Slice} messagesSlice the new messagesSlice.
|
|
*/
|
|
bindToSlice: function(messagesSlice) {
|
|
this.resetMessageSlice();
|
|
|
|
this.messagesSlice = messagesSlice;
|
|
this.sliceEvents.forEach(function(type) {
|
|
messagesSlice['on' + type] = makeListener(type, this);
|
|
}.bind(this));
|
|
},
|
|
|
|
onMessagesSplice: function(index, howMany, addedItems,
|
|
requested, moreExpected) {
|
|
// Avoid doing work if get called while in the process of
|
|
// shutting down.
|
|
if (!this.messagesSlice) {
|
|
return;
|
|
}
|
|
|
|
// If there was a messageSuid expected and at the top, then
|
|
// check to see if it was received. This is really just nice
|
|
// for when a new message notification comes in, as the atTop
|
|
// test is a bit fuzzy generally. Not all slices go to the top.
|
|
if (this.messagesSlice.atTop && this.expectingMessageSuid &&
|
|
this.messagesSlice.items && this.messagesSlice.items.length) {
|
|
this.checkExpectingMessageSuid(true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Choose a new currentMessage if we spilled the existing one.
|
|
* Otherwise, emit 'currentMessage' event to update stale listeners
|
|
* in case we spilled a sibling.
|
|
*
|
|
* @param {MailHeader} removedHeader header that got removed.
|
|
* @param {number} removedFromIndex index header was removed from.
|
|
*/
|
|
onMessagesSpliceRemove: function(removedHeader, removedFromIndex) {
|
|
if (this.currentMessage !== removedHeader) {
|
|
// Emit 'currentMessage' event in case we're spilling a sibling.
|
|
return this.setCurrentMessage(this.currentMessage);
|
|
}
|
|
|
|
var messages = this.messagesSlice.items;
|
|
if (messages.length === 0) {
|
|
// No more messages... sad!
|
|
return (this.currentMessage = null);
|
|
}
|
|
|
|
var index = Math.min(removedFromIndex, messages.length - 1);
|
|
var message = this.messagesSlice.items[index];
|
|
this.setCurrentMessage(message);
|
|
},
|
|
|
|
resetMessageSlice: function() {
|
|
if (this.messagesSlice) {
|
|
this.messagesSlice.die();
|
|
this.messagesSlice = null;
|
|
}
|
|
|
|
this.currentMessage = null;
|
|
},
|
|
|
|
die: function() {
|
|
this.model.removeListener('folder', this.onLatestFolder);
|
|
this.resetMessageSlice();
|
|
}
|
|
|
|
});
|
|
|
|
/*
|
|
* Override the .on method so that initialization and slice creation is
|
|
* delayed until there are listeners.
|
|
*/
|
|
var oldOn = HeaderCursor.prototype.on;
|
|
HeaderCursor.prototype.on = function() {
|
|
if (!this._inited) {
|
|
this.init();
|
|
}
|
|
return oldOn.apply(this, arguments);
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {MailHeader} header message header.
|
|
* @param {Object} siblings whether message has next and previous siblings.
|
|
*/
|
|
function CurrentMessage(header, siblings) {
|
|
this.header = header;
|
|
this.siblings = siblings;
|
|
}
|
|
|
|
CurrentMessage.prototype = {
|
|
/**
|
|
* @type {MailHeader}
|
|
*/
|
|
header: null,
|
|
|
|
/**
|
|
* Something like { hasPrevious: true, hasNext: false }.
|
|
* @type {Object}
|
|
*/
|
|
siblings: null
|
|
};
|
|
|
|
HeaderCursor.CurrentMessage = CurrentMessage;
|
|
|
|
return HeaderCursor;
|
|
});
|