VulpesOS/gaia/apps/email/js/header_cursor.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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;
});