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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
33
gaia/apps/email/js/cards/lst/default_vscroll_data.js
Executable file
33
gaia/apps/email/js/cards/lst/default_vscroll_data.js
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
// Default data used for the VScroll component, when data is not loaded yet for
|
||||
// display in the virtual scroll listing. The u2583 character is a solid bar
|
||||
// character, so a string of them gives a "redacted" look to a line. Just a
|
||||
// temporary placeholder until the real data comes in.
|
||||
define({
|
||||
'isPlaceholderData': true,
|
||||
'id': 'INVALID',
|
||||
'author': {
|
||||
'name': '\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583',
|
||||
'address': '',
|
||||
'contactId': null
|
||||
},
|
||||
'to': [
|
||||
{
|
||||
'name': ' ',
|
||||
'address': ' ',
|
||||
'contactId': null
|
||||
}
|
||||
],
|
||||
'cc': null,
|
||||
'bcc': null,
|
||||
'date': '0',
|
||||
'hasAttachments': false,
|
||||
'snippet': '\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583' +
|
||||
'\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583' +
|
||||
'\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583',
|
||||
'isRead': true,
|
||||
'isStarred': false,
|
||||
'sendStatus': {},
|
||||
'subject': '\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583' +
|
||||
'\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583' +
|
||||
'\u2583\u2583\u2583\u2583\u2583\u2583\u2583\u2583'
|
||||
});
|
||||
190
gaia/apps/email/js/cards/lst/edit_controller.js
Executable file
190
gaia/apps/email/js/cards/lst/edit_controller.js
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require, exports) {
|
||||
var cards = require('cards'),
|
||||
ConfirmDialog = require('confirm_dialog'),
|
||||
deleteConfirmMsgNode = require('tmpl!../msg/delete_confirm.html'),
|
||||
mozL10n = require('l10n!'),
|
||||
toaster = require('toaster');
|
||||
|
||||
return {
|
||||
createdCallback: function() {
|
||||
this.editMode = false;
|
||||
this.selectedMessages = null;
|
||||
this.editModeEnabled = false;
|
||||
},
|
||||
|
||||
setEditMode: function(editMode) {
|
||||
// Do not bother if edit mode is not enabled yet.
|
||||
if (!this.editModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setEditMode(editMode);
|
||||
},
|
||||
|
||||
// This function is called from setEditMode() after ensuring that
|
||||
// the backend is in a state where we can safely use edit mode.
|
||||
_setEditMode: function(editMode) {
|
||||
var i;
|
||||
|
||||
this.editMode = editMode;
|
||||
|
||||
if (editMode) {
|
||||
this.classList.add('show-edit');
|
||||
|
||||
this.selectedMessages = [];
|
||||
this.updateDomEditControls();
|
||||
}
|
||||
else {
|
||||
this.classList.remove('show-edit');
|
||||
|
||||
this.selectedMessages = null;
|
||||
}
|
||||
|
||||
// Reset checked mode for all message items.
|
||||
var msgNodes = this.msgVScroll.querySelectorAll('.msg-header-item');
|
||||
for (i = 0; i < msgNodes.length; i++) {
|
||||
this.updateDomMessageChecked(msgNodes[i], false);
|
||||
}
|
||||
|
||||
if (this.editModeChanged) {
|
||||
this.editModeChanged(editMode);
|
||||
}
|
||||
},
|
||||
|
||||
// Event handler wired up in HTML template
|
||||
setEditModeStart: function() {
|
||||
this.setEditMode(true);
|
||||
},
|
||||
|
||||
// Event handler wired up in HTML template
|
||||
editHeaderClose: function() {
|
||||
this.setEditMode(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the selection state of the node and therefore the stored
|
||||
* `selectedMessages` state based on the previous state of the selection.
|
||||
* The change in the total number of selections is then reflected in DOM
|
||||
* updates.
|
||||
* @param {Element} msgNode
|
||||
*/
|
||||
toggleSelection: function(msgNode) {
|
||||
var header = msgNode.message;
|
||||
var idx = this.selectedMessages.indexOf(header);
|
||||
if (idx !== -1) {
|
||||
this.selectedMessages.splice(idx, 1);
|
||||
} else {
|
||||
this.selectedMessages.push(header);
|
||||
}
|
||||
this.updateDomMessageChecked(msgNode, idx === -1);
|
||||
this.updateDomEditControls();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set or unset the select state on a node based on the edit mode.
|
||||
*/
|
||||
updateDomSelectState: function(msgNode, message) {
|
||||
if (this.editMode) {
|
||||
this.updateDomMessageChecked(msgNode,
|
||||
this.selectedMessages.indexOf(message) !== -1);
|
||||
} else {
|
||||
msgNode.removeAttribute('aria-selected');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the checked state for the message item in the list. It sets both
|
||||
* checkbox checked and aria-selected states.
|
||||
*/
|
||||
updateDomMessageChecked: function(msgNode, checked) {
|
||||
var checkbox = msgNode.querySelector('input[type=checkbox]');
|
||||
checkbox.checked = checked;
|
||||
msgNode.setAttribute('aria-selected', checked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the edit mode UI bits sensitive to a change in the set of selected
|
||||
* messages. This means the label that says how many messages are selected,
|
||||
* whether the buttons are enabled, which of the toggle-pairs are visible.
|
||||
*/
|
||||
updateDomEditControls: function() {
|
||||
this.editHeader.updateDomHeaderCount(this.selectedMessages.length);
|
||||
this.editToolbar.updateDomEditButtons(this.selectedMessages.length > 0);
|
||||
|
||||
// Enabling/disabling rules (not UX-signed-off): Our bias is that people
|
||||
// want to star messages and mark messages unread (since it they naturally
|
||||
// end up unread), so unless messages are all in this state, we assume
|
||||
// that is the desired action.
|
||||
var numStarred = 0, numRead = 0;
|
||||
for (var i = 0; i < this.selectedMessages.length; i++) {
|
||||
var msg = this.selectedMessages[i];
|
||||
if (msg.isStarred) {
|
||||
numStarred++;
|
||||
}
|
||||
if (msg.isRead) {
|
||||
numRead++;
|
||||
}
|
||||
}
|
||||
|
||||
// Unstar if everything is starred, otherwise star
|
||||
this.setAsStarred = !(numStarred && numStarred ===
|
||||
this.selectedMessages.length);
|
||||
this.editToolbar.updateDomStartButton(this.setAsStarred);
|
||||
|
||||
// Mark read if everything is unread, otherwise unread
|
||||
this.setAsRead = (!!this.selectedMessages.length && numRead === 0);
|
||||
this.editToolbar.updateDomReadButton(numRead > 0);
|
||||
},
|
||||
|
||||
onDeleteMessages: function() {
|
||||
if (this.selectedMessages.length === 0) {
|
||||
return this.setEditMode(false);
|
||||
}
|
||||
|
||||
var dialog = deleteConfirmMsgNode.cloneNode(true);
|
||||
var content = dialog.getElementsByTagName('p')[0];
|
||||
mozL10n.setAttributes(content, 'message-multiedit-delete-confirm',
|
||||
{ n: this.selectedMessages.length });
|
||||
ConfirmDialog.show(dialog,
|
||||
{ // Confirm
|
||||
id: 'msg-delete-ok',
|
||||
handler: function() {
|
||||
var op = this.model.api.deleteMessages(this.selectedMessages);
|
||||
toaster.toastOperation(op);
|
||||
this.setEditMode(false);
|
||||
}.bind(this)
|
||||
},
|
||||
{ // Cancel
|
||||
id: 'msg-delete-cancel',
|
||||
handler: null
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onStarMessages: function() {
|
||||
var op = this.model.api.markMessagesStarred(this.selectedMessages,
|
||||
this.setAsStarred);
|
||||
this.setEditMode(false);
|
||||
toaster.toastOperation(op);
|
||||
},
|
||||
|
||||
onMarkMessagesRead: function() {
|
||||
var op = this.model.api.markMessagesRead(this.selectedMessages,
|
||||
this.setAsRead);
|
||||
this.setEditMode(false);
|
||||
toaster.toastOperation(op);
|
||||
},
|
||||
|
||||
onMoveMessages: function() {
|
||||
cards.folderSelector(this.model, function(folder) {
|
||||
var op = this.model.api.moveMessages(this.selectedMessages, folder);
|
||||
toaster.toastOperation(op);
|
||||
this.setEditMode(false);
|
||||
}.bind(this), function(folder) {
|
||||
return folder.isValidMoveTarget;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
12
gaia/apps/email/js/cards/lst/edit_header.html
Executable file
12
gaia/apps/email/js/cards/lst/edit_header.html
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
<section role="region">
|
||||
<header>
|
||||
<a href="#"
|
||||
data-event="click:domEvt"
|
||||
data-domevt-name="editHeaderClose"
|
||||
role="button"
|
||||
data-l10n-id="close-button">
|
||||
<span class="icon icon-close"></span>
|
||||
</a>
|
||||
<h1 data-prop="headerNode"></h1>
|
||||
</header>
|
||||
</section>
|
||||
16
gaia/apps/email/js/cards/lst/edit_header.js
Executable file
16
gaia/apps/email/js/cards/lst/edit_header.js
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require, exports) {
|
||||
var mozL10n = require('l10n!');
|
||||
|
||||
return [
|
||||
require('../base')(require('template!./edit_header.html')),
|
||||
require('../mixins/dom_evt'),
|
||||
{
|
||||
updateDomHeaderCount: function(count) {
|
||||
mozL10n.setAttributes(this.headerNode, 'message-multiedit-header',
|
||||
{ n: count });
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
31
gaia/apps/email/js/cards/lst/edit_toolbar.html
Executable file
31
gaia/apps/email/js/cards/lst/edit_toolbar.html
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<!-- Toolbar for multi-edit state -->
|
||||
<ul class="bb-tablist" role="toolbar">
|
||||
<li role="presentation">
|
||||
<button data-prop="deleteBtn"
|
||||
data-event="click:domEvt"
|
||||
data-domevt-name="onDeleteMessages"
|
||||
class="icon msg-delete-btn"
|
||||
data-l10n-id="message-delete-button"></button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button data-prop="starBtn"
|
||||
data-event="click:domEvt"
|
||||
data-domevt-name="onStarMessages"
|
||||
class="icon msg-star-btn"
|
||||
data-l10n-id="message-star-button"></button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button data-prop="readBtn"
|
||||
data-event="click:domEvt"
|
||||
data-domevt-name="onMarkMessagesRead"
|
||||
class="icon msg-mark-read-btn"
|
||||
data-l10n-id="message-mark-read-button"></button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button data-prop="moveBtn"
|
||||
data-event="click:domEvt"
|
||||
data-domevt-name="onMoveMessages"
|
||||
class="icon msg-move-btn"
|
||||
data-l10n-id="message-move-button"></button>
|
||||
</li>
|
||||
</ul>
|
||||
47
gaia/apps/email/js/cards/lst/edit_toolbar.js
Executable file
47
gaia/apps/email/js/cards/lst/edit_toolbar.js
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require, exports) {
|
||||
var mozL10n = require('l10n!'),
|
||||
toolbarEditButtonNames = ['starBtn', 'readBtn', 'deleteBtn', 'moveBtn'];
|
||||
|
||||
return [
|
||||
require('../base')(require('template!./edit_toolbar.html')),
|
||||
require('../mixins/dom_evt'),
|
||||
{
|
||||
updateDomFolderType: function(folderType) {
|
||||
// You can't move messages in localdrafts or the outbox.
|
||||
this.moveBtn.classList.toggle('collapsed',
|
||||
folderType === 'localdrafts' ||
|
||||
folderType === 'outbox');
|
||||
// You can't flag or change the read status of messages in the outbox.
|
||||
this.starBtn.classList.toggle('collapsed',
|
||||
folderType === 'outbox');
|
||||
this.readBtn.classList.toggle('collapsed',
|
||||
folderType === 'outbox');
|
||||
},
|
||||
|
||||
// If true, show the star button, otherwise show unstar button.
|
||||
updateDomStartButton: function(isStarred) {
|
||||
mozL10n.setAttributes(this.starBtn,
|
||||
isStarred ? 'message-star-button' : 'message-unstar-button');
|
||||
},
|
||||
|
||||
// If true, some messages read, so show readBtn as marking unread.
|
||||
updateDomReadButton: function(hasUnread) {
|
||||
// Update mark read/unread button to show what action will be taken.
|
||||
this.readBtn.classList.toggle('unread', hasUnread);
|
||||
|
||||
mozL10n.setAttributes(this.readBtn, hasUnread ?
|
||||
'message-mark-unread-button' : 'message-mark-read-button');
|
||||
},
|
||||
|
||||
// Set how many messages to edit. If greater than zero, enable buttons.
|
||||
updateDomEditButtons: function(hasMessages) {
|
||||
// Update disabled state based on if there are selected messages
|
||||
toolbarEditButtonNames.forEach(function(key) {
|
||||
this[key].disabled = !hasMessages;
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
158
gaia/apps/email/js/cards/lst/msg_click.js
Executable file
158
gaia/apps/email/js/cards/lst/msg_click.js
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require, exports) {
|
||||
|
||||
var cards = require('cards'),
|
||||
ConfirmDialog = require('confirm_dialog'),
|
||||
largeMsgConfirmMsgNode = require('tmpl!../msg/large_message_confirm.html');
|
||||
|
||||
/**
|
||||
* Show a warning that the given message is large.
|
||||
* Callback is called with cb(true|false) to continue.
|
||||
*/
|
||||
function showLargeMessageWarning(size, cb) {
|
||||
var dialog = largeMsgConfirmMsgNode.cloneNode(true);
|
||||
// TODO: If UX designers want the size included in the warning
|
||||
// message, add it here.
|
||||
ConfirmDialog.show(dialog,
|
||||
{ // Confirm
|
||||
id: 'msg-large-message-ok',
|
||||
handler: function() { cb(true); }
|
||||
},
|
||||
{ // Cancel
|
||||
id: 'msg-large-message-cancel',
|
||||
handler: function() { cb(false); }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin for lst cards that want tapping on a message to go to a message
|
||||
* reader. Check all the `this` references below to see implicit dependencies
|
||||
* on instance state.
|
||||
* A longer range TODO is to remove or make explicit the dependencies.
|
||||
*/
|
||||
return {
|
||||
onClickMessage: function(event) {
|
||||
var messageNode = event.detail;
|
||||
|
||||
// You cannot open a message if this is the outbox and it is syncing.
|
||||
if (this.curFolder &&
|
||||
this.curFolder.type === 'outbox' && this.outboxSyncInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
var header = messageNode.message;
|
||||
|
||||
// Skip nodes that are default/placeholder ones.
|
||||
if (header && header.isPlaceholderData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If in edit mode, the clicks on message nodes are about changing the
|
||||
// selection for bulk edit actions.
|
||||
if (this.editMode) {
|
||||
this.toggleSelection(messageNode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.curFolder && this.curFolder.type === 'localdrafts') {
|
||||
var composer = header.editAsDraft(() => {
|
||||
cards.pushCard('compose', 'animate', {
|
||||
model: this.model,
|
||||
composer
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// When tapping a message in the outbox, don't open the message;
|
||||
// instead, move it to localdrafts and edit the message as a
|
||||
// draft.
|
||||
if (this.curFolder && this.curFolder.type === 'outbox') {
|
||||
// If the message is currently being sent, abort.
|
||||
if (header.sendStatus.state === 'sending') {
|
||||
return;
|
||||
}
|
||||
var draftsFolder =
|
||||
this.model.foldersSlice.getFirstFolderWithType('localdrafts');
|
||||
|
||||
console.log('outbox: Moving message to localdrafts.');
|
||||
this.model.api.moveMessages([header], draftsFolder, (moveMap) => {
|
||||
header.id = moveMap[header.id];
|
||||
console.log('outbox: Editing message in localdrafts.');
|
||||
var composer = header.editAsDraft(() => {
|
||||
cards.pushCard('compose', 'animate', {
|
||||
model: this.model,
|
||||
composer
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var model = this.model,
|
||||
headerCursor = this.headerCursor;
|
||||
|
||||
function pushMessageCard() {
|
||||
cards.pushCard(
|
||||
'message_reader', 'animate',
|
||||
{
|
||||
model,
|
||||
headerCursor,
|
||||
|
||||
// The header here may be undefined here, since the click
|
||||
// could be on a cached HTML node before the back end has
|
||||
// started up. It is OK if header is not available as the
|
||||
// message_reader knows how to wait for the back end to
|
||||
// start up to get the header value later.
|
||||
header,
|
||||
// Use the property on the HTML, since the click could be
|
||||
// from a cached HTML node and the real data object may not
|
||||
// be available yet.
|
||||
messageSuid: messageNode.dataset.id
|
||||
});
|
||||
}
|
||||
|
||||
if (header) {
|
||||
this.headerCursor.setCurrentMessage(header);
|
||||
} else if (messageNode.dataset.id) {
|
||||
// a case where header was not set yet, like clicking on a
|
||||
// html cached node, or virtual scroll item that is no
|
||||
// longer backed by a header.
|
||||
this.headerCursor.setCurrentMessageBySuid(messageNode.dataset.id);
|
||||
} else {
|
||||
// Not an interesting click, bail
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is really big, warn them before they open it.
|
||||
// Ideally we'd only warn if you're on a cell connection
|
||||
// (metered), but as of now `navigator.connection.metered` isn't
|
||||
// implemented.
|
||||
|
||||
// This number is somewhat arbitrary, based on a guess that most
|
||||
// plain-text/HTML messages will be smaller than this. If this
|
||||
// value is too small, users get warned unnecessarily. Too large
|
||||
// and they download a lot of data without knowing. Since we
|
||||
// currently assume that all network connections are metered,
|
||||
// they'll always see this if they get a large message...
|
||||
var LARGE_MESSAGE_SIZE = 1 * 1024 * 1024;
|
||||
|
||||
// watch out, header might be undefined here (that's okay, see above)
|
||||
if (header && header.bytesToDownloadForBodyDisplay > LARGE_MESSAGE_SIZE) {
|
||||
showLargeMessageWarning(
|
||||
header.bytesToDownloadForBodyDisplay, function(result) {
|
||||
if (result) {
|
||||
pushMessageCard();
|
||||
} else {
|
||||
// abort
|
||||
}
|
||||
});
|
||||
} else {
|
||||
pushMessageCard();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
22
gaia/apps/email/js/cards/lst/msg_vscroll.html
Executable file
22
gaia/apps/email/js/cards/lst/msg_vscroll.html
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
<div data-prop="vScrollContainer" class="msg-vscroll-container"></div>
|
||||
<!-- maintain vertical space for the syncing/sync more div's
|
||||
regardless of their displayed status so we don't scroll them
|
||||
out of the way -->
|
||||
<div class="msg-messages-sync-container">
|
||||
<p data-prop="syncingNode" class="msg-messages-syncing collapsed"
|
||||
role="progressbar" data-l10n-id="messages-syncing-progressbar">
|
||||
<span data-l10n-id="messages-syncing"></span>
|
||||
</p>
|
||||
<p data-prop="syncMoreNode"
|
||||
data-event="click:onGetMoreMessages"
|
||||
class="msg-messages-sync-more collapsed"
|
||||
role="button">
|
||||
<span data-l10n-id="messages-load-more"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div data-prop="messageEmptyContainer"
|
||||
class="msg-list-empty-container collapsed">
|
||||
<p data-prop="messageEmptyText"
|
||||
class="msg-list-empty-message-text"
|
||||
data-l10n-id="messages-folder-empty"></p>
|
||||
</div>
|
||||
511
gaia/apps/email/js/cards/lst/msg_vscroll.js
Executable file
511
gaia/apps/email/js/cards/lst/msg_vscroll.js
Executable file
|
|
@ -0,0 +1,511 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require, exports) {
|
||||
|
||||
var cards = require('cards'),
|
||||
containerListen = require('container_listen'),
|
||||
mozL10n = require('l10n!'),
|
||||
msgHeaderItemNode = require('tmpl!../msg/header_item.html'),
|
||||
toaster = require('toaster'),
|
||||
VScroll = require('vscroll');
|
||||
|
||||
var sliceEvents = ['splice', 'change', 'status', 'complete'];
|
||||
|
||||
/**
|
||||
* Minimum number of items there must be in the message slice
|
||||
* for us to attempt to limit the selection of snippets to fetch.
|
||||
*/
|
||||
var MINIMUM_ITEMS_FOR_SCROLL_CALC = 10;
|
||||
|
||||
/**
|
||||
* Maximum amount of time between issuing snippet requests.
|
||||
*/
|
||||
var MAXIMUM_MS_BETWEEN_SNIPPET_REQUEST = 6000;
|
||||
|
||||
/**
|
||||
* Fetch up to 4kb while scrolling
|
||||
*/
|
||||
var MAXIMUM_BYTES_PER_MESSAGE_DURING_SCROLL = 4 * 1024;
|
||||
|
||||
|
||||
/**
|
||||
* Component that shows a message-based vscroll. Assumes the following are set
|
||||
* on the this component before vscroll is active:
|
||||
* - headerCursor
|
||||
*/
|
||||
return [
|
||||
require('../base')(require('template!./msg_vscroll.html')),
|
||||
{
|
||||
createdCallback: function() {
|
||||
this.setAttribute('role', 'listbox');
|
||||
this.setAttribute('aria-multiselectable', 'true');
|
||||
|
||||
mozL10n.setAttributes(this.messageEmptyText, this.dataset.emptyL10nId);
|
||||
|
||||
containerListen(this.vScrollContainer, 'click',
|
||||
this.onClickMessage.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Call this from the createdCallback of the module that wants to display
|
||||
* a message-based vscroll.
|
||||
*/
|
||||
init: function(scrollContainer, bindData, defaultVScrollData) {
|
||||
this.scrollContainer = scrollContainer;
|
||||
|
||||
// Set up the list data source for VScroll
|
||||
var listFunc = (function(index) {
|
||||
return this.headerCursor.messagesSlice.items[index];
|
||||
}.bind(this));
|
||||
|
||||
listFunc.size = (function() {
|
||||
// This method could get called during VScroll updates triggered
|
||||
// by messages_splice. However at that point, the headerCount may
|
||||
// not be correct, like when fetching more messages from the
|
||||
// server. So approximate by using the size of slice.items.
|
||||
var slice = this.headerCursor.messagesSlice;
|
||||
// coerce headerCount to 0 if it was undefined to avoid a NaN
|
||||
return Math.max(slice.headerCount || 0, slice.items.length);
|
||||
}.bind(this));
|
||||
this.listFunc = listFunc;
|
||||
|
||||
// We need to wait for the slice to complete before we can issue any
|
||||
// sensible growth requests.
|
||||
this.waitingOnChunk = true;
|
||||
this.desiredHighAbsoluteIndex = 0;
|
||||
this._needVScrollData = false;
|
||||
this.vScroll = new VScroll(
|
||||
this.vScrollContainer,
|
||||
this.scrollContainer,
|
||||
msgHeaderItemNode,
|
||||
defaultVScrollData
|
||||
);
|
||||
|
||||
// Called by VScroll wants to bind some data to a node it wants to
|
||||
// display in the DOM.
|
||||
this.vScroll.bindData = bindData;
|
||||
|
||||
// Called by VScroll when it detects it will need more data in the near
|
||||
// future. VScroll does not know if it already asked for this
|
||||
// information, so this function needs to be sure it actually needs to
|
||||
// ask for more from the back end.
|
||||
this.vScroll.prepareData = (function(highAbsoluteIndex) {
|
||||
var items = this.headerCursor.messagesSlice &&
|
||||
this.headerCursor.messagesSlice.items,
|
||||
headerCount = this.headerCursor.messagesSlice.headerCount;
|
||||
|
||||
if (!items || !headerCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure total amount stays within possible range.
|
||||
if (highAbsoluteIndex > headerCount - 1) {
|
||||
highAbsoluteIndex = headerCount - 1;
|
||||
}
|
||||
|
||||
// We're already prepared if the slice is already that big.
|
||||
if (highAbsoluteIndex < items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadNextChunk(highAbsoluteIndex);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setHeaderCursor: function(headerCursor) {
|
||||
if (this.headerCursor) {
|
||||
throw new Error('headerCursor already set');
|
||||
}
|
||||
|
||||
this.headerCursor = headerCursor;
|
||||
|
||||
sliceEvents.forEach(function(type) {
|
||||
var name = 'messages_' + type;
|
||||
// Bind to local, but replace property name here so that removing the
|
||||
// listeners are easier in die().
|
||||
this[name] = this[name].bind(this);
|
||||
headerCursor.on(name, this[name]);
|
||||
}.bind(this));
|
||||
|
||||
this.onCurrentMessage = this.onCurrentMessage.bind(this);
|
||||
headerCursor.on('currentMessage', this.onCurrentMessage);
|
||||
|
||||
// Find the containing element that is a card, for use when asking if that
|
||||
// card is visible and extra work should be done.
|
||||
var parent = this;
|
||||
while((parent = parent.parentNode)) {
|
||||
if (parent.classList.contains('card')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.cardParent = parent;
|
||||
|
||||
this._onVScrollStopped = this._onVScrollStopped.bind(this);
|
||||
this.vScroll.on('scrollStopped', this._onVScrollStopped);
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to remove the cached HTML from a cache restore of HTML. Do not use
|
||||
* this for clearing the display of messages when the list of messages
|
||||
* change, instead use methods on the vScroll object.
|
||||
*/
|
||||
removeMessagesHtml: function() {
|
||||
this.vScrollContainer.innerHTML = '';
|
||||
},
|
||||
|
||||
onClickMessage: function(node, event) {
|
||||
this.emitDomEvent('messageClick', node);
|
||||
},
|
||||
|
||||
/**
|
||||
* Waits for scrolling to stop before fetching snippets.
|
||||
*/
|
||||
_onVScrollStopped: function() {
|
||||
// Give any pending requests in the slice priority.
|
||||
if (!this.headerCursor.messagesSlice ||
|
||||
this.headerCursor.messagesSlice.pendingRequestCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not bother fetching snippets if this card is not in view.
|
||||
// The card could still have a scroll event triggered though
|
||||
// by the next/previous work done in message_reader.
|
||||
if (cards.isVisible(this.cardParent) && !this._hasSnippetRequest()) {
|
||||
this._requestSnippets();
|
||||
}
|
||||
},
|
||||
|
||||
onGetMoreMessages: function() {
|
||||
if (!this.headerCursor.messagesSlice) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For accessibility purposes, focus on the first newly loaded item in the
|
||||
// messages list. This will ensure that screen reader's cursor position
|
||||
// will get updated to the right place.
|
||||
this.vScroll.once('recalculated', function(calledFromTop, refIndex) {
|
||||
// refIndex is the index of the first new message item.
|
||||
this.vScrollContainer.querySelector(
|
||||
'[data-index="' + refIndex + '"]').focus();
|
||||
}.bind(this));
|
||||
|
||||
this.headerCursor.messagesSlice.requestGrowth(1, true);
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
return this.headerCursor.messagesSlice.items.length === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide buttons that are not appropriate if we have no messages and display
|
||||
* the appropriate l10n string in the message list proper.
|
||||
*/
|
||||
showEmptyLayout: function() {
|
||||
this.messageEmptyContainer.classList.remove('collapsed');
|
||||
this.emit('emptyLayoutShown');
|
||||
},
|
||||
/**
|
||||
* Show buttons we hid in `showEmptyLayout` and hide the "empty folder"
|
||||
* message.
|
||||
*/
|
||||
hideEmptyLayout: function() {
|
||||
this.messageEmptyContainer.classList.add('collapsed');
|
||||
this.emit('emptyLayoutHidden');
|
||||
},
|
||||
|
||||
// The funny name because it is auto-bound as a listener for
|
||||
// messagesSlice events in headerCursor using a naming convention.
|
||||
messages_status: function(newStatus) {
|
||||
var syncInProgress = true;
|
||||
if (newStatus === 'synchronizing' ||
|
||||
newStatus === 'syncblocked') {
|
||||
this.syncingNode.classList.remove('collapsed');
|
||||
this.syncMoreNode.classList.add('collapsed');
|
||||
this.hideEmptyLayout();
|
||||
} else if (newStatus === 'syncfailed' ||
|
||||
newStatus === 'synced') {
|
||||
syncInProgress = false;
|
||||
if (newStatus === 'syncfailed') {
|
||||
// If there was a problem talking to the server, notify the user and
|
||||
// provide a means to attempt to talk to the server again. We have
|
||||
// made onRefresh pretty clever, so it can do all the legwork on
|
||||
// accomplishing this goal.
|
||||
toaster.toast({
|
||||
text: mozL10n.get('toaster-retryable-syncfailed')
|
||||
});
|
||||
}
|
||||
this.syncingNode.classList.add('collapsed');
|
||||
}
|
||||
this.emit('syncInProgress', syncInProgress);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number=} newEmailCount Optional number of new messages.
|
||||
* The funny name because it is auto-bound as a listener for
|
||||
* messagesSlice events in headerCursor using a naming convention.
|
||||
*/
|
||||
messages_complete: function(newEmailCount) {
|
||||
var headerCursor = this.headerCursor;
|
||||
|
||||
console.log('message_list complete:',
|
||||
headerCursor.messagesSlice.items.length, 'items of',
|
||||
headerCursor.messagesSlice.headerCount,
|
||||
'alleged known headers. canGrow:',
|
||||
headerCursor.messagesSlice.userCanGrowDownwards);
|
||||
|
||||
// Show "load more", but only if the slice can grow and if there is a
|
||||
// non-zero headerCount. If zero headerCount, it likely means the folder
|
||||
// has never been synchronized, and this display was an offline display,
|
||||
// so it is hard to know if messages can be synchronized. In this case,
|
||||
// canGrow is not enough of an indicator, because as far as the back end is
|
||||
// concerned, it could grow, it just has no way to check for sure yet. So
|
||||
// hide the "load more", the user can use the refresh icon once online to
|
||||
// load messages.
|
||||
if (headerCursor.messagesSlice.userCanGrowDownwards &&
|
||||
headerCursor.messagesSlice.headerCount) {
|
||||
this.syncMoreNode.classList.remove('collapsed');
|
||||
} else {
|
||||
this.syncMoreNode.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Show empty layout, unless this is a slice with fake data that
|
||||
// will get changed soon.
|
||||
if (headerCursor.messagesSlice.items.length === 0) {
|
||||
this.showEmptyLayout();
|
||||
}
|
||||
|
||||
this.waitingOnChunk = false;
|
||||
// Load next chunk if one is pending
|
||||
if (this.desiredHighAbsoluteIndex) {
|
||||
this.loadNextChunk(this.desiredHighAbsoluteIndex);
|
||||
this.desiredHighAbsoluteIndex = 0;
|
||||
}
|
||||
|
||||
// It's possible for a synchronization to result in a change to
|
||||
// headerCount without resulting in a splice. This is very likely
|
||||
// to happen with a search filter when it was lying about another
|
||||
// messages existing, but it's also possible to happen in
|
||||
// synchronizations.
|
||||
//
|
||||
// XXX Our total correctness currently depends on headerCount only
|
||||
// changing as a result of a synchronization triggered by this slice.
|
||||
// This does not hold up when confronted with periodic background sync; we
|
||||
// need to finish cleanup up the headerCount change notification stuff.
|
||||
//
|
||||
// (However, this is acceptable glitchage right now. We just need to make
|
||||
// sure it doesn't happen for searches since it's so blatant.)
|
||||
//
|
||||
// So, anyways, use updateDataBind() to cause VScroll to double-check that
|
||||
// our list size didn't change from what it thought it was. (It renders
|
||||
// coordinate-space predictively based on our headerCount, but we
|
||||
// currently only provide strong correctness guarantees for actually
|
||||
// reported `items`, so we must do this.) If our list size is the same,
|
||||
// this call is effectively a no-op.
|
||||
this.vScroll.updateDataBind(0, [], 0);
|
||||
|
||||
this.emit('messagesComplete', newEmailCount);
|
||||
},
|
||||
|
||||
// The funny name because it is auto-bound as a listener for
|
||||
// messagesSlice events in headerCursor using a naming convention.
|
||||
messages_splice: function(index, howMany, addedItems,
|
||||
requested, moreExpected) {
|
||||
var headerCursor = this.headerCursor;
|
||||
|
||||
// If no work to do, just skip it.
|
||||
if (index === 0 && howMany === 0 && !addedItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('messagesSpliceStart', index, howMany, addedItems,
|
||||
requested, moreExpected);
|
||||
|
||||
if (this._needVScrollData) {
|
||||
this.vScroll.setData(this.listFunc);
|
||||
this._needVScrollData = false;
|
||||
}
|
||||
|
||||
this.vScroll.updateDataBind(index, addedItems, howMany);
|
||||
|
||||
// Remove the no message text while new messages added:
|
||||
if (addedItems.length > 0) {
|
||||
this.hideEmptyLayout();
|
||||
}
|
||||
|
||||
// If the end result is no more messages, then show empty layout.
|
||||
// This is needed mostly because local drafts do not trigger
|
||||
// a messages_complete callback when removing the last draft
|
||||
// from the compose triggered in that view. The scrollStopped
|
||||
// is used to avoid a flash where the old message is briefly visible
|
||||
// before cleared, and having the empty layout overlay it.
|
||||
// Using the slice's headerCount because it is updated before splice
|
||||
// listeners are notified, so should be accurate.
|
||||
if (!headerCursor.messagesSlice.headerCount) {
|
||||
this.vScroll.once('scrollStopped', function() {
|
||||
// Confirm there are still no messages. Since this callback happens
|
||||
// async, some items could have appeared since first issuing the
|
||||
// request to show empty.
|
||||
if (!headerCursor.messagesSlice.headerCount) {
|
||||
this.showEmptyLayout();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.emit('messagesSpliceEnd', index, howMany, addedItems,
|
||||
requested, moreExpected);
|
||||
},
|
||||
|
||||
// The funny name because it is auto-bound as a listener for
|
||||
// messagesSlice events in headerCursor using a naming convention.
|
||||
messages_change: function(message, index) {
|
||||
this.emit('messagesChange', message, index);
|
||||
},
|
||||
|
||||
/**
|
||||
* Request data through desiredHighAbsoluteIndex if we don't have it
|
||||
* already and we think it exists. If we already have an outstanding
|
||||
* request we will save off this most recent request to process once
|
||||
* the current request completes. Any previously queued request will
|
||||
* be forgotten regardless of how it compares to the newly queued
|
||||
* request.
|
||||
*
|
||||
* @param {Number} desiredHighAbsoluteIndex
|
||||
*/
|
||||
loadNextChunk: function(desiredHighAbsoluteIndex) {
|
||||
// The recalculate logic will trigger a call to prepareData, so
|
||||
// it's okay for us to bail. It's advisable for us to bail
|
||||
// because any calls to prepareData will be based on outdated
|
||||
// index information.
|
||||
if (this.vScroll.waitingForRecalculate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.waitingOnChunk) {
|
||||
this.desiredHighAbsoluteIndex = desiredHighAbsoluteIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not bother asking for more than exists
|
||||
var headerCursor = this.headerCursor;
|
||||
if (desiredHighAbsoluteIndex >= headerCursor.messagesSlice.headerCount) {
|
||||
desiredHighAbsoluteIndex = headerCursor.messagesSlice.headerCount - 1;
|
||||
}
|
||||
|
||||
// Do not bother asking for more than what is already
|
||||
// fetched
|
||||
var items = headerCursor.messagesSlice.items;
|
||||
var curHighAbsoluteIndex = items.length - 1;
|
||||
var amount = desiredHighAbsoluteIndex - curHighAbsoluteIndex;
|
||||
if (amount > 0) {
|
||||
// IMPORTANT NOTE!
|
||||
// 1 is unfortunately a special value right now for historical reasons
|
||||
// that the other side interprets as a request to grow downward with the
|
||||
// default growth size. XXX change backend and its tests...
|
||||
console.log('message_list loadNextChunk growing', amount,
|
||||
(amount === 1 ? '(will get boosted to 15!) to' : 'to'),
|
||||
(desiredHighAbsoluteIndex + 1), 'items out of',
|
||||
headerCursor.messagesSlice.headerCount, 'alleged known');
|
||||
headerCursor.messagesSlice.requestGrowth(
|
||||
amount,
|
||||
// the user is not requesting us to go synchronize new messages
|
||||
false);
|
||||
this.waitingOnChunk = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll to make sure that the current message is in our visible window.
|
||||
*
|
||||
* @param {header_cursor.CurrentMessage} currentMessage representation of
|
||||
* the email we're currently reading.
|
||||
* @param {Number} index the index of the message in the messagesSlice
|
||||
*/
|
||||
onCurrentMessage: function(currentMessage, index) {
|
||||
if (!currentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
var visibleIndices = this.vScroll.getVisibleIndexRange();
|
||||
if (visibleIndices &&
|
||||
(index < visibleIndices[0] || index > visibleIndices[1])) {
|
||||
this.vScroll.jumpToIndex(index);
|
||||
}
|
||||
},
|
||||
|
||||
_hasSnippetRequest: function() {
|
||||
var max = MAXIMUM_MS_BETWEEN_SNIPPET_REQUEST;
|
||||
var now = Date.now();
|
||||
|
||||
// if we before the maximum time to wait between requests...
|
||||
var beforeTimeout =
|
||||
(this._lastSnippetRequest + max) > now;
|
||||
|
||||
// there is an important case where the backend may be slow OR have some
|
||||
// fatal error which would prevent us from ever requesting an new set of
|
||||
// snippets because we wait until the last batch finishes. To prevent that
|
||||
// from ever happening we maintain the request start time and if more then
|
||||
// MAXIMUM_MS_BETWEEN_SNIPPET_REQUEST passes we issue a new request.
|
||||
if (this._snippetRequestPending && beforeTimeout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_pendingSnippetRequest: function() {
|
||||
this._snippetRequestPending = true;
|
||||
this._lastSnippetRequest = Date.now();
|
||||
},
|
||||
|
||||
_clearSnippetRequest: function() {
|
||||
this._snippetRequestPending = false;
|
||||
},
|
||||
|
||||
_requestSnippets: function() {
|
||||
var headerCursor = this.headerCursor;
|
||||
var items = headerCursor.messagesSlice.items;
|
||||
var len = items.length;
|
||||
|
||||
if (!len) {
|
||||
return;
|
||||
}
|
||||
|
||||
var clearSnippets = this._clearSnippetRequest.bind(this);
|
||||
var options = {
|
||||
// this is per message
|
||||
maximumBytesToFetch: MAXIMUM_BYTES_PER_MESSAGE_DURING_SCROLL
|
||||
};
|
||||
|
||||
if (len < MINIMUM_ITEMS_FOR_SCROLL_CALC) {
|
||||
this._pendingSnippetRequest();
|
||||
headerCursor.messagesSlice.maybeRequestBodies(0,
|
||||
MINIMUM_ITEMS_FOR_SCROLL_CALC - 1, options, clearSnippets);
|
||||
return;
|
||||
}
|
||||
|
||||
var visibleIndices = this.vScroll.getVisibleIndexRange();
|
||||
|
||||
if (visibleIndices) {
|
||||
this._pendingSnippetRequest();
|
||||
headerCursor.messagesSlice.maybeRequestBodies(
|
||||
visibleIndices[0],
|
||||
visibleIndices[1],
|
||||
options,
|
||||
clearSnippets
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
die: function() {
|
||||
sliceEvents.forEach(function(type) {
|
||||
var name = 'messages_' + type;
|
||||
this.headerCursor.removeListener(name, this[name]);
|
||||
}.bind(this));
|
||||
|
||||
this.headerCursor.removeListener('currentMessage', this.onCurrentMessage);
|
||||
this.vScroll.destroy();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
});
|
||||
8
gaia/apps/email/js/cards/lst/peep_dom.js
Executable file
8
gaia/apps/email/js/cards/lst/peep_dom.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
'use strict';
|
||||
define(function() {
|
||||
return {
|
||||
update: function(peep) {
|
||||
peep.element.textContent = peep.name || peep.address;
|
||||
}
|
||||
};
|
||||
});
|
||||
Loading…
Reference in a new issue