-
-
Save jrburke/85a05a65314b03e07768 to your computer and use it in GitHub Desktop.
Gaia diff for the app/email folder between: b161f42c5647b37e35ba5a20f394ba40bf674d9c
1aed138dfef6279a00475f51ce0c491f32799ab6 Related to https://bugzilla.mozilla.org/show_bug.cgi?id=871826
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/apps/email/index.html b/apps/email/index.html | |
index 550ed1f..65ba063 100755 | |
--- a/apps/email/index.html | |
+++ b/apps/email/index.html | |
@@ -10,6 +10,7 @@ | |
<link rel="stylesheet" type="text/css" href="style/mail.css"> | |
<link rel="stylesheet" type="text/css" href="style/message-cards.css"> | |
<link rel="stylesheet" type="text/css" href="style/folder-cards.css"> | |
+ <link rel="stylesheet" type="text/css" href="style/marquee.css"> | |
<!-- Include shared building blocks --> | |
<link rel="stylesheet" type="text/css" href="shared/style/headers.css"/> | |
<link rel="stylesheet" type="text/css" href="shared/style/switches.css"/> | |
@@ -32,8 +33,10 @@ | |
<script type="text/javascript" defer src="js/input_areas.js"></script> | |
<script type="text/javascript" defer src="js/console-hook.js"></script> | |
<script type="text/javascript" defer src="js/mail-common.js"></script> | |
+ <script type="text/javascript" defer src="js/message_list_topbar.js"></script> | |
<script type="text/javascript" defer src="js/message-cards.js"></script> | |
<script type="text/javascript" defer src="js/folder-cards.js"></script> | |
+ <script type="text/javascript" defer src="js/marquee.js"></script> | |
<!-- This one is an optimized vendor library containing our mail backend. --> | |
<!-- START BACKEND INJECT - do not modify --> | |
<script type="text/javascript" defer src="js/ext/mailapi/main-frame-setup.js"></script> | |
@@ -219,6 +222,8 @@ | |
<div class="bottom-toolbar-spacer"></div> | |
</div> | |
</div> | |
+ <!-- New email notification bar --> | |
+ <div class="msg-list-topbar collapsed"></div> | |
<!-- Conveys background send, plus undo-able recent actions --> | |
<div class="msg-activity-infobar hidden"> | |
</div> | |
@@ -779,13 +784,8 @@ | |
<span data-l10n-id="settings-default-account" class="list-text"> | |
DefaulT AccounT</span> | |
<em class="aside end"> | |
- <label> | |
- <!-- When the input is focused, a resize event will be fired | |
- and will trigger _showCard() in mail-common.js | |
- the UI state will become wrong after _showCard() is called | |
- so disable the input first to prevent the wrong state | |
- --> | |
- <input disabled type="checkbox" checked="checked"> | |
+ <label class="tng-default-label"> | |
+ <input class="tng-default-input" type="checkbox"> | |
<span></span> | |
</label> | |
</em> | |
@@ -1005,6 +1005,15 @@ | |
</button> | |
</menu> | |
</form> | |
+ <form role="dialog" class="msg-attachment-disabled-confirm" data-type="confirm"> | |
+ <section> | |
+ <p><span data-l10n-id="message-send-attachment-disabled-confirm"></span></p> | |
+ </section> | |
+ <menu> | |
+ <button id="msg-attachment-disabled-cancel" data-l10n-id="message-multiedit-cancel">CanceL</button> | |
+ <button id="msg-attachment-disabled-ok" data-l10n-id="dialog-button-ok">OK</button> | |
+ </menu> | |
+ </form> | |
<form role="dialog" class="msg-delete-confirm" data-type="confirm"> | |
<section> | |
<h1 data-l10n-id="confirm-dialog-title">ConfirmatioN</h1> | |
@@ -1025,6 +1034,15 @@ | |
<button id="msg-browse-ok" class="recommend" data-l10n-id="dialog-button-ok">OK</button> | |
</menu> | |
</form> | |
+ <form role="dialog" class="msg-attach-confirm" data-type="confirm"> | |
+ <section> | |
+ <h1></h1> | |
+ <p></p> | |
+ </section> | |
+ <menu> | |
+ <button id="msg-attach-ok" class="full" data-l10n-id="dialog-button-ok">OK</button> | |
+ </menu> | |
+ </form> | |
</div> | |
<!-- Widget nodes for the compose cards; styles use the cmp prefix --> | |
<div id="templ-cmp"> | |
diff --git a/apps/email/js/compose-cards.js b/apps/email/js/compose-cards.js | |
index 641931d..65d78ec 100644 | |
--- a/apps/email/js/compose-cards.js | |
+++ b/apps/email/js/compose-cards.js | |
@@ -5,6 +5,11 @@ | |
**/ | |
/** | |
+ * Max composer attachment size is defined as 5120000 bytes. | |
+ */ | |
+var MAX_ATTACHMENT_SIZE = 5120000; | |
+ | |
+/** | |
* To make it easier to focus input boxes, we have clicks on their owning | |
* container cause a focus event to occur on the input. This method helps us | |
* also position the cursor based on the location of the click so the cursor | |
@@ -359,8 +364,13 @@ ComposeCard.prototype = { | |
if (target.classList.contains('cmp-peep-bubble')) { | |
var contents = cmpNodes['contact-menu'].cloneNode(true); | |
var email = target.querySelector('.cmp-peep-address').textContent; | |
- contents.getElementsByTagName('header')[0].textContent = email; | |
+ var headerNode = contents.getElementsByTagName('header')[0]; | |
+ // Setup the marquee structure | |
+ Marquee.setup(email, headerNode); | |
+ // Activate marquee once the contents DOM are added to document | |
document.body.appendChild(contents); | |
+ Marquee.activate('alternate', 'ease'); | |
+ | |
var formSubmit = (function(evt) { | |
document.body.removeChild(contents); | |
switch (evt.explicitOriginalTarget.className) { | |
@@ -411,8 +421,41 @@ ComposeCard.prototype = { | |
attTemplate.getElementsByClassName('cmp-attachment-filename')[0], | |
filesizeTemplate = | |
attTemplate.getElementsByClassName('cmp-attachment-filesize')[0]; | |
+ var totalSize = 0; | |
for (var i = 0; i < this.composer.attachments.length; i++) { | |
var attachment = this.composer.attachments[i]; | |
+ //check for attachment max size | |
+ if ((totalSize + attachment.blob.size) > MAX_ATTACHMENT_SIZE) { | |
+ | |
+ /*Remove all the remaining attachments from composer*/ | |
+ while (this.composer.attachments.length > i) { | |
+ this.composer.removeAttachment(this.composer.attachments[i]); | |
+ } | |
+ var dialog = msgNodes['attach-confirm'].cloneNode(true); | |
+ var title = dialog.getElementsByTagName('h1')[0]; | |
+ var content = dialog.getElementsByTagName('p')[0]; | |
+ | |
+ if (this.composer.attachments.length > 0) { | |
+ title.textContent = mozL10n.get('composer-attachments-large'); | |
+ content.textContent = | |
+ mozL10n.get('compose-attchments-size-exceeded'); | |
+ } else { | |
+ title.textContent = mozL10n.get('composer-attachment-large'); | |
+ content.textContent = | |
+ mozL10n.get('compose-attchment-size-exceeded'); | |
+ } | |
+ ConfirmDialog.show(dialog, | |
+ { | |
+ // ok | |
+ id: 'msg-attach-ok', | |
+ handler: function() { | |
+ this.updateAttachmentsSize(); | |
+ }.bind(this) | |
+ } | |
+ ); | |
+ return; | |
+ } | |
+ totalSize = totalSize + attachment.blob.size; | |
filenameTemplate.textContent = attachment.name; | |
filesizeTemplate.textContent = prettyFileSize(attachment.blob.size); | |
var attachmentNode = attTemplate.cloneNode(true); | |
diff --git a/apps/email/js/contacts.js b/apps/email/js/contacts.js | |
deleted file mode 100644 | |
index 7a0c1a7..0000000 | |
--- a/apps/email/js/contacts.js | |
+++ /dev/null | |
@@ -1,28 +0,0 @@ | |
-/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / | |
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ | |
- | |
-'use strict'; | |
- | |
-var ContactDataManager = { | |
- contactData: {}, | |
- | |
- searchContactData: function cm_searchContactData(string, callback) { | |
- var options = { | |
- filterBy: ['email'], | |
- filterOp: 'contains', | |
- filterValue: string | |
- }; | |
- | |
- var self = this; | |
- var req = window.navigator.mozContacts.find(options); | |
- req.onsuccess = function onsuccess() { | |
- callback(req.result); | |
- }; | |
- | |
- req.onerror = function onerror() { | |
- var msg = 'Contact finding error. Error: ' + req.errorCode; | |
- console.log(msg); | |
- callback(null); | |
- }; | |
- } | |
-}; | |
diff --git a/apps/email/js/ext/mailapi/activesync/configurator.js b/apps/email/js/ext/mailapi/activesync/configurator.js | |
index 7442295..46c4d18 100644 | |
--- a/apps/email/js/ext/mailapi/activesync/configurator.js | |
+++ b/apps/email/js/ext/mailapi/activesync/configurator.js | |
@@ -1386,6 +1386,10 @@ ActiveSyncFolderConn.prototype = { | |
/** | |
* Download the bodies for a set of headers. | |
+ * | |
+ * XXX This method is a slightly modified version of | |
+ * ImapFolderConn._lazyDownloadBodies; we should attempt to remove the | |
+ * duplication. | |
*/ | |
downloadBodies: function(headers, options, callback) { | |
if (this._account.conn.currentVersion.lt('12.0')) | |
@@ -1393,6 +1397,7 @@ ActiveSyncFolderConn.prototype = { | |
var anyErr, | |
pending = 1, | |
+ downloadsNeeded = 0, | |
folderConn = this; | |
function next(err) { | |
@@ -1401,16 +1406,26 @@ ActiveSyncFolderConn.prototype = { | |
if (!--pending) { | |
folderConn._storage.runAfterDeferredCalls(function() { | |
- callback(anyErr); | |
+ callback(anyErr, /* number downloaded */ downloadsNeeded - pending); | |
}); | |
} | |
} | |
for (var i = 0; i < headers.length; i++) { | |
- if (!headers[i] || headers[i].snippet) | |
+ // We obviously can't do anything with null header references. | |
+ // To avoid redundant work, we also don't want to do any fetching if we | |
+ // already have a snippet. This could happen because of the extreme | |
+ // potential for a caller to spam multiple requests at us before we | |
+ // service any of them. (Callers should only have one or two outstanding | |
+ // jobs of this and do their own suppression tracking, but bugs happen.) | |
+ if (!headers[i] || headers[i].snippet !== null) { | |
continue; | |
+ } | |
pending++; | |
+ // This isn't absolutely guaranteed to be 100% correct, but is good enough | |
+ // for indicating to the caller that we did some work. | |
+ downloadsNeeded++; | |
this.downloadBodyReps(headers[i], options, next); | |
} | |
@@ -1684,14 +1699,19 @@ ActiveSyncFolderConn.prototype = { | |
if (!moreAvailable) { | |
var messagesSeen = addedMessages + changedMessages + deletedMessages; | |
- // Note: For the second argument here, we report the number of messages | |
- // we saw that *changed*. This differs from IMAP, which reports the | |
- // number of messages it *saw*. | |
- folderConn._LOG.sync_end(addedMessages, changedMessages, | |
- deletedMessages); | |
- storage.markSyncRange($sync.OLDEST_SYNC_DATE, accuracyStamp, 'XXX', | |
- accuracyStamp); | |
- doneCallback(null, null, messagesSeen); | |
+ // Do not report completion of sync until all of our operations have | |
+ // been persisted to our in-memory database. (We do not wait for | |
+ // things to hit the disk.) | |
+ storage.runAfterDeferredCalls(function() { | |
+ // Note: For the second argument here, we report the number of | |
+ // messages we saw that *changed*. This differs from IMAP, which | |
+ // reports the number of messages it *saw*. | |
+ folderConn._LOG.sync_end(addedMessages, changedMessages, | |
+ deletedMessages); | |
+ storage.markSyncRange($sync.OLDEST_SYNC_DATE, accuracyStamp, 'XXX', | |
+ accuracyStamp); | |
+ doneCallback(null, null, messagesSeen); | |
+ }); | |
} | |
}, | |
progressCallback); | |
@@ -2450,6 +2470,8 @@ ActiveSyncJobDriver.prototype = { | |
do_downloadBodies: $jobmixins.do_downloadBodies, | |
+ check_downloadBodies: $jobmixins.check_downloadBodies, | |
+ | |
////////////////////////////////////////////////////////////////////////////// | |
// downloadBodyReps: Download the bodies from a single message | |
@@ -2457,6 +2479,8 @@ ActiveSyncJobDriver.prototype = { | |
do_downloadBodyReps: $jobmixins.do_downloadBodyReps, | |
+ check_downloadBodyReps: $jobmixins.check_downloadBodyReps, | |
+ | |
////////////////////////////////////////////////////////////////////////////// | |
// download: Download one or more attachments from a single message | |
@@ -2716,6 +2740,8 @@ ActiveSyncAccount.prototype = { | |
path: this.accountDef.name, | |
type: this.accountDef.type, | |
+ defaultPriority: this.accountDef.defaultPriority, | |
+ | |
enabled: this.enabled, | |
problems: this.problems, | |
@@ -3054,7 +3080,10 @@ ActiveSyncAccount.prototype = { | |
/** | |
* Recreate the folder storage for a particular folder; useful when we end up | |
- * desyncing with the server and need to start fresh. | |
+ * desyncing with the server and need to start fresh. No notification is | |
+ * generated, although slices are repopulated. | |
+ * | |
+ * FYI: There is a nearly identical method in IMAP's account implementation. | |
* | |
* @param {string} folderId the local ID of the folder | |
* @param {function} callback a function to be called when the operation is | |
@@ -3373,6 +3402,7 @@ define('mailapi/activesync/configurator', | |
'../accountcommon', | |
'../a64', | |
'./account', | |
+ '../date', | |
'require', | |
'exports' | |
], | |
@@ -3381,6 +3411,7 @@ define('mailapi/activesync/configurator', | |
$accountcommon, | |
$a64, | |
$asacct, | |
+ $date, | |
require, | |
exports | |
) { | |
@@ -3495,6 +3526,7 @@ exports.configurator = { | |
var accountDef = { | |
id: accountId, | |
name: userDetails.accountName || userDetails.emailAddress, | |
+ defaultPriority: $date.NOW(), | |
type: 'activesync', | |
syncRange: 'auto', | |
diff --git a/apps/email/js/ext/mailapi/chewlayer.js b/apps/email/js/ext/mailapi/chewlayer.js | |
index 9a2829c..6437317 100644 | |
--- a/apps/email/js/ext/mailapi/chewlayer.js | |
+++ b/apps/email/js/ext/mailapi/chewlayer.js | |
@@ -2193,37 +2193,33 @@ var HTMLParser = (function(){ | |
// | |
// The spec defines attributes by what they must not include, which is: | |
// [\0\s"'>/=] plus also no control characters, or non-unicode characters. | |
- // But we currently use the same regexp as we use for tags because that's what | |
- // the code was using already. | |
+ // | |
+ // The (inherited) code used to have the regular expression effectively | |
+ // validate the attribute syntax by including their grammer in the regexp. | |
+ // The problem with this is that it can make the regexp fail to match tags | |
+ // that are clearly tags. When we encountered (quoted) attributes without | |
+ // whitespace between them, we would escape the entire tag. Attempted | |
+ // trivial fixes resulted in regex back-tracking, which begged the issue of | |
+ // why the regex would do this in the first place. So we stopped doing that. | |
// | |
// CDATA *is not a thing* in the HTML namespace. <![CDATA[ just gets treated | |
// as a "bogus comment". See: | |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#markup-declaration-open-state | |
- // NOTE: tag and attr regexps changed to ignore name spaces prefixes! via | |
+ // NOTE: tag and attr regexps changed to ignore name spaces prefixes! | |
+ // | |
+ // CHANGE: "we" previously required there to be white-space between attributes. | |
+ // Unfortunately, the world does not agree with this, so we now require | |
+ // whitespace only after the tag name prior to the first attribute and make | |
+ // the whole attribute clause optional. | |
+ // | |
// - Regular Expressions for parsing tags and attributes | |
// ^< anchored tag open character | |
// (?:[-A-Za-z0-9_]+:)? eat the namespace | |
// ([-A-Za-z0-9_]+) the tag name | |
- // ( repeated attributes: | |
- // (?: | |
- // \s+ Mandatory whitespace between attribute names | |
- // (?:[-A-Za-z0-9_]+:)? optional attribute prefix | |
- // [-A-Za-z0-9_]+ attribute name | |
- // (?: The attribute doesn't need a value | |
- // \s*=\s* whitespace, = to indicate value, whitespace | |
- // (?: attribute values: | |
- // (?:"[^"]*")| double-quoted | |
- // (?:'[^']*')| single-quoted | |
- // [^>\s]+ unquoted | |
- // ) | |
- // )? (the attribute does't need a value) | |
- // )* (there can be multiple attributes) | |
- // ) (capture the list of attributes) | |
- // \s* optional whitespace before the tag closer | |
- // (\/?) optional self-closing character | |
+ // ([^>]*) capture attributes and/or closing '/' if present | |
// > tag close character | |
- var startTag = /^<(?:[-A-Za-z0-9_]+:)?([-A-Za-z0-9_]+)((?:\s+(?:[-A-Za-z0-9_]+:)?[-A-Za-z0-9_]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, | |
+ var startTag = /^<(?:[-A-Za-z0-9_]+:)?([-A-Za-z0-9_]+)([^>]*)>/, | |
// ^<\/ close tag lead-in | |
// (?:[-A-Za-z0-9_]+:)? optional tag prefix | |
// ([-A-Za-z0-9_]+) tag name | |
@@ -2379,7 +2375,7 @@ var HTMLParser = (function(){ | |
// Clean up any remaining tags | |
parseEndTag(); | |
- function parseStartTag( tag, tagName, rest, unary ) { | |
+ function parseStartTag( tag, tagName, rest ) { | |
tagName = tagName.toLowerCase(); | |
if ( block[ tagName ] ) { | |
while ( stack.last() && inline[ stack.last() ] ) { | |
@@ -2391,7 +2387,13 @@ var HTMLParser = (function(){ | |
parseEndTag( "", tagName ); | |
} | |
- unary = empty[ tagName ] || !!unary; | |
+ var unary = empty[ tagName ]; | |
+ // to simplify the regexp, the 'rest capture group now absorbs the /, so | |
+ // we need to strip it off if it's there. | |
+ if (rest.length && rest[rest.length - 1] === '/') { | |
+ unary = true; | |
+ rest = rest.slice(0, -1); | |
+ } | |
if ( !unary ) | |
stack.push( tagName ); | |
@@ -3921,13 +3923,16 @@ function chewStructure(msg) { | |
sizeEstimate: partInfo.size, | |
amountDownloaded: 0, | |
// its important to know that sizeEstimate and amountDownloaded | |
- // do _not_ determine if the bodyRep is fully downloaded the | |
+ // do _not_ determine if the bodyRep is fully downloaded; the | |
// estimated amount is not reliable | |
- isDownloaded: false, | |
+ // Zero-byte bodies are assumed to be accurate and we treat the file | |
+ // as already downloaded. | |
+ isDownloaded: partInfo.size === 0, | |
// full internal IMAP representation | |
// it would also be entirely appropriate to move | |
// the information on the bodyRep directly? | |
- _partInfo: partInfo | |
+ _partInfo: partInfo.size ? partInfo : null, | |
+ content: '' | |
}; | |
} | |
@@ -4114,7 +4119,7 @@ exports.updateMessageWithFetch = function(header, body, req, res, _LOG) { | |
bodyRep.isDownloaded = true; | |
// clear private space for maintaining parser state. | |
- delete bodyRep._partInfo; | |
+ bodyRep._partInfo = null; | |
} | |
if (!bodyRep.isDownloaded && res.buffer) { | |
diff --git a/apps/email/js/ext/mailapi/composite/configurator.js b/apps/email/js/ext/mailapi/composite/configurator.js | |
index 6992434..1bf6fd8 100644 | |
--- a/apps/email/js/ext/mailapi/composite/configurator.js | |
+++ b/apps/email/js/ext/mailapi/composite/configurator.js | |
@@ -269,6 +269,7 @@ define('mailapi/imap/folder', | |
var $imaptextparser = null; | |
var $imapsnippetparser = null; | |
var $imapbodyfetcher = null; | |
+var $imapchew = null; | |
var $imapsync = null; | |
var allbackMaker = $allback.allbackMaker, | |
@@ -448,12 +449,14 @@ ImapFolderConn.prototype = { | |
/** | |
* If no connection, acquires one and also sets up | |
* deathback if connection is lost. | |
+ * | |
+ * See `acquireConn` for argument docs. | |
*/ | |
- withConnection: function (callback, deathback, label) { | |
+ withConnection: function (callback, deathback, label, dieOnConnectFailure) { | |
if (!this._conn) { | |
this.acquireConn(function () { | |
this.withConnection(callback, deathback, label); | |
- }.bind(this), deathback, label); | |
+ }.bind(this), deathback, label, dieOnConnectFailure); | |
return; | |
} | |
@@ -813,6 +816,14 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta); | |
overallMaximumBytes -= rep.sizeEstimate; | |
} | |
+ // For a byte-serve request, we need to request at least 1 byte, so | |
+ // request some bytes. This is a logic simplification that should not | |
+ // need to be used because imapchew.js should declare 0-byte files | |
+ // fully downloaded when their parts are created, but better a wasteful | |
+ // network request than breaking here. | |
+ if (bytesToFetch <= 0) | |
+ bytesToFetch = 64; | |
+ | |
// we may only need a subset of the total number of bytes. | |
if (overallMaximumBytes !== undefined || rep.amountDownloaded) { | |
// request the remainder | |
@@ -927,7 +938,7 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta); | |
* Download snippets for a set of headers. | |
*/ | |
_lazyDownloadBodies: function(headers, options, callback) { | |
- var pending = 1; | |
+ var pending = 1, downloadsNeeded = 0; | |
var self = this; | |
var anyErr; | |
@@ -937,17 +948,26 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta); | |
if (!--pending) { | |
self._storage.runAfterDeferredCalls(function() { | |
- callback(anyErr); | |
+ callback(anyErr, /* number downloaded */ downloadsNeeded - pending); | |
}); | |
} | |
} | |
for (var i = 0; i < headers.length; i++) { | |
- if (!headers[i] || headers[i].snippet) { | |
+ // We obviously can't do anything with null header references. | |
+ // To avoid redundant work, we also don't want to do any fetching if we | |
+ // already have a snippet. This could happen because of the extreme | |
+ // potential for a caller to spam multiple requests at us before we | |
+ // service any of them. (Callers should only have one or two outstanding | |
+ // jobs of this and do their own suppression tracking, but bugs happen.) | |
+ if (!headers[i] || headers[i].snippet !== null) { | |
continue; | |
} | |
pending++; | |
+ // This isn't absolutely guaranteed to be 100% correct, but is good enough | |
+ // for indicating to the caller that we did some work. | |
+ downloadsNeeded++; | |
this.downloadBodyReps(headers[i], options, next); | |
} | |
@@ -1160,14 +1180,34 @@ ImapFolderSyncer.prototype = { | |
initialSync: function(slice, initialDays, syncCallback, | |
doneCallback, progressCallback) { | |
syncCallback('sync', false); | |
- this._startSync( | |
- slice, PASTWARDS, // sync into the past | |
- 'grow', | |
- null, // start syncing from the (unconstrained) future | |
- $sync.OLDEST_SYNC_DATE, // sync no further back than this constant | |
- null, | |
- initialDays, | |
- doneCallback, progressCallback); | |
+ // We want to enter the folder and get the box info so we can know if we | |
+ // should trigger our SYNC_WHOLE_FOLDER_AT_N_MESSAGES logic. | |
+ // _timelySyncSearch is what will get called next either way, and it will | |
+ // just reuse the connection and will correctly update the deathback so | |
+ // that our deathback is no longer active. | |
+ this.folderConn.withConnection( | |
+ function(folderConn, storage) { | |
+ // Flag to sync the whole range if we | |
+ var syncWholeTimeRange = false; | |
+ if (folderConn && folderConn.box && | |
+ folderConn.box.messages.total < | |
+ $sync.SYNC_WHOLE_FOLDER_AT_N_MESSAGES) { | |
+ syncWholeTimeRange = true; | |
+ } | |
+ | |
+ this._startSync( | |
+ slice, PASTWARDS, // sync into the past | |
+ 'grow', | |
+ null, // start syncing from the (unconstrained) future | |
+ $sync.OLDEST_SYNC_DATE, // sync no further back than this constant | |
+ null, | |
+ syncWholeTimeRange ? null : initialDays, | |
+ doneCallback, progressCallback); | |
+ }.bind(this), | |
+ function died() { | |
+ doneCallback('aborted'); | |
+ }, | |
+ 'initialSync', true); | |
}, | |
/** | |
@@ -1491,27 +1531,27 @@ console.log("folder message count", folderMessageCount, | |
daysToSearch = Math.ceil(this._curSyncDayStep * | |
$sync.TIME_SCALE_FACTOR_ON_NO_MESSAGES); | |
+ // These values used to be more conservative, but the importance of these | |
+ // guards was reduced when we switched to only syncing headers. | |
+ // At current constants (sync=3, scale=2), our doubling in the face of | |
+ // clamping is: 3, 6, 12, 24, 45, ... 90, | |
if (lastSyncDaysInPast < 180) { | |
- if (daysToSearch > 14) | |
- daysToSearch = 14; | |
+ if (daysToSearch > 45) | |
+ daysToSearch = 45; | |
} | |
- else if (lastSyncDaysInPast < 365) { | |
- if (daysToSearch > 30) | |
- daysToSearch = 30; | |
- } | |
- else if (lastSyncDaysInPast < 730) { | |
- if (daysToSearch > 60) | |
- daysToSearch = 60; | |
- } | |
- else if (lastSyncDaysInPast < 1095) { | |
+ else if (lastSyncDaysInPast < 365) { // 1 year | |
if (daysToSearch > 90) | |
daysToSearch = 90; | |
} | |
- else if (lastSyncDaysInPast < 1825) { // 5 years | |
+ else if (lastSyncDaysInPast < 730) { // 2 years | |
if (daysToSearch > 120) | |
daysToSearch = 120; | |
} | |
- else if (lastSyncDaysInPast < 3650) { | |
+ else if (lastSyncDaysInPast < 1825) { // 5 years | |
+ if (daysToSearch > 180) | |
+ daysToSearch = 180; | |
+ } | |
+ else if (lastSyncDaysInPast < 3650) { // 10 years | |
if (daysToSearch > 365) | |
daysToSearch = 365; | |
} | |
@@ -1850,7 +1890,11 @@ ImapJobDriver.prototype = { | |
}); | |
action(); | |
- }, deathback, label); | |
+ }, | |
+ // Always pass true for dieOnConnectFailure; we don't want any of our | |
+ // operations hanging out waiting for retry backoffs. The ops want to | |
+ // only run when we believe we are online with a good connection. | |
+ deathback, label, true); | |
} else { | |
action(); | |
} | |
@@ -1900,6 +1944,8 @@ ImapJobDriver.prototype = { | |
do_downloadBodies: $jobmixins.do_downloadBodies, | |
+ check_downloadBodies: $jobmixins.check_downloadBodies, | |
+ | |
////////////////////////////////////////////////////////////////////////////// | |
// downloadBodyReps: Download the bodies from a single message | |
@@ -1907,6 +1953,8 @@ ImapJobDriver.prototype = { | |
do_downloadBodyReps: $jobmixins.do_downloadBodyReps, | |
+ check_downloadBodyReps: $jobmixins.check_downloadBodyReps, | |
+ | |
////////////////////////////////////////////////////////////////////////////// | |
// download: Download one or more attachments from a single message | |
@@ -2972,6 +3020,50 @@ ImapAccount.prototype = { | |
}, | |
/** | |
+ * Completely reset the state of a folder. For use by unit tests and in the | |
+ * case of UID validity rolls. No notification is generated, although slices | |
+ * are repopulated. | |
+ * | |
+ * FYI: There is a nearly identical method in ActiveSync's account | |
+ * implementation. | |
+ */ | |
+ _recreateFolder: function(folderId, callback) { | |
+ this._LOG.recreateFolder(folderId); | |
+ var folderInfo = this._folderInfos[folderId]; | |
+ folderInfo.$impl = { | |
+ nextId: 0, | |
+ nextHeaderBlock: 0, | |
+ nextBodyBlock: 0, | |
+ }; | |
+ folderInfo.accuracy = []; | |
+ folderInfo.headerBlocks = []; | |
+ folderInfo.bodyBlocks = []; | |
+ // IMAP does not use serverIdHeaderBlockMapping | |
+ | |
+ if (this._deadFolderIds === null) | |
+ this._deadFolderIds = []; | |
+ this._deadFolderIds.push(folderId); | |
+ | |
+ var self = this; | |
+ this.saveAccountState(null, function() { | |
+ var newStorage = | |
+ new $mailslice.FolderStorage(self, folderId, folderInfo, self._db, | |
+ $imapfolder.ImapFolderSyncer, | |
+ self._LOG); | |
+ for (var iter in Iterator(self._folderStorages[folderId]._slices)) { | |
+ var slice = iter[1]; | |
+ slice._storage = newStorage; | |
+ slice.reset(); | |
+ newStorage.sliceOpenMostRecent(slice); | |
+ } | |
+ self._folderStorages[folderId]._slices = []; | |
+ self._folderStorages[folderId] = newStorage; | |
+ | |
+ callback(newStorage); | |
+ }, 'recreateFolder'); | |
+ }, | |
+ | |
+ /** | |
* We are being told that a synchronization pass completed, and that we may | |
* want to consider persisting our state. | |
*/ | |
@@ -2989,10 +3081,10 @@ ImapAccount.prototype = { | |
* that ever ends up not being the case that we need to cause mutating | |
* operations to defer until after that snapshot has occurred. | |
*/ | |
- saveAccountState: function(reuseTrans, callback) { | |
+ saveAccountState: function(reuseTrans, callback, reason) { | |
if (!this._alive) { | |
this._LOG.accountDeleted('saveAccountState'); | |
- return; | |
+ return null; | |
} | |
var perFolderStuff = [], self = this; | |
@@ -3003,7 +3095,7 @@ ImapAccount.prototype = { | |
if (folderStuff) | |
perFolderStuff.push(folderStuff); | |
} | |
- this._LOG.saveAccountState(); | |
+ this._LOG.saveAccountState(reason); | |
var trans = this._db.saveAccountFolderStates( | |
this.id, this._folderInfos, perFolderStuff, | |
this._deadFolderIds, | |
@@ -3733,6 +3825,7 @@ var LOGFAB = exports.LOGFAB = $log.register($module, { | |
events: { | |
createFolder: {}, | |
deleteFolder: {}, | |
+ recreateFolder: { id: false }, | |
createConnection: {}, | |
reuseConnection: {}, | |
@@ -3741,7 +3834,7 @@ var LOGFAB = exports.LOGFAB = $log.register($module, { | |
unknownDeadConnection: {}, | |
connectionMismatch: {}, | |
- saveAccountState: {}, | |
+ saveAccountState: { reason: false }, | |
/** | |
* XXX: this is really an error/warning, but to make the logging less | |
* confusing, treat it as an event. | |
@@ -4114,6 +4207,8 @@ CompositeAccount.prototype = { | |
name: this.accountDef.name, | |
type: this.accountDef.type, | |
+ defaultPriority: this.accountDef.defaultPriority, | |
+ | |
enabled: this.enabled, | |
problems: this.problems, | |
@@ -4258,6 +4353,7 @@ define('mailapi/composite/configurator', | |
'../a64', | |
'../allback', | |
'./account', | |
+ '../date', | |
'require', | |
'exports' | |
], | |
@@ -4267,6 +4363,7 @@ define('mailapi/composite/configurator', | |
$a64, | |
$allback, | |
$account, | |
+ $date, | |
require, | |
exports | |
) { | |
@@ -4393,6 +4490,7 @@ exports.configurator = { | |
var accountDef = { | |
id: accountId, | |
name: userDetails.accountName || userDetails.emailAddress, | |
+ defaultPriority: $date.NOW(), | |
type: 'imap+smtp', | |
receiveType: 'imap', | |
diff --git a/apps/email/js/ext/mailapi/main-frame-setup.js b/apps/email/js/ext/mailapi/main-frame-setup.js | |
index d9d4f46..b10d558 100644 | |
--- a/apps/email/js/ext/mailapi/main-frame-setup.js | |
+++ b/apps/email/js/ext/mailapi/main-frame-setup.js | |
@@ -142,13 +142,16 @@ var HEADER_CACHE_LIMIT = 8; | |
/** | |
* | |
*/ | |
-function MailAccount(api, wireRep) { | |
+function MailAccount(api, wireRep, acctsSlice) { | |
this._api = api; | |
this.id = wireRep.id; | |
// Hold on to wireRep for caching | |
this._wireRep = wireRep; | |
+ // Hold on to acctsSlice for use in determining default account. | |
+ this.acctsSlice = acctsSlice; | |
+ | |
this.type = wireRep.type; | |
this.name = wireRep.name; | |
this.syncRange = wireRep.syncRange; | |
@@ -209,6 +212,11 @@ MailAccount.prototype = { | |
__update: function(wireRep) { | |
this.enabled = wireRep.enabled; | |
this.problems = wireRep.problems; | |
+ this._wireRep.defaultPriority = wireRep.defaultPriority; | |
+ }, | |
+ | |
+ __die: function() { | |
+ // currently, nothing to clean up | |
}, | |
/** | |
@@ -224,7 +232,11 @@ MailAccount.prototype = { | |
* @param[mods @dict[ | |
* @key[password String] | |
* ]] | |
- * ] | |
+ * ]{ | |
+ * In addition to regular account property settings, | |
+ * "setAsDefault": true can be passed to set this | |
+ * account as the default acccount. | |
+ * } | |
*/ | |
modifyAccount: function(mods) { | |
this._api._modifyAccount(this, mods); | |
@@ -238,6 +250,17 @@ MailAccount.prototype = { | |
deleteAccount: function() { | |
this._api._deleteAccount(this); | |
}, | |
+ | |
+ /** | |
+ * Returns true if this account is the default account, by looking at | |
+ * all accounts in the acctsSlice. | |
+ */ | |
+ get isDefault() { | |
+ if (!this.acctsSlice) | |
+ throw new Error('No account slice available'); | |
+ | |
+ return this.acctsSlice.defaultAccount === this; | |
+ }, | |
}; | |
/** | |
@@ -268,6 +291,10 @@ MailSenderIdentity.prototype = { | |
toJSON: function() { | |
return { type: 'MailSenderIdentity' }; | |
}, | |
+ | |
+ __die: function() { | |
+ // nothing to clean up currently | |
+ }, | |
}; | |
function MailFolder(api, wireRep) { | |
@@ -351,6 +378,10 @@ MailFolder.prototype = { | |
this.lastSyncedAt = wireRep.lastSyncedAt ? new Date(wireRep.lastSyncedAt) | |
: null; | |
}, | |
+ | |
+ __die: function() { | |
+ // currently nothing to clean up | |
+ } | |
}; | |
function filterOutBuiltinFlags(flags) { | |
@@ -381,18 +412,44 @@ function serializeMessageName(x) { | |
} | |
/** | |
- * Caches contact lookups, both hits and misses. | |
+ * Caches contact lookups, both hits and misses, as well as updating the | |
+ * MailPeep instances returned by resolve calls. | |
+ * | |
+ * We maintain strong maps from both contact id and e-mail address to MailPeep | |
+ * instances. We hold a strong reference because BridgedViewSlices already | |
+ * require explicit lifecycle maintenance (aka call die() when done with them). | |
+ * We need the contact id and e-mail address because when a contact is changed, | |
+ * an e-mail address may be changed, and we don't get to see the old | |
+ * representation. So if the e-mail address was deleted, we need the contact id | |
+ * mapping. And if the e-mail address was added, we need the e-mail address | |
+ * mapping. | |
+ * | |
+ * If the mozContacts API is not available, we just create inert MailPeep | |
+ * instances that do not get tracked or updated. | |
+ * | |
+ * Domain notes: | |
+ * | |
+ * The contacts API does not enforce any constraints on the number of contacts | |
+ * who can use an e-mail address, but the e-mail app only allows one contact | |
+ * to correspond to an e-mail address at a time. | |
*/ | |
-var ContactCache = { | |
+var ContactCache = exports.ContactCache = { | |
/** | |
* Maps e-mail addresses to the mozContact rep for the object, or null if | |
* there was a miss. | |
+ * | |
+ * We explicitly do not want to choose an arbitrary MailPeep instance to | |
+ * (re)use because it could lead to GC memory leaks if data/element/an expando | |
+ * were set on the MailPeep and we did not zero it out when the owning slice | |
+ * was destroyed. We could, however, use the live set of peeps as a fallback | |
+ * if we don't have a contact cached. | |
*/ | |
- _cache: {}, | |
+ _contactCache: Object.create(null), | |
/** The number of entries in the cache. */ | |
_cacheHitEntries: 0, | |
/** The number of stored misses in the cache. */ | |
_cacheEmptyEntries: 0, | |
+ | |
/** | |
* Maximum number of hit entries in the cache before we should clear the | |
* cache. | |
@@ -400,6 +457,189 @@ var ContactCache = { | |
MAX_CACHE_HITS: 256, | |
/** Maximum number of empty entries to store in the cache before clearing. */ | |
MAX_CACHE_EMPTY: 1024, | |
+ | |
+ /** Maps contact id to lists of MailPeep instances. */ | |
+ _livePeepsById: Object.create(null), | |
+ /** Maps e-mail addresses to lists of MailPeep instances */ | |
+ _livePeepsByEmail: Object.create(null), | |
+ | |
+ pendingLookupCount: 0, | |
+ | |
+ callbacks: [], | |
+ | |
+ init: function() { | |
+ var contactsAPI = navigator.mozContacts; | |
+ if (!contactsAPI) | |
+ return; | |
+ | |
+ contactsAPI.oncontactchange = this._onContactChange.bind(this); | |
+ }, | |
+ | |
+ _resetCache: function() { | |
+ this._contactCache = Object.create(null); | |
+ this._cacheHitEntries = 0; | |
+ this._cacheEmptyEntries = 0; | |
+ }, | |
+ | |
+ shutdown: function() { | |
+ var contactsAPI = navigator.mozContacts; | |
+ if (!contactsAPI) | |
+ return; | |
+ contactsAPI.oncontactchange = null; | |
+ }, | |
+ | |
+ /** | |
+ * Currently we process the updates in real-time as we get them. There's an | |
+ * inherent trade-off between chewing CPU when we're in the background and | |
+ * minimizing latency when we are displayed. We're biased towards minimizing | |
+ * latency right now. | |
+ * | |
+ * All contact changes flush our contact cache rather than try and be fancy. | |
+ * We are already fancy with the set of live peeps and our lookups could just | |
+ * leverage that. (The contact cache is just intended as a steady-state | |
+ * high-throughput thing like when displaying messages in the UI. We don't | |
+ * expect a lot of contact changes to happen during that time.) | |
+ * | |
+ * For info on the events/triggers, see: | |
+ * https://developer.mozilla.org/en-US/docs/DOM/ContactManager.oncontactchange | |
+ */ | |
+ _onContactChange: function(event) { | |
+ var contactsAPI = navigator.mozContacts; | |
+ var livePeepsById = this._livePeepsById, | |
+ livePeepsByEmail = this._livePeepsByEmail; | |
+ | |
+ // clear the cache if it has anything in it (per the above doc block) | |
+ if (this._cacheHitEntries || this._cacheEmptyEntries) | |
+ this._resetCache(); | |
+ | |
+ // -- Contact removed OR all contacts removed! | |
+ if (event.reason === 'remove') { | |
+ function cleanOutPeeps(livePeeps) { | |
+ for (var iPeep = 0; iPeep < livePeeps.length; iPeep++) { | |
+ var peep = livePeeps[iPeep]; | |
+ peep.contactId = null; | |
+ if (peep.onchange) { | |
+ try { | |
+ peep.onchange(peep); | |
+ } | |
+ catch (ex) { | |
+ reportClientCodeError('peep.onchange error', ex, '\n', | |
+ ex.stack); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ // - all contacts removed! (clear() called) | |
+ var livePeeps; | |
+ if (!event.contactID) { | |
+ for (var contactId in livePeepsById) { | |
+ livePeeps = livePeepsById[contactId]; | |
+ cleanOutPeeps(livePeeps); | |
+ this._livePeepsById = Object.create(null); | |
+ } | |
+ } | |
+ // - just one contact removed | |
+ else { | |
+ livePeeps = livePeepsById[event.contactID]; | |
+ if (livePeeps) { | |
+ cleanOutPeeps(livePeeps); | |
+ delete livePeepsById[event.contactID]; | |
+ } | |
+ } | |
+ } | |
+ // -- Created or updated; we need to fetch the contact to investigate | |
+ else { | |
+ var req = contactsAPI.find({ | |
+ filterBy: ['id'], | |
+ filterOp: 'equals', | |
+ filterValue: event.contactID | |
+ }); | |
+ req.onsuccess = function() { | |
+ // If the contact disappeared we will hear a 'remove' event and so don't | |
+ // need to process this. | |
+ if (!req.result.length) | |
+ return; | |
+ var contact = req.result[0], livePeeps, iPeep, peep; | |
+ | |
+ // - process update with apparent e-mail address removal | |
+ if (event.reason === 'update') { | |
+ livePeeps = livePeepsById[contact.id]; | |
+ if (livePeeps) { | |
+ var contactEmails = contact.email ? | |
+ contact.email.map(function(e) { return e.value; }) : | |
+ []; | |
+ for (iPeep = 0; iPeep < livePeeps.length; iPeep++) { | |
+ peep = livePeeps[iPeep]; | |
+ if (contactEmails.indexOf(peep.address) === -1) { | |
+ // Need to fix-up iPeep because of the splice; reverse iteration | |
+ // reorders our notifications and we don't want that, hence | |
+ // this. | |
+ livePeeps.splice(iPeep--, 1); | |
+ peep.contactId = null; | |
+ if (peep.onchange) { | |
+ try { | |
+ peep.onchange(peep); | |
+ } | |
+ catch (ex) { | |
+ reportClientCodeError('peep.onchange error', ex, '\n', | |
+ ex.stack); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ if (livePeeps.length === 0) | |
+ delete livePeepsById[contact.id]; | |
+ } | |
+ } | |
+ // - process create/update causing new coverage | |
+ if (!contact.email) | |
+ return; | |
+ for (var iEmail = 0; iEmail < contact.email.length; iEmail++) { | |
+ var email = contact.email[iEmail].value; | |
+ livePeeps = livePeepsByEmail[email]; | |
+ // nothing to do if there are no peeps that use that email address | |
+ if (!livePeeps) | |
+ continue; | |
+ | |
+ for (iPeep = 0; iPeep < livePeeps.length; iPeep++) { | |
+ peep = livePeeps[iPeep]; | |
+ // If the peep is not yet associated with this contact or any other | |
+ // contact, then associate it. | |
+ if (!peep.contactId) { | |
+ peep.contactId = contact.id; | |
+ var idLivePeeps = livePeepsById[peep.contactId]; | |
+ if (idLivePeeps === undefined) | |
+ idLivePeeps = livePeepsById[peep.contactId] = []; | |
+ idLivePeeps.push(peep); | |
+ } | |
+ // However, if it's associated with a different contact, then just | |
+ // skip the peep. | |
+ else if (peep.contactId !== contact.id) { | |
+ continue; | |
+ } | |
+ // (The peep must be associated with this contact, so update and | |
+ // fire) | |
+ | |
+ if (contact.name && contact.name.length) | |
+ peep.name = contact.name[0]; | |
+ if (peep.onchange) { | |
+ try { | |
+ peep.onchange(peep); | |
+ } | |
+ catch (ex) { | |
+ reportClientCodeError('peep.onchange error', ex, '\n', | |
+ ex.stack); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ }; | |
+ // We don't need to do anything about onerror; the 'remove' event will | |
+ // probably have fired in this case, making us correct. | |
+ } | |
+ }, | |
+ | |
resolvePeeps: function(addressPairs) { | |
if (addressPairs === null) | |
return null; | |
@@ -409,76 +649,106 @@ var ContactCache = { | |
} | |
return resolved; | |
}, | |
+ /** | |
+ * Create a MailPeep instance with the best information available and return | |
+ * it. Information from the (moz)Contacts API always trumps the passed-in | |
+ * information. If we have a cache hit (which covers both positive and | |
+ * negative evidence), we are done/all resolved immediately. Otherwise, we | |
+ * need to issue an async request. In that case, you want to check | |
+ * ContactCache.pendingLookupCount and push yourself onto | |
+ * ContactCache.callbacks if you want to be notified when the current set of | |
+ * lookups gets resolved. | |
+ * | |
+ * This is a slightly odd API, but it's based on the knowledge that for a | |
+ * single e-mail we will potentially need to perform multiple lookups and that | |
+ * e-mail addresses are also likely to come in batches so there's no need to | |
+ * generate N callbacks when 1 will do. | |
+ */ | |
resolvePeep: function(addressPair) { | |
var emailAddress = addressPair.address; | |
- var entry = this._cache[emailAddress], contact; | |
+ var entry = this._contactCache[emailAddress], contact, peep; | |
+ var contactsAPI = navigator.mozContacts; | |
// known miss; create miss peep | |
- if (entry === null) { | |
- return new MailPeep(addressPair.name || '', emailAddress, false, null); | |
+ // no contacts API, always a miss, skip out before extra logic happens | |
+ if (entry === null || !contactsAPI) { | |
+ peep = new MailPeep(addressPair.name || '', emailAddress, null, null); | |
+ if (!contactsAPI) | |
+ return peep; | |
} | |
// known contact; unpack contact info | |
else if (entry !== undefined) { | |
- return new MailPeep(entry.name || addressPair.name || '', emailAddress, | |
- true, | |
+ peep = new MailPeep(entry.name || addressPair.name || '', emailAddress, | |
+ entry.id, | |
(entry.photo && entry.photo.length) ? | |
entry.photo[0] : null); | |
} | |
// not yet looked-up; assume it's a miss and we'll fix-up if it's a hit | |
else { | |
- var peep = new MailPeep(addressPair.name || '', emailAddress, false, | |
- null), | |
- pendingLookups = this.pendingLookups; | |
- | |
- var idxPendingLookup = pendingLookups.indexOf(emailAddress), | |
- peepsToFixup; | |
- if (idxPendingLookup !== -1) { | |
- peepsToFixup = pendingLookups[idxPendingLookup + 1]; | |
- peepsToFixup.push(peep); | |
- return peep; | |
- } | |
+ peep = new MailPeep(addressPair.name || '', | |
+ emailAddress, null, null); | |
- var contactsAPI = navigator.mozContacts; | |
- if (!contactsAPI) | |
- return peep; | |
+ // Place a speculative miss in the contact cache so that additional | |
+ // requests take that path. They will get fixed up when our lookup | |
+ // returns (or if a change event happens to come in before our lookup | |
+ // returns.) Note that we do not do any hit/miss counting right now; we | |
+ // wait for the result to come back. | |
+ this._contactCache[emailAddress] = null; | |
+ this.pendingLookupCount++; | |
var req = contactsAPI.find({ | |
filterBy: ['email'], | |
- filterOp: 'contains', | |
+ filterOp: 'equals', | |
filterValue: emailAddress | |
}); | |
- pendingLookups.push(emailAddress); | |
- pendingLookups.push(peepsToFixup = [peep]); | |
- var handleResult = function handleResult() { | |
- var idxPendingLookup = pendingLookups.indexOf(emailAddress), i; | |
+ var self = this, handleResult = function() { | |
if (req.result && req.result.length) { | |
var contact = req.result[0]; | |
- ContactCache._cache[emailAddress] = contact; | |
- if (++ContactCache._cacheHitEntries > ContactCache.MAX_CACHE_HITS) { | |
- ContactCache._cacheHitEntries = 0; | |
- ContactCache._cacheEmptyEntries = 0; | |
- ContactCache._cache = {}; | |
- } | |
+ ContactCache._contactCache[emailAddress] = contact; | |
+ if (++ContactCache._cacheHitEntries > ContactCache.MAX_CACHE_HITS) | |
+ self._resetCache(); | |
- for (i = 0; i < peepsToFixup.length; i++) { | |
+ var peepsToFixup = self._livePeepsByEmail[emailAddress]; | |
+ // there might no longer be any MailPeeps alive to care; leave | |
+ if (!peepsToFixup) | |
+ return; | |
+ for (var i = 0; i < peepsToFixup.length; i++) { | |
var peep = peepsToFixup[i]; | |
- peep.isContact = true; | |
+ if (!peep.contactId) { | |
+ peep.contactId = contact.id; | |
+ var livePeeps = self._livePeepsById[peep.contactId]; | |
+ if (livePeeps === undefined) | |
+ livePeeps = self._livePeepsById[peep.contactId] = []; | |
+ livePeeps.push(peep); | |
+ } | |
+ | |
if (contact.name && contact.name.length) | |
peep.name = contact.name[0]; | |
if (contact.photo && contact.photo.length) | |
peep._thumbnailBlob = contact.photo[0]; | |
+ | |
+ // If no one is waiting for our/any request to complete, generate an | |
+ // onchange notification. | |
+ if (!self.callbacks.length) { | |
+ if (peep.onchange) { | |
+ try { | |
+ peep.onchange(peep); | |
+ } | |
+ catch (ex) { | |
+ reportClientCodeError('peep.onchange error', ex, '\n', | |
+ ex.stack); | |
+ } | |
+ } | |
+ } | |
} | |
} | |
else { | |
- ContactCache._cache[emailAddress] = null; | |
- if (++ContactCache._cacheEmptyEntries > ContactCache.MAX_CACHE_EMPTY) { | |
- ContactCache._cacheHitEntries = 0; | |
- ContactCache._cacheEmptyEntries = 0; | |
- ContactCache._cache = {}; | |
- } | |
+ ContactCache._contactCache[emailAddress] = null; | |
+ if (++ContactCache._cacheEmptyEntries > ContactCache.MAX_CACHE_EMPTY) | |
+ self._resetCache(); | |
} | |
- pendingLookups.splice(idxPendingLookup, 2); | |
- if (!pendingLookups.length) { | |
+ // Only notify callbacks if all outstanding lookups have completed | |
+ if (--self.pendingLookupCount === 0) { | |
for (i = 0; i < ContactCache.callbacks.length; i++) { | |
ContactCache.callbacks[i](); | |
} | |
@@ -487,12 +757,57 @@ var ContactCache = { | |
}; | |
req.onsuccess = handleResult; | |
req.onerror = handleResult; | |
+ } | |
- return peep; | |
+ // - track the peep in our lists of live peeps | |
+ var livePeeps; | |
+ livePeeps = this._livePeepsByEmail[emailAddress]; | |
+ if (livePeeps === undefined) | |
+ livePeeps = this._livePeepsByEmail[emailAddress] = []; | |
+ livePeeps.push(peep); | |
+ | |
+ if (peep.contactId) { | |
+ livePeeps = this._livePeepsById[peep.contactId]; | |
+ if (livePeeps === undefined) | |
+ livePeeps = this._livePeepsById[peep.contactId] = []; | |
+ livePeeps.push(peep); | |
+ } | |
+ | |
+ return peep; | |
+ }, | |
+ | |
+ forgetPeepInstances: function() { | |
+ var livePeepsById = this._livePeepsById, | |
+ livePeepsByEmail = this._livePeepsByEmail; | |
+ for (var iArg = 0; iArg < arguments.length; iArg++) { | |
+ var peeps = arguments[iArg]; | |
+ if (!peeps) | |
+ continue; | |
+ for (var iPeep = 0; iPeep < peeps.length; iPeep++) { | |
+ var peep = peeps[iPeep], livePeeps, idx; | |
+ if (peep.contactId) { | |
+ livePeeps = livePeepsById[peep.contactId]; | |
+ if (livePeeps) { | |
+ idx = livePeeps.indexOf(peep); | |
+ if (idx !== -1) { | |
+ livePeeps.splice(idx, 1); | |
+ if (livePeeps.length === 0) | |
+ delete livePeepsById[peep.contactId]; | |
+ } | |
+ } | |
+ } | |
+ livePeeps = livePeepsByEmail[peep.address]; | |
+ if (livePeeps) { | |
+ idx = livePeeps.indexOf(peep); | |
+ if (idx !== -1) { | |
+ livePeeps.splice(idx, 1); | |
+ if (livePeeps.length === 0) | |
+ delete livePeepsByEmail[peep.address]; | |
+ } | |
+ } | |
+ } | |
} | |
}, | |
- pendingLookups: [], | |
- callbacks: [], | |
}; | |
function revokeImageSrc() { | |
@@ -515,13 +830,24 @@ function showBlobInImg(imgNode, blob) { | |
imgNode.addEventListener('load', revokeImageSrc); | |
} | |
-function MailPeep(name, address, isContact, thumbnailBlob) { | |
- this.isContact = isContact; | |
+function MailPeep(name, address, contactId, thumbnailBlob) { | |
this.name = name; | |
this.address = address; | |
+ this.contactId = contactId; | |
this._thumbnailBlob = thumbnailBlob; | |
+ | |
+ this.element = null; | |
+ this.data = null; | |
+ // peeps are usually one of: from, to, cc, bcc | |
+ this.type = null; | |
+ | |
+ this.onchange = null; | |
} | |
MailPeep.prototype = { | |
+ get isContact() { | |
+ return this.contactId !== null; | |
+ }, | |
+ | |
toString: function() { | |
return '[MailPeep: ' + this.address + ']'; | |
}, | |
@@ -529,6 +855,13 @@ MailPeep.prototype = { | |
return { | |
name: this.name, | |
address: this.address, | |
+ contactId: this.contactId | |
+ }; | |
+ }, | |
+ toWireRep: function() { | |
+ return { | |
+ name: this.name, | |
+ address: this.address | |
}; | |
}, | |
@@ -596,6 +929,27 @@ MailHeader.prototype = { | |
}; | |
}, | |
+ /** | |
+ * The use-case is the message list providing the message reader with a | |
+ * header. The header really wants to get update notifications from the | |
+ * backend and therefore not be inert, but that's a little complicated and out | |
+ * of scope for the current bug. | |
+ * | |
+ * We clone at all because our MailPeep.onchange and MailPeep.element values | |
+ * were getting clobbered. All the instances are currently intended to map | |
+ * 1:1 to a single UI widget, so cloning seems like the right thing to do. | |
+ * | |
+ * A deeper issue is whether the message reader will want to have its own | |
+ * slice since the reader will soon allow forward/backward navigation. I | |
+ * assume we'll want the message list to track that movement, which suggests | |
+ * that it really doesn't want to do that. This suggests we'll either want | |
+ * non-inert clones or to just use a list-of-handlers model with us using | |
+ * closures and being careful about removing event handlers. | |
+ */ | |
+ makeCopy: function() { | |
+ return new MailHeader(this._slice, this._wireRep); | |
+ }, | |
+ | |
__update: function(wireRep) { | |
if (wireRep.snippet !== null) | |
this.snippet = wireRep.snippet; | |
@@ -609,6 +963,14 @@ MailHeader.prototype = { | |
}, | |
/** | |
+ * Release subscriptions associated with the header; currently this just means | |
+ * tell the ContactCache we no longer care about the `MailPeep` instances. | |
+ */ | |
+ __die: function() { | |
+ ContactCache.forgetPeepInstances([this.author], this.to, this.cc, this.bcc); | |
+ }, | |
+ | |
+ /** | |
* Delete this message | |
*/ | |
deleteMessage: function() { | |
@@ -746,6 +1108,10 @@ MailMatchedHeader.prototype = { | |
id: this.header.id | |
}; | |
}, | |
+ | |
+ __die: function() { | |
+ this.header.__die(); | |
+ }, | |
}; | |
/** | |
@@ -1217,9 +1583,34 @@ BridgedViewSlice.prototype = { | |
type: 'killSlice', | |
handle: this._handle | |
}); | |
+ | |
+ for (var i = 0; i < this.items.length; i++) { | |
+ var item = this.items[i]; | |
+ item.__die(); | |
+ } | |
}, | |
}; | |
+function AccountsViewSlice(api, handle) { | |
+ BridgedViewSlice.call(this, api, 'accounts', handle); | |
+} | |
+AccountsViewSlice.prototype = Object.create(BridgedViewSlice.prototype); | |
+ | |
+Object.defineProperty(AccountsViewSlice.prototype, 'defaultAccount', { | |
+ get: function () { | |
+ var defaultAccount = this.items[0]; | |
+ for (var i = 1; i < this.items.length; i++) { | |
+ // For UI upgrades, the defaultPriority may not be set, so default to | |
+ // zero for comparisons | |
+ if ((this.items[i]._wireRep.defaultPriority || 0) > | |
+ (defaultAccount._wireRep.defaultPriority || 0)) | |
+ defaultAccount = this.items[i]; | |
+ } | |
+ | |
+ return defaultAccount; | |
+ } | |
+}); | |
+ | |
function FoldersViewSlice(api, handle) { | |
BridgedViewSlice.call(this, api, 'folders', handle); | |
} | |
@@ -1256,6 +1647,7 @@ function HeadersViewSlice(api, handle, ns) { | |
this._bodiesRequest = {}; | |
} | |
HeadersViewSlice.prototype = Object.create(BridgedViewSlice.prototype); | |
+ | |
/** | |
* Request a re-sync of the time interval covering the effective time | |
* range. If the most recently displayed message is the most recent message | |
@@ -1731,6 +2123,8 @@ function MailAPI() { | |
} | |
this._setHasAccounts(); | |
+ | |
+ ContactCache.init(); | |
} | |
exports.MailAPI = MailAPI; | |
MailAPI.prototype = { | |
@@ -1850,7 +2244,7 @@ MailAPI.prototype = { | |
_recv_badLogin: function ma__recv_badLogin(msg) { | |
if (this.onbadlogin) | |
- this.onbadlogin(new MailAccount(this, msg.account), msg.problem); | |
+ this.onbadlogin(new MailAccount(this, msg.account, null), msg.problem); | |
return true; | |
}, | |
@@ -1874,7 +2268,7 @@ MailAPI.prototype = { | |
// call to mozContacts. In this case, we don't want to surface the data to | |
// the UI until the contacts are fully resolved in order to avoid the UI | |
// flickering or just triggering reflows that could otherwise be avoided. | |
- if (ContactCache.pendingLookups.length) { | |
+ if (ContactCache.pendingLookupCount) { | |
ContactCache.callbacks.push(function contactsResolved() { | |
this._fire_sliceSplice(msg, slice, transformedItems, fake); | |
this._doneProcessingMessage(msg); | |
@@ -1892,7 +2286,7 @@ MailAPI.prototype = { | |
switch (slice._ns) { | |
case 'accounts': | |
for (i = 0; i < addItems.length; i++) { | |
- transformedItems.push(new MailAccount(this, addItems[i])); | |
+ transformedItems.push(new MailAccount(this, addItems[i], slice)); | |
} | |
break; | |
@@ -2015,6 +2409,8 @@ MailAPI.prototype = { | |
slice.onremove(item, i); | |
if (item.onremove) | |
item.onremove(item, i); | |
+ // the item needs a chance to clean up after itself. | |
+ item.__die(); | |
} | |
} | |
catch (ex) { | |
@@ -2050,7 +2446,7 @@ MailAPI.prototype = { | |
// reset before calling in case it wants to chain. | |
slice.oncomplete = null; | |
try { | |
- completeFunc(); | |
+ completeFunc(msg.newEmailCount); | |
} | |
catch (ex) { | |
reportClientCodeError('oncomplete notification error', ex, | |
@@ -2067,19 +2463,20 @@ MailAPI.prototype = { | |
switch (slice._ns) { | |
case 'accounts': | |
- // Cache the first / default account. | |
- var firstItem = slice.items[0] && slice.items[0]._wireRep; | |
+ // Cache default account. | |
+ var defaultItem = slice.defaultAccount; | |
// Clear cache if no accounts or the first account has changed. | |
- if (!slice.items.length || this._recvCache.accountId !== firstItem.id) | |
+ if (!slice.items.length || | |
+ this._recvCache.accountId !== defaultItem.id) | |
this._resetCache(); | |
tempMsg = objCopy(msg); | |
tempMsg.howMany = 0; | |
tempMsg.index = 0; | |
- if (firstItem) { | |
- tempMsg.addItems = [firstItem]; | |
- this._recvCache.accountId = firstItem.id; | |
+ if (defaultItem) { | |
+ tempMsg.addItems = [defaultItem._wireRep]; | |
+ this._recvCache.accountId = defaultItem.id; | |
} | |
this._recvCache.accounts = tempMsg; | |
this._setHasAccounts(); | |
@@ -2448,6 +2845,12 @@ MailAPI.prototype = { | |
}, | |
_modifyAccount: function ma__modifyAccount(account, mods) { | |
+ if (mods.hasOwnProperty('setAsDefault')) { | |
+ // The order of accounts changed. The cache is now invalid. | |
+ this._resetCache(); | |
+ this._saveCache(); | |
+ } | |
+ | |
this.__bridgeSend({ | |
type: 'modifyAccount', | |
accountId: account.id, | |
@@ -2477,7 +2880,7 @@ MailAPI.prototype = { | |
*/ | |
viewAccounts: function ma_viewAccounts(realAccountsOnly) { | |
var handle = this._nextHandle++, | |
- slice = new BridgedViewSlice(this, 'accounts', handle); | |
+ slice = new AccountsViewSlice(this, handle); | |
this._slices[handle] = slice; | |
this.__bridgeSend({ | |
@@ -2749,7 +3152,7 @@ MailAPI.prototype = { | |
resolveEmailAddressToPeep: function(emailAddress, callback) { | |
var peep = ContactCache.resolvePeep({ name: null, address: emailAddress }); | |
- if (ContactCache.pendingLookups.length) | |
+ if (ContactCache.pendingLookupCount) | |
ContactCache.callbacks.push(callback.bind(null, peep)); | |
else | |
callback(peep); | |
@@ -2822,7 +3225,7 @@ MailAPI.prototype = { | |
msg.refSuid = options.replyTo.id; | |
msg.refDate = options.replyTo.date.valueOf(); | |
msg.refGuid = options.replyTo.guid; | |
- msg.refAuthor = options.replyTo.author.toJSON(); | |
+ msg.refAuthor = options.replyTo.author.toWireRep(); | |
msg.refSubject = options.replyTo.subject; | |
} | |
else if (options.hasOwnProperty('forwardOf') && options.forwardOf) { | |
@@ -2831,7 +3234,7 @@ MailAPI.prototype = { | |
msg.refSuid = options.forwardOf.id; | |
msg.refDate = options.forwardOf.date.valueOf(); | |
msg.refGuid = options.forwardOf.guid; | |
- msg.refAuthor = options.forwardOf.author.toJSON(); | |
+ msg.refAuthor = options.forwardOf.author.toWireRep(); | |
msg.refSubject = options.forwardOf.subject; | |
} | |
else { | |
@@ -3397,6 +3800,7 @@ define('mailapi/worker-support/devicestorage-main',[],function() { | |
dump('DeviceStorage: ' + str + '\n'); | |
} | |
+ | |
function save(uid, cmd, storage, blob, filename) { | |
var deviceStorage = navigator.getDeviceStorage(storage); | |
var req = deviceStorage.addNamed(blob, filename); | |
@@ -3405,8 +3809,15 @@ define('mailapi/worker-support/devicestorage-main',[],function() { | |
self.sendMessage(uid, cmd, [false, req.error.name]); | |
}; | |
- req.onsuccess = function() { | |
- self.sendMessage(uid, cmd, [true]); | |
+ req.onsuccess = function(e) { | |
+ var prefix = ''; | |
+ | |
+ if (typeof window.IS_GELAM_TEST !== 'undefined') { | |
+ prefix = 'TEST_PREFIX/'; | |
+ } | |
+ | |
+ // Bool success, String err, String filename | |
+ self.sendMessage(uid, cmd, [true, null, prefix + e.target.result]); | |
}; | |
} | |
@@ -3651,6 +4062,8 @@ function MailDB(testOptions, successCb, errorCb, upgradeCb) { | |
}; | |
var dbVersion = CUR_VERSION; | |
+ if (testOptions && testOptions.dbDelta) | |
+ dbVersion += testOptions.dbDelta; | |
if (testOptions && testOptions.dbVersion) | |
dbVersion = testOptions.dbVersion; | |
var openRequest = IndexedDB.open('b2g-email', dbVersion), self = this; | |
diff --git a/apps/email/js/ext/mailapi/worker-bootstrap.js b/apps/email/js/ext/mailapi/worker-bootstrap.js | |
index dd57083..bf03630 100644 | |
--- a/apps/email/js/ext/mailapi/worker-bootstrap.js | |
+++ b/apps/email/js/ext/mailapi/worker-bootstrap.js | |
@@ -1819,6 +1819,15 @@ define('event-queue',['require'],function (require) { | |
}); | |
define('microtime',['require'],function (require) { | |
+ // workers won't have this, of course... | |
+ if (window && window.performance && window.performance.now) { | |
+ return { | |
+ now: function () { | |
+ return window.performance.now() * 1000; | |
+ } | |
+ }; | |
+ } | |
+ | |
return { | |
now: function () { | |
return Date.now() * 1000; | |
@@ -3151,7 +3160,7 @@ LoggestClassMaker.prototype = { | |
throw new Error("Attempt to add expectations when already resolved!"); | |
var exp = [name]; | |
- for (var iArg = 0; iArg < numArgs; iArg++) { | |
+ for (var iArg = 0; iArg < arguments.length; iArg++) { | |
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION) { | |
exp.push(arguments[iArg]); | |
} | |
@@ -3284,7 +3293,7 @@ LoggestClassMaker.prototype = { | |
throw new Error("Attempt to add expectations when already resolved!"); | |
var exp = [name_begin]; | |
- for (var iArg = 0; iArg < numArgs; iArg++) { | |
+ for (var iArg = 0; iArg < arguments.length; iArg++) { | |
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION) | |
exp.push(arguments[iArg]); | |
} | |
@@ -3301,7 +3310,7 @@ LoggestClassMaker.prototype = { | |
throw new Error("Attempt to add expectations when already resolved!"); | |
var exp = [name_end]; | |
- for (var iArg = 0; iArg < numArgs; iArg++) { | |
+ for (var iArg = 0; iArg < arguments.length; iArg++) { | |
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION) | |
exp.push(arguments[iArg]); | |
} | |
@@ -3319,6 +3328,10 @@ LoggestClassMaker.prototype = { | |
return true; | |
}; | |
}, | |
+ /** | |
+ * Call like: loggedCall(logArg1, ..., logArgN, useAsThis, func, | |
+ * callArg1, ... callArgN); | |
+ */ | |
addCall: function(name, logArgs, testOnlyLogArgs) { | |
this._define(name, 'call'); | |
@@ -3514,7 +3527,7 @@ LoggestClassMaker.prototype = { | |
throw new Error("Attempt to add expectations when already resolved!"); | |
var exp = [name]; | |
- for (var iArg = 0; iArg < numArgs; iArg++) { | |
+ for (var iArg = 0; iArg < arguments.length; iArg++) { | |
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION) | |
exp.push(arguments[iArg]); | |
} | |
@@ -4605,12 +4618,17 @@ exports.INITIAL_SYNC_GROWTH_DAYS = 3; | |
/** | |
* What should be multiple the current number of sync days by when we perform | |
* a sync and don't find any messages? There are upper bounds in | |
- * `FolderStorage.onSyncCompleted` that cap this and there's more comments | |
- * there. | |
+ * `ImapFolderSyncer.onSyncCompleted` that cap this and there's more comments | |
+ * there. Note that we keep moving our window back as we go. | |
+ * | |
+ * This was 1.6 for a while, but it was proving to be a bit slow when the first | |
+ * messages start a ways back. Also, once we moved to just syncing headers | |
+ * without bodies, the cost of fetching more than strictly required went way | |
+ * down. | |
* | |
* IMAP only. | |
*/ | |
-exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 1.6; | |
+exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 2; | |
/** | |
* What is the furthest back in time we are willing to go? This is an | |
@@ -4625,6 +4643,23 @@ exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 1.6; | |
exports.OLDEST_SYNC_DATE = Date.UTC(1990, 0, 1); | |
/** | |
+ * Don't bother with iterative deepening if a folder has less than this many | |
+ * messages; just sync the whole thing. The trade-offs here are: | |
+ * | |
+ * - Not wanting to fetch more messages than we need. | |
+ * - Because header envelope fetches are done in a batch and IMAP servers like | |
+ * to sort UIDs from low-to-high, we will get the oldest messages first. | |
+ * This can be mitigated by having our sync logic use request windowing to | |
+ * offset this. | |
+ * - The time required to fetch the headers versus the time required to | |
+ * perform deepening. Because of network and disk I/O, deepening can take | |
+ * a very long time | |
+ * | |
+ * IMAP only. | |
+ */ | |
+exports.SYNC_WHOLE_FOLDER_AT_N_MESSAGES = 40; | |
+ | |
+/** | |
* If we issued a search for a date range and we are getting told about more | |
* than the following number of messages, we will try and reduce the date | |
* range proportionately (assuming a linear distribution) so that we sync | |
@@ -4634,7 +4669,7 @@ exports.OLDEST_SYNC_DATE = Date.UTC(1990, 0, 1); | |
* | |
* IMAP only. | |
*/ | |
-exports.BISECT_DATE_AT_N_MESSAGES = 50; | |
+exports.BISECT_DATE_AT_N_MESSAGES = 60; | |
/** | |
* What's the maximum number of messages we should ever handle in a go and | |
@@ -4762,6 +4797,10 @@ exports.SYNC_RANGE_ENUMS_TO_MS = { | |
* Testing support to adjust the value we use for the number of initial sync | |
* days. The tests are written with a value in mind (7), but 7 turns out to | |
* be too high an initial value for actual use, but is fine for tests. | |
+ * | |
+ * This started by taking human-friendly strings, but I changed to just using | |
+ * the constant names when I realized that consistency for grepping purposes | |
+ * would be a good thing. | |
*/ | |
exports.TEST_adjustSyncValues = function TEST_adjustSyncValues(syncValues) { | |
if (syncValues.hasOwnProperty('fillSize')) | |
@@ -4771,6 +4810,9 @@ exports.TEST_adjustSyncValues = function TEST_adjustSyncValues(syncValues) { | |
if (syncValues.hasOwnProperty('growDays')) | |
exports.INITIAL_SYNC_GROWTH_DAYS = syncValues.growDays; | |
+ if (syncValues.hasOwnProperty('SYNC_WHOLE_FOLDER_AT_N_MESSAGES')) | |
+ exports.SYNC_WHOLE_FOLDER_AT_N_MESSAGES = | |
+ syncValues.SYNC_WHOLE_FOLDER_AT_N_MESSAGES; | |
if (syncValues.hasOwnProperty('bisectThresh')) | |
exports.BISECT_DATE_AT_N_MESSAGES = syncValues.bisectThresh; | |
if (syncValues.hasOwnProperty('tooMany')) | |
@@ -5158,7 +5200,7 @@ MailSlice.prototype = { | |
}, | |
setStatus: function(status, requested, moreExpected, flushAccumulated, | |
- progress) { | |
+ progress, newEmailCount) { | |
if (!this._bridgeHandle) | |
return; | |
@@ -5181,14 +5223,16 @@ MailSlice.prototype = { | |
// XXX remove concat() once our bridge sending makes rep sharing | |
// impossible by dint of actual postMessage or JSON roundtripping. | |
this._bridgeHandle.sendSplice(0, 0, this.headers.concat(), | |
- requested, moreExpected); | |
+ requested, moreExpected, | |
+ newEmailCount); | |
// If we're no longer synchronizing, we want to update desiredHeaders | |
// to avoid accumulating extra 'desire'. | |
if (status !== 'synchronizing') | |
this.desiredHeaders = this.headers.length; | |
} | |
else { | |
- this._bridgeHandle.sendStatus(status, requested, moreExpected, progress); | |
+ this._bridgeHandle.sendStatus(status, requested, moreExpected, progress, | |
+ newEmailCount); | |
} | |
}, | |
@@ -5570,7 +5614,24 @@ MailSlice.prototype = { | |
* @key[flags @listof[String]] | |
* @key[hasAttachments Boolean] | |
* @key[subject String] | |
- * @key[snippet String] | |
+ * @key[snippet @oneof[ | |
+ * @case[null]{ | |
+ * We haven't tried to generate a snippet yet. | |
+ * } | |
+ * @case['']{ | |
+ * We tried to generate a snippet, but got nothing useful. Note that we | |
+ * may try and generate a snippet from a partial body fetch; this does not | |
+ * indicate that we should avoid computing a better snippet. Whenever the | |
+ * snippet is falsey and we have retrieved more body data, we should | |
+ * always try and derive a snippet. | |
+ * } | |
+ * @case[String]{ | |
+ * A non-empty string means we managed to produce some snippet data. It | |
+ * is still appropriate to regenerate the snippet if more body data is | |
+ * fetched since our snippet may be a fallback where we chose quoted text | |
+ * instead of authored text, etc. | |
+ * } | |
+ * ]] | |
* ]] | |
* @typedef[HeaderBlock @dict[ | |
* @key[ids @listof[ID]]{ | |
@@ -5848,6 +5909,13 @@ FolderStorage.prototype = { | |
}, | |
/** | |
+ * Function that we call with header whenever addMessageHeader gets called. | |
+ * @type {Function} | |
+ * @private | |
+ */ | |
+ _onAddingHeader: null, | |
+ | |
+ /** | |
* Reset all active slices. | |
*/ | |
resetAndRefreshActiveSlices: function() { | |
@@ -6374,7 +6442,7 @@ FolderStorage.prototype = { | |
// These variables let us detect if the deletion happened fully | |
// synchronously and thereby avoid blowing up the stack. | |
callActive = false, deleteTriggered = false; | |
- var deleteNextHeader = function deleteNextHeader() { | |
+ var deleteNextHeader = function() { | |
// if things are happening synchronously, bail out | |
if (callActive) { | |
deleteTriggered = true; | |
@@ -7032,16 +7100,19 @@ FolderStorage.prototype = { | |
// other synchronizations already in progress. | |
this._slices.push(slice); | |
- var doneCallback = function doneSyncCallback(err, reportSyncStatusAs) { | |
+ var doneCallback = function doneSyncCallback(err, reportSyncStatusAs, | |
+ moreExpected) { | |
if (!reportSyncStatusAs) { | |
if (err) | |
reportSyncStatusAs = 'syncfailed'; | |
else | |
reportSyncStatusAs = 'synced'; | |
} | |
+ if (moreExpected === undefined) | |
+ moreExpected = false; | |
slice.waitingOnData = false; | |
- slice.setStatus(reportSyncStatusAs, true, false, true); | |
+ slice.setStatus(reportSyncStatusAs, true, moreExpected, true); | |
this._curSyncSlice = null; | |
releaseMutex(); | |
@@ -7087,7 +7158,7 @@ FolderStorage.prototype = { | |
// blocked. We'll update it soon enough. | |
if (!this.folderSyncer.syncable) { | |
console.log('Synchronization is currently blocked; waiting...'); | |
- doneCallback(null, 'syncblocked'); | |
+ doneCallback(null, 'syncblocked', true); | |
return; | |
} | |
@@ -7104,6 +7175,7 @@ FolderStorage.prototype = { | |
} | |
this._curSyncSlice = slice; | |
}.bind(this); | |
+ | |
this.folderSyncer.initialSync( | |
slice, $sync.INITIAL_SYNC_DAYS, | |
syncCallback, doneCallback, progressCallback); | |
@@ -7346,7 +7418,8 @@ FolderStorage.prototype = { | |
if (!this._account.universe.online || | |
!this.folderSyncer.canGrowSync || | |
!userRequestsGrowth) { | |
- slice.sendEmptyCompletion(); | |
+ if (this.folderSyncer.syncable) | |
+ slice.sendEmptyCompletion(); | |
releaseMutex(); | |
return; | |
} | |
@@ -7430,13 +7503,33 @@ FolderStorage.prototype = { | |
// In the event we grow the startTS to the dawn of time, then we want | |
// to also provide the original startTS so that the bisection does not | |
// need to scan through years of empty space. | |
- origStartTS = null; | |
+ origStartTS = null, | |
+ // If we are refreshing through 'now', we will count the new messages we | |
+ // hear about and update this.newEmailCount once the sync completes. If | |
+ // we are performing any othe sync, the value will not be updated. | |
+ newEmailCount = null; | |
// - Grow endTS | |
// If the endTS lines up with the most recent known message for the folder, | |
// then remove the timestamp constraint so it goes all the way to now. | |
// OR if we just have no known messages | |
if (this.headerIsYoungestKnown(endTS, slice.endUID)) { | |
+ var prevTS = endTS; | |
+ newEmailCount = 0; | |
+ | |
+ /** | |
+ * Increment our new email count if the following conditions are met: | |
+ * 1. This header is younger than the youngest one before sync | |
+ * 2. and this hasn't already been seen. | |
+ * @param {HeaderInfo} header The header being added. | |
+ */ | |
+ this._onAddingHeader = function(header) { | |
+ if (SINCE(header.date, prevTS) && | |
+ (!header.flags || header.flags.indexOf('\\Seen') === -1)) { | |
+ newEmailCount += 1; | |
+ } | |
+ }.bind(this); | |
+ | |
endTS = null; | |
} | |
else { | |
@@ -7467,6 +7560,8 @@ FolderStorage.prototype = { | |
var doneCallback = function refreshDoneCallback(err, bisectInfo, | |
numMessages) { | |
+ this._onAddingHeader = null; | |
+ | |
var reportSyncStatusAs = 'synced'; | |
switch (err) { | |
case 'aborted': | |
@@ -7477,7 +7572,8 @@ FolderStorage.prototype = { | |
releaseMutex(); | |
slice.waitingOnData = false; | |
- slice.setStatus(reportSyncStatusAs, true, false); | |
+ slice.setStatus(reportSyncStatusAs, true, false, false, null, | |
+ newEmailCount); | |
return undefined; | |
}.bind(this); | |
@@ -8133,7 +8229,9 @@ FolderStorage.prototype = { | |
aranges.splice.apply(aranges, [newInfo[0], delCount].concat(insertions)); | |
- this.folderMeta.lastSyncedAt = endTS; | |
+ /*lastSyncedAt depends on current timestamp of the client device | |
+ should not be added timezone offset*/ | |
+ this.folderMeta.lastSyncedAt = NOW(); | |
if (this._account.universe) | |
this._account.universe.__notifyModifiedFolder(this._account, | |
this.folderMeta); | |
@@ -8433,6 +8531,9 @@ FolderStorage.prototype = { | |
slice.desiredHeaders++; | |
} | |
+ if (this._onAddingHeader !== null) { | |
+ this._onAddingHeader(header); | |
+ } | |
slice.onHeaderAdded(header, false, true); | |
} | |
} | |
@@ -9374,9 +9475,9 @@ BodyFilter.prototype = { | |
matchQuotes = this.matchQuotes, | |
idx; | |
- for (var iBodyRep = 0; iBodyRep < body.bodyReps.length; iBodyRep += 2) { | |
- var bodyType = body.bodyReps[iBodyRep], | |
- bodyRep = body.bodyReps[iBodyRep + 1]; | |
+ for (var iBodyRep = 0; iBodyRep < body.bodyReps.length; iBodyRep++) { | |
+ var bodyType = body.bodyReps[iBodyRep].type, | |
+ bodyRep = body.bodyReps[iBodyRep].content; | |
if (bodyType === 'plain') { | |
for (var iRep = 0; iRep < bodyRep.length && matches.length < stopAfter; | |
@@ -9946,7 +10047,7 @@ exports.local_do_modtags = function(op, doneCallback, undo) { | |
function() { | |
doneCallback(null, null, true); | |
}, | |
- null, | |
+ null, // connection loss does not happen for local-only ops | |
undo, | |
'modtags'); | |
}; | |
@@ -10058,7 +10159,7 @@ exports.local_do_move = function(op, doneCallback, targetFolderId) { | |
function() { | |
doneCallback(null, null, true); | |
}, | |
- null, | |
+ null, // connection loss does not happen for local-only ops | |
false, | |
'local move source'); | |
}; | |
@@ -10143,11 +10244,11 @@ exports.do_download = function(op, callback) { | |
function saveToStorage(blob, storage, filename, partInfo, isRetry) { | |
pendingStorageWrites++; | |
- var callback = function(success, error) { | |
+ var callback = function(success, error, savedFilename) { | |
if (success) { | |
self._LOG.savedAttachment(storage, blob.type, blob.size); | |
- console.log('saved attachment to', storage, filename, 'type:', blob.type); | |
- partInfo.file = [storage, filename]; | |
+ console.log('saved attachment to', storage, savedFilename, 'type:', blob.type); | |
+ partInfo.file = [storage, savedFilename]; | |
if (--pendingStorageWrites === 0) | |
done(); | |
} else { | |
@@ -10215,8 +10316,8 @@ exports.local_do_download = function(op, callback) { | |
}; | |
exports.check_download = function(op, callback) { | |
- // If we had download the file and persisted it successfully, this job would | |
- // be marked done because of the atomicity guarantee on our commits. | |
+ // If we downloaded the file and persisted it successfully, this job would be | |
+ // marked done because of the atomicity guarantee on our commits. | |
callback(null, 'coherent-notyet'); | |
}; | |
exports.local_undo_download = function(op, callback) { | |
@@ -10232,12 +10333,13 @@ exports.local_do_downloadBodies = function(op, callback) { | |
}; | |
exports.do_downloadBodies = function(op, callback) { | |
- var aggrErr; | |
+ var aggrErr, totalDownloaded = 0; | |
this._partitionAndAccessFoldersSequentially( | |
op.messages, | |
true, | |
function perFolder(folderConn, storage, headers, namers, callWhenDone) { | |
- folderConn.downloadBodies(headers, op.options, function(err) { | |
+ folderConn.downloadBodies(headers, op.options, function(err, numDownloaded) { | |
+ totalDownloaded += numDownloaded; | |
if (err && !aggrErr) { | |
aggrErr = err; | |
} | |
@@ -10245,7 +10347,9 @@ exports.do_downloadBodies = function(op, callback) { | |
}); | |
}, | |
function allDone() { | |
- callback(aggrErr, null, true); | |
+ callback(aggrErr, null, | |
+ // save if we might have done work. | |
+ totalDownloaded > 0); | |
}, | |
function deadConn() { | |
aggrErr = 'aborted-retry'; | |
@@ -10256,6 +10360,21 @@ exports.do_downloadBodies = function(op, callback) { | |
); | |
}; | |
+exports.check_downloadBodies = function(op, callback) { | |
+ // If we had downloaded the bodies and persisted them successfully, this job | |
+ // would be marked done because of the atomicity guarantee on our commits. It | |
+ // is possible this request might only be partially serviced, in which case we | |
+ // will avoid redundant body fetches, but redundant folder selection is | |
+ // possible if this request spans multiple folders. | |
+ callback(null, 'coherent-notyet'); | |
+}; | |
+ | |
+exports.check_downloadBodyReps = function(op, callback) { | |
+ // If we downloaded all of the body parts and persisted them successfully, | |
+ // this job would be marked done because of the atomicity guarantee on our | |
+ // commits. But it's not, so there's more to do. | |
+ callback(null, 'coherent-notyet'); | |
+}; | |
exports.do_downloadBodyReps = function(op, callback) { | |
var self = this; | |
@@ -10276,8 +10395,10 @@ exports.do_downloadBodyReps = function(op, callback) { | |
var gotHeader = function gotHeader(header) { | |
// header may have been deleted by the time we get here... | |
- if (!header) | |
- return callback(); | |
+ if (!header) { | |
+ callback(); | |
+ return; | |
+ } | |
folderConn.downloadBodyReps(header, onDownloadReps); | |
}; | |
@@ -10286,7 +10407,8 @@ exports.do_downloadBodyReps = function(op, callback) { | |
if (err) { | |
console.error('Error downloading reps', err); | |
// fail we cannot download for some reason? | |
- return callback('unknown'); | |
+ callback('unknown'); | |
+ return; | |
} | |
// success | |
@@ -10441,6 +10563,11 @@ exports.allJobsDone = function() { | |
* Its possible that entire folders will be skipped if no headers requested are | |
* now present. | |
* | |
+ * Connection loss by default causes this method to stop trying to traverse | |
+ * folders, calling callOnConnLoss and callWhenDone in that order. If you want | |
+ * to do something more clever, extend this method so that you can return a | |
+ * sentinel value or promise or something and do your clever thing. | |
+ * | |
* @args[ | |
* @param[messageNamers @listof[MessageNamer]] | |
* @param[needConn Boolean]{ | |
@@ -10461,8 +10588,19 @@ exports.allJobsDone = function() { | |
* @param[callWhenDoneWithFolder Function] | |
* ] | |
* ]] | |
- * @param[callWhenDone Function] | |
- * @param[callOnConnLoss Function] | |
+ * @param[callWhenDone @func[ | |
+ * @args[err @oneof[null 'connection-list']] | |
+ * ]]{ | |
+ * The function to invoke when all of the folders have been processed or the | |
+ * connection has been lost and we're giving up. This will be invoked after | |
+ * `callOnConnLoss` in the event of a conncetion loss. | |
+ * } | |
+ * @param[callOnConnLoss Function]{ | |
+ * This function we invoke when we lose a connection. Traditionally, you would | |
+ * use this to flag an error in your function that you would then return when | |
+ * we invoke `callWhenDone`. Then your check function will be invoked and you | |
+ * can laboriously check what actually happened on the server, etc. | |
+ * } | |
* @param[reverse #:optional Boolean]{ | |
* Should we walk the partitions in reverse order? | |
* } | |
@@ -10486,13 +10624,20 @@ exports._partitionAndAccessFoldersSequentially = function( | |
var partitions = $util.partitionMessagesByFolderId(allMessageNamers); | |
var folderConn, storage, self = this, | |
folderId = null, folderMessageNamers = null, serverIds = null, | |
- iNextPartition = 0, curPartition = null, modsToGo = 0; | |
+ iNextPartition = 0, curPartition = null, modsToGo = 0, | |
+ // Set to true immediately before calling callWhenDone; causes us to | |
+ // immediately bail out of any of our callbacks in order to avoid | |
+ // continuing beyond the point when we should have stopped. | |
+ terminated = false; | |
if (reverse) | |
partitions.reverse(); | |
var openNextFolder = function openNextFolder() { | |
+ if (terminated) | |
+ return; | |
if (iNextPartition >= partitions.length) { | |
+ terminated = true; | |
callWhenDone(null); | |
return; | |
} | |
@@ -10514,10 +10659,26 @@ exports._partitionAndAccessFoldersSequentially = function( | |
if (curPartition.folderId !== folderId) { | |
folderId = curPartition.folderId; | |
self._accessFolderForMutation(folderId, needConn, gotFolderConn, | |
- callOnConnLoss, label); | |
+ connDied, label); | |
} | |
}; | |
+ var connDied = function connDied() { | |
+ if (terminated) | |
+ return; | |
+ if (callOnConnLoss) { | |
+ try { | |
+ callOnConnLoss(); | |
+ } | |
+ catch (ex) { | |
+ self._LOG.callbackErr(ex); | |
+ } | |
+ } | |
+ terminated = true; | |
+ callWhenDone('connection-lost'); | |
+ }; | |
var gotFolderConn = function gotFolderConn(_folderConn, _storage) { | |
+ if (terminated) | |
+ return; | |
folderConn = _folderConn; | |
storage = _storage; | |
// - Get headers or resolve current server id from name map | |
@@ -10555,6 +10716,8 @@ exports._partitionAndAccessFoldersSequentially = function( | |
} | |
}; | |
var gotNeededHeaders = function gotNeededHeaders(headers) { | |
+ if (terminated) | |
+ return; | |
var iNextServerId = serverIds.indexOf(null); | |
for (var i = 0; i < headers.length; i++) { | |
var header = headers[i]; | |
@@ -10578,7 +10741,8 @@ exports._partitionAndAccessFoldersSequentially = function( | |
// skip entering this folder as the job cannot do anything with an empty | |
// header. | |
if (!serverIds.length) { | |
- return openNextFolder(); | |
+ openNextFolder(); | |
+ return; | |
} | |
try { | |
@@ -10590,10 +10754,13 @@ exports._partitionAndAccessFoldersSequentially = function( | |
} | |
}; | |
var gotHeaders = function gotHeaders(headers) { | |
+ if (terminated) | |
+ return; | |
// its unlikely but entirely possible that all pending headers have been | |
// removed somehow between when the job was queued and now. | |
if (!headers.length) { | |
- return openNextFolder(); | |
+ openNextFolder(); | |
+ return; | |
} | |
// Sort the headers in ascending-by-date order so that slices hear about | |
@@ -11524,6 +11691,7 @@ define('mailapi/mailbridge', | |
'rdcommon/log', | |
'./util', | |
'./mailchew-strings', | |
+ './date', | |
'require', | |
'module', | |
'exports' | |
@@ -11532,6 +11700,7 @@ define('mailapi/mailbridge', | |
$log, | |
$imaputil, | |
$mailchewStrings, | |
+ $date, | |
require, | |
$module, | |
exports | |
@@ -11776,6 +11945,14 @@ MailBridge.prototype = { | |
case 'syncRange': | |
accountDef.syncRange = val; | |
break; | |
+ | |
+ case 'setAsDefault': | |
+ // Weird things can happen if the device's clock goes back in time, | |
+ // but this way, at least the user can change their default if they | |
+ // cycle through their accounts. | |
+ if (val) | |
+ accountDef.defaultPriority = $date.NOW(); | |
+ break; | |
} | |
} | |
this.universe.saveAccountDef(accountDef, null); | |
@@ -12654,9 +12831,11 @@ function SliceBridgeProxy(bridge, ns, handle) { | |
SliceBridgeProxy.prototype = { | |
/** | |
* Issue a splice to add and remove items. | |
+ * @param {number} newEmailCount Number of new emails synced during this | |
+ * slice request. | |
*/ | |
sendSplice: function sbp_sendSplice(index, howMany, addItems, requested, | |
- moreExpected) { | |
+ moreExpected, newEmailCount) { | |
this._bridge.__sendMessage({ | |
type: 'sliceSplice', | |
handle: this._handle, | |
@@ -12671,6 +12850,7 @@ SliceBridgeProxy.prototype = { | |
atBottom: this.atBottom, | |
userCanGrowUpwards: this.userCanGrowUpwards, | |
userCanGrowDownwards: this.userCanGrowDownwards, | |
+ newEmailCount: newEmailCount, | |
}); | |
}, | |
@@ -12685,12 +12865,16 @@ SliceBridgeProxy.prototype = { | |
}); | |
}, | |
+ /** | |
+ * @param {number} newEmailCount Number of new emails synced during this | |
+ * slice request. | |
+ */ | |
sendStatus: function sbp_sendStatus(status, requested, moreExpected, | |
- progress) { | |
+ progress, newEmailCount) { | |
this.status = status; | |
if (progress != null) | |
this.progress = progress; | |
- this.sendSplice(0, 0, [], requested, moreExpected); | |
+ this.sendSplice(0, 0, [], requested, moreExpected, newEmailCount); | |
}, | |
sendSyncProgress: function(progress) { | |
@@ -14457,9 +14641,9 @@ MailUniverse.prototype = { | |
endings: 'transparent' | |
}); | |
var filename = 'gem-log-' + Date.now() + '.json'; | |
- sendMessage('save', ['sdcard', blob, filename], function(success) { | |
+ sendMessage('save', ['sdcard', blob, filename], function(success, err, savedFile) { | |
if (success) | |
- console.log('saved log to "sdcard" devicestorage:', filename); | |
+ console.log('saved log to "sdcard" devicestorage:', savedFile); | |
else | |
console.error('failed to save log to', filename); | |
@@ -14703,6 +14887,12 @@ MailUniverse.prototype = { | |
saveAccountDef: function(accountDef, folderInfo) { | |
this._db.saveAccountDef(this.config, accountDef, folderInfo); | |
+ var account = this.getAccountForAccountId(accountDef.id); | |
+ | |
+ // If account exists, notify of modification. However on first | |
+ // save, the account does not exist yet. | |
+ if (account) | |
+ this.__notifyModifiedAccount(account); | |
}, | |
/** | |
@@ -15492,7 +15682,7 @@ MailUniverse.prototype = { | |
type: 'downloadBodies', | |
longtermId: 'session', // don't persist this job. | |
lifecycle: 'do', | |
- localStatus: null, | |
+ localStatus: 'done', | |
serverStatus: null, | |
tryCount: 0, | |
humanOp: 'downloadBodies', | |
diff --git a/apps/email/js/folder-cards.js b/apps/email/js/folder-cards.js | |
index 494f3d0..81a7270 100644 | |
--- a/apps/email/js/folder-cards.js | |
+++ b/apps/email/js/folder-cards.js | |
@@ -136,8 +136,8 @@ FolderPickerCard.prototype = { | |
self.updateFolderDom(folder, true); | |
foldersContainer.insertBefore(folderNode, insertBuddy); | |
- if (this.mostRecentSyncTimestamp < folder.lastSyncedAt) { | |
- this.mostRecentSyncTimestamp = folder.lastSyncedAt; | |
+ if (self.mostRecentSyncTimestamp < folder.lastSyncedAt) { | |
+ self.mostRecentSyncTimestamp = folder.lastSyncedAt; | |
dirtySyncTime = true; | |
} | |
}); | |
diff --git a/apps/email/js/mail-app.js b/apps/email/js/mail-app.js | |
index 25f1f7a..b3ca1b8 100644 | |
--- a/apps/email/js/mail-app.js | |
+++ b/apps/email/js/mail-app.js | |
@@ -95,7 +95,7 @@ var App = { | |
// XXX: Because we don't have unified account now, we should switch to | |
// the latest account which user just added. | |
var account = showLatest ? acctsSlice.items.slice(-1)[0] : | |
- acctsSlice.items[0]; | |
+ acctsSlice.defaultAccount; | |
var foldersSlice = MailAPI.viewFolders('account', account); | |
foldersSlice.oncomplete = function() { | |
diff --git a/apps/email/js/marquee.js b/apps/email/js/marquee.js | |
new file mode 100644 | |
index 0000000..c4f714f | |
--- /dev/null | |
+++ b/apps/email/js/marquee.js | |
@@ -0,0 +1,125 @@ | |
+'use strict'; | |
+ | |
+/** | |
+ * HTML Marquee in JavaScript/CSS | |
+ * - slow scrolling of text depending on `behavior' and `timingFunction' | |
+ * parameters provided to the `activate' method | |
+ * - start aligned left with delay (see marquee.css for details on classes) | |
+ * | |
+ * Creates a HTML element of the form: | |
+ * <headerNode> | |
+ * <div id="marquee-h-wrapper"> | |
+ * <div id="marquee-h-text" class="marquee"> | |
+ * Marqueed text content | |
+ * </div> | |
+ * </div> | |
+ * </headerNode> | |
+ * where 'headerNode' is the node that will containt the text that needs a | |
+ * marquee (i.e. <header>, <h1>, etc.). | |
+ */ | |
+var Marquee = { | |
+ /** | |
+ * List of supported timing functions | |
+ */ | |
+ timingFunction: ['linear', 'ease'], | |
+ | |
+ /** | |
+ * Setup the marquee DOM structure | |
+ * @param {string} text the string of text that requires a marquee. | |
+ * @param {element} headerNode the DOM element parent of the text. | |
+ */ | |
+ setup: function marquee_setup(text, headerNode) { | |
+ this._headerNode = headerNode; | |
+ | |
+ this._headerWrapper = document.getElementById('marquee-h-wrapper'); | |
+ if (!this._headerWrapper) { | |
+ this._headerWrapper = document.createElement('div'); | |
+ this._headerWrapper.id = 'marquee-h-wrapper'; | |
+ this._headerNode.appendChild(this._headerWrapper); | |
+ } | |
+ | |
+ var headerText = document.getElementById('marquee-h-text'); | |
+ if (!headerText) { | |
+ headerText = document.createElement('div'); | |
+ headerText.id = 'marquee-h-text'; | |
+ this._headerWrapper.appendChild(headerText); | |
+ } | |
+ | |
+ headerText.textContent = text; | |
+ }, | |
+ | |
+ /** | |
+ * Activate the marquee | |
+ * NOTE: This should only be called once the DOM structure is updated with | |
+ * Marquee.setup() and all created DOM elements are appended to the | |
+ * document, otherwise the text overflow check will not work properly. | |
+ * @param {string} behavior the way the marquee behaves: 'scroll' (default) | |
+ * for continuous right-to-left (rtl) scrolling or | |
+ * 'alternate' for alternating right-to-left and | |
+ * left-to-right scrolling. | |
+ * @param {string} timingFun the animation timing function: 'linear' (default) | |
+ * for linear animation speed, or 'ease' for slow | |
+ * start of the animation. | |
+ */ | |
+ activate: function marquee_activate(behavior, timingFun) { | |
+ if (!this._headerNode || !this._headerWrapper) | |
+ return; | |
+ | |
+ // Set defaults for arguments | |
+ var mode = behavior || 'scroll'; | |
+ var tf = timingFun || null; | |
+ var timing = (Marquee.timingFunction.indexOf(tf) >= 0) ? tf : 'linear'; | |
+ var marqueeCssClass = 'marquee'; | |
+ | |
+ var titleText = document.getElementById('marquee-h-text'); | |
+ | |
+ // Check if the title text overflows, and if so, add the marquee class | |
+ // NOTE: this can only be checked it the DOM structure is updated | |
+ // through Marquee.setup() | |
+ if (this._headerWrapper.clientWidth < this._headerWrapper.scrollWidth) { | |
+ // Track the CSS classes added to the text | |
+ this._marqueeCssClassList = []; | |
+ switch (mode) { | |
+ case 'scroll': | |
+ var cssClass = marqueeCssClass + '-rtl'; | |
+ // Set the width of the marquee to match the text contents length | |
+ var width = this._headerWrapper.scrollWidth; | |
+ titleText.style.width = width + 'px'; | |
+ // Start the marquee animation (aligned left with delay) | |
+ titleText.classList.add(cssClass + '-start-' + timing); | |
+ this._marqueeCssClassList.push(cssClass + '-start-' + timing); | |
+ | |
+ var self = this; | |
+ titleText.addEventListener('animationend', function() { | |
+ titleText.classList.remove(cssClass + '-start-' + timing); | |
+ this._marqueeCssClassList.pop(); | |
+ // Correctly calculate the width of the marquee | |
+ var visibleWidth = self._headerWrapper.clientWidth + 'px'; | |
+ titleText.style.transform = 'translateX(' + visibleWidth + ')'; | |
+ // Enable the continuous marquee | |
+ titleText.classList.add(cssClass); | |
+ this._marqueeCssClassList.push(cssClass); | |
+ }); | |
+ break; | |
+ case 'alternate': | |
+ var cssClass = marqueeCssClass + '-alt-'; | |
+ // Set the width of the marquee to match the text contents length | |
+ var width = | |
+ this._headerWrapper.scrollWidth - this._headerWrapper.clientWidth; | |
+ titleText.style.width = width + 'px'; | |
+ | |
+ // Start the marquee animation (aligned left with delay) | |
+ titleText.classList.add(cssClass + timing); | |
+ break; | |
+ } | |
+ } else { | |
+ if (!this._marqueeCssClassList) | |
+ return; | |
+ // Remove the active marquee CSS classes | |
+ for (var cssClass in this._marqueeCssClassList) | |
+ titleText.classList.remove(cssClass); | |
+ | |
+ titleText.style.transform = ''; | |
+ } | |
+ } | |
+}; | |
diff --git a/apps/email/js/message-cards.js b/apps/email/js/message-cards.js | |
index 7b4a237..db44b92 100755 | |
--- a/apps/email/js/message-cards.js | |
+++ b/apps/email/js/message-cards.js | |
@@ -213,6 +213,12 @@ MessageListCard.prototype = { | |
*/ | |
PROGRESS_CANDYBAR_TIMEOUT_MS: 2000, | |
+ /** | |
+ * @type {MessageListTopbar} | |
+ * @private | |
+ */ | |
+ _topbar: null, | |
+ | |
postInsert: function() { | |
this._hideSearchBoxByScrolling(); | |
@@ -538,7 +544,11 @@ MessageListCard.prototype = { | |
this.toolbar.searchBtn.classList.remove('disabled'); | |
}, | |
- onSliceRequestComplete: function() { | |
+ | |
+ /** | |
+ * @param {number=} newEmailCount Optional number of new messages. | |
+ */ | |
+ onSliceRequestComplete: function(newEmailCount) { | |
// We always want our logic to fire, but complete auto-clears before firing. | |
this.messagesSlice.oncomplete = this._boundSliceRequestComplete; | |
@@ -552,12 +562,31 @@ MessageListCard.prototype = { | |
if (this.messagesSlice.items.length === 0 && !this.messagesSlice._fake) { | |
this.showEmptyLayout(); | |
} | |
+ | |
+ if (newEmailCount && newEmailCount !== NaN && newEmailCount !== 0) { | |
+ // Decorate or update the little notification bar that tells the user | |
+ // how many new emails they've received after a sync. | |
+ if (this._topbar && this._topbar.getElement() !== null) { | |
+ // Update the existing status bar. | |
+ this._topbar.updateNewEmailCount(newEmailCount); | |
+ } else { | |
+ this._topbar = new MessageListTopbar( | |
+ this.scrollContainer, newEmailCount); | |
+ | |
+ var el = | |
+ document.getElementsByClassName(MessageListTopbar.CLASS_NAME)[0]; | |
+ this._topbar.decorate(el); | |
+ this._topbar.render(); | |
+ } | |
+ } | |
+ | |
// Consider requesting more data or discarding data based on scrolling that | |
// has happened since we issued the request. (While requests were pending, | |
// onScroll ignored scroll events.) | |
this._onScroll(null); | |
}, | |
+ | |
onScroll: function(evt) { | |
if (this._pendingScrollEvent) { | |
return; | |
@@ -829,6 +858,10 @@ MessageListCard.prototype = { | |
} | |
}, | |
+ _updatePeepDom: function(peep) { | |
+ peep.element.textContent = peep.name || peep.address; | |
+ }, | |
+ | |
updateMessageDom: function(firstTime, message) { | |
var msgNode = message.element; | |
@@ -850,8 +883,10 @@ MessageListCard.prototype = { | |
listPerson = message.author; | |
// author | |
- msgNode.getElementsByClassName('msg-header-author')[0] | |
- .textContent = listPerson.name || listPerson.address; | |
+ listPerson.element = | |
+ msgNode.getElementsByClassName('msg-header-author')[0]; | |
+ listPerson.onchange = this._updatePeepDom; | |
+ listPerson.onchange(listPerson); | |
// date | |
dateNode.dataset.time = message.date.valueOf(); | |
dateNode.textContent = prettyDate(message.date); | |
@@ -898,10 +933,15 @@ MessageListCard.prototype = { | |
if (firstTime) { | |
// author | |
var authorNode = msgNode.getElementsByClassName('msg-header-author')[0]; | |
- if (matches.author) | |
+ if (matches.author) { | |
appendMatchItemTo(matches.author, authorNode); | |
- else | |
- authorNode.textContent = message.author.name || message.author.address; | |
+ } | |
+ else { | |
+ // we can only update the name if it wasn't matched on. | |
+ message.author.element = authorNode; | |
+ message.author.onchange = this._updatePeepDom; | |
+ message.author.onchange(message.author); | |
+ } | |
// date | |
dateNode.dataset.time = message.date.valueOf(); | |
@@ -1111,7 +1151,7 @@ var MAX_QUOTE_CLASS_NAME = 'msg-body-qmax'; | |
function MessageReaderCard(domNode, mode, args) { | |
this.domNode = domNode; | |
- this.header = args.header; | |
+ this.header = args.header.makeCopy(); | |
// The body elements for the (potentially multiple) iframes we created to hold | |
// HTML email content. | |
this.htmlBodyNodes = []; | |
@@ -1227,11 +1267,40 @@ MessageReaderCard.prototype = { | |
}, | |
onForward: function(event) { | |
- Cards.eatEventsUntilNextCard(); | |
- var composer = this.header.forwardMessage('inline', function() { | |
- Cards.pushCard('compose', 'default', 'animate', | |
- { composer: composer }); | |
- }); | |
+ // If we don't have a body yet, we can't forward the message. In the future | |
+ // we should visibly disable the button until the body has been retrieved. | |
+ if (!this.body) | |
+ return; | |
+ | |
+ var needToPrompt = this.header.hasAttachments || | |
+ this.body.embeddedImageCount > 0; | |
+ | |
+ var forwardMessage = function() { | |
+ Cards.eatEventsUntilNextCard(); | |
+ var composer = this.header.forwardMessage('inline', function() { | |
+ Cards.pushCard('compose', 'default', 'animate', | |
+ { composer: composer }); | |
+ }); | |
+ }.bind(this); | |
+ | |
+ if (needToPrompt) { | |
+ var dialog = msgNodes['attachment-disabled-confirm'].cloneNode(true); | |
+ ConfirmDialog.show(dialog, | |
+ { | |
+ id: 'msg-attachment-disabled-ok', | |
+ handler: function() { | |
+ forwardMessage(); | |
+ } | |
+ }, | |
+ { | |
+ id: 'msg-attachment-disabled-cancel', | |
+ handler: null | |
+ } | |
+ ); | |
+ } else { | |
+ forwardMessage(); | |
+ } | |
+ | |
}, | |
onDelete: function() { | |
@@ -1285,51 +1354,17 @@ MessageReaderCard.prototype = { | |
onPeepClick: function(target) { | |
var contents = msgNodes['contact-menu'].cloneNode(true); | |
- var email = target.dataset.address; | |
- var contact = null; | |
- contents.getElementsByTagName('header')[0].textContent = email; | |
- document.body.appendChild(contents); | |
- | |
- /* | |
- * Show menu items based on the options which consists of values of | |
- * the type "_contextMenuType". | |
- */ | |
- var showContextMenuItems = (function(options) { | |
- if (options & this._contextMenuType.VIEW_CONTACT) | |
- contents.querySelector('.msg-contact-menu-view') | |
- .classList.remove('collapsed'); | |
- if (options & this._contextMenuType.CREATE_CONTACT) | |
- contents.querySelector('.msg-contact-menu-create-contact') | |
- .classList.remove('collapsed'); | |
- if (options & this._contextMenuType.ADD_TO_CONTACT) | |
- contents.querySelector('.msg-contact-menu-add-to-existing-contact') | |
- .classList.remove('collapsed'); | |
- if (options & this._contextMenuType.REPLY) | |
- contents.querySelector('.msg-contact-menu-reply') | |
- .classList.remove('collapsed'); | |
- if (options & this._contextMenuType.NEW_MESSAGE) | |
- contents.querySelector('.msg-contact-menu-new') | |
- .classList.remove('collapsed'); | |
- }).bind(this); | |
- | |
- var updateName = (function(targetMail, name) { | |
- if (!name || name === '') | |
- return; | |
+ var peep = target.peep; | |
+ var headerNode = contents.getElementsByTagName('header')[0]; | |
+ // Setup the marquee structure | |
+ Marquee.setup(peep.address, headerNode); | |
- // update UI | |
- var selector = '.msg-peep-bubble[data-address="' + | |
- targetMail + '"]'; | |
- var nodes = Array.prototype.slice | |
- .call(this.domNode.querySelectorAll(selector)); | |
- | |
- for (var i = 0; i < nodes.length; i++) { | |
- var node = nodes[i]; | |
- var content = node.querySelector('.msg-peep-content'); | |
- node.dataset.name = name; | |
- content.textContent = name; | |
- } | |
- }).bind(this); | |
+ // Activate marquee once the contents DOM are added to document | |
+ document.body.appendChild(contents); | |
+ // XXX Remove 'ease' if linear animation is wanted | |
+ Marquee.activate('alternate', 'ease'); | |
+ // -- context menu selection handling | |
var formSubmit = (function(evt) { | |
document.body.removeChild(contents); | |
switch (evt.explicitOriginalTarget.className) { | |
@@ -1339,34 +1374,35 @@ MessageReaderCard.prototype = { | |
var composer = | |
MailAPI.beginMessageComposition(this.header, null, null, | |
function composerReady() { | |
+ // XXX future work to just let us pass peeps directly; will | |
+ // require normalization when sending things over the wire to the | |
+ // backend. | |
composer.to = [{ | |
- address: target.dataset.address, | |
- name: target.dataset.name | |
+ address: peep.address, | |
+ name: peep.name | |
}]; | |
Cards.pushCard('compose', 'default', 'animate', | |
{ composer: composer }); | |
}); | |
break; | |
case 'msg-contact-menu-view': | |
- if (contact) { | |
- var activity = new MozActivity({ | |
- name: 'open', | |
- data: { | |
- type: 'webcontacts/contact', | |
- params: { | |
- 'id': contact.id | |
- } | |
+ var activity = new MozActivity({ | |
+ name: 'open', | |
+ data: { | |
+ type: 'webcontacts/contact', | |
+ params: { | |
+ 'id': peep.contactId | |
} | |
- }); | |
- } | |
+ } | |
+ }); | |
break; | |
case 'msg-contact-menu-create-contact': | |
var params = { | |
- 'email': email | |
+ 'email': peep.address | |
}; | |
- if (name) | |
- params['givenName'] = target.dataset.name; | |
+ if (peep.name) | |
+ params['givenName'] = peep.name; | |
var activity = new MozActivity({ | |
name: 'new', | |
@@ -1375,12 +1411,8 @@ MessageReaderCard.prototype = { | |
params: params | |
} | |
}); | |
- | |
- activity.onsuccess = function() { | |
- var contact = activity.result.contact; | |
- if (contact) | |
- updateName(email, contact.name); | |
- }; | |
+ // since we already have contact change listeners that are hooked up | |
+ // to the UI, we leave it up to them to update the UI for us. | |
break; | |
case 'msg-contact-menu-add-to-existing-contact': | |
var activity = new MozActivity({ | |
@@ -1388,16 +1420,12 @@ MessageReaderCard.prototype = { | |
data: { | |
type: 'webcontacts/contact', | |
params: { | |
- 'email': email | |
+ 'email': peep.address | |
} | |
} | |
}); | |
- | |
- activity.onsuccess = function() { | |
- var contact = activity.result.contact; | |
- if (contact) | |
- updateName(email, contact.name); | |
- }; | |
+ // since we already have contact change listeners that are hooked up | |
+ // to the UI, we leave it up to them to update the UI for us. | |
break; | |
case 'msg-contact-menu-reply': | |
//TODO: We need to enter compose view with specific email address. | |
@@ -1411,23 +1439,36 @@ MessageReaderCard.prototype = { | |
}).bind(this); | |
contents.addEventListener('submit', formSubmit); | |
- ContactDataManager.searchContactData(email, function(contacts) { | |
- var contextMenuOptions = this._contextMenuType.NEW_MESSAGE; | |
- var messageType = target.dataset.type; | |
- if (messageType === 'from') | |
- contextMenuOptions |= this._contextMenuType.REPLY; | |
+ // -- populate context menu | |
+ var contextMenuOptions = this._contextMenuType.NEW_MESSAGE; | |
+ var messageType = peep.type; | |
- if (contacts && contacts.length > 0) { | |
- contact = contacts[0]; | |
- contextMenuOptions |= this._contextMenuType.VIEW_CONTACT; | |
- } else { | |
- contact = null; | |
- contextMenuOptions |= this._contextMenuType.CREATE_CONTACT; | |
- contextMenuOptions |= this._contextMenuType.ADD_TO_CONTACT; | |
- } | |
- showContextMenuItems(contextMenuOptions); | |
- }.bind(this)); | |
+ if (messageType === 'from') | |
+ contextMenuOptions |= this._contextMenuType.REPLY; | |
+ | |
+ if (peep.isContact) { | |
+ contextMenuOptions |= this._contextMenuType.VIEW_CONTACT; | |
+ } else { | |
+ contextMenuOptions |= this._contextMenuType.CREATE_CONTACT; | |
+ contextMenuOptions |= this._contextMenuType.ADD_TO_CONTACT; | |
+ } | |
+ | |
+ if (contextMenuOptions & this._contextMenuType.VIEW_CONTACT) | |
+ contents.querySelector('.msg-contact-menu-view') | |
+ .classList.remove('collapsed'); | |
+ if (contextMenuOptions & this._contextMenuType.CREATE_CONTACT) | |
+ contents.querySelector('.msg-contact-menu-create-contact') | |
+ .classList.remove('collapsed'); | |
+ if (contextMenuOptions & this._contextMenuType.ADD_TO_CONTACT) | |
+ contents.querySelector('.msg-contact-menu-add-to-existing-contact') | |
+ .classList.remove('collapsed'); | |
+ if (contextMenuOptions & this._contextMenuType.REPLY) | |
+ contents.querySelector('.msg-contact-menu-reply') | |
+ .classList.remove('collapsed'); | |
+ if (contextMenuOptions & this._contextMenuType.NEW_MESSAGE) | |
+ contents.querySelector('.msg-contact-menu-new') | |
+ .classList.remove('collapsed'); | |
}, | |
onLoadBarClick: function(event) { | |
@@ -1599,6 +1640,28 @@ MessageReaderCard.prototype = { | |
var header = this.header, body = this.body; | |
// -- Header | |
+ function updatePeep(peep) { | |
+ var nameNode = peep.element.getElementsByClassName('msg-peep-content')[0]; | |
+ | |
+ if (peep.type === 'from') { | |
+ // We display the sender of the message's name in the header and the | |
+ // address in the bubble. | |
+ domNode.getElementsByClassName('msg-reader-header-label')[0] | |
+ .textContent = peep.name || peep.address; | |
+ | |
+ nameNode.textContent = peep.address; | |
+ nameNode.classList.add('msg-peep-address'); | |
+ } | |
+ else { | |
+ nameNode.textContent = peep.name || peep.address; | |
+ if (!peep.name && peep.address) { | |
+ nameNode.classList.add('msg-peep-address'); | |
+ } else { | |
+ nameNode.classList.remove('msg-peep-address'); | |
+ } | |
+ } | |
+ } | |
+ | |
function addHeaderEmails(type, peeps) { | |
var lineClass = 'msg-envelope-' + type + '-line'; | |
var lineNode = domNode.getElementsByClassName(lineClass)[0]; | |
@@ -1610,40 +1673,16 @@ MessageReaderCard.prototype = { | |
// Because we can avoid having to do multiple selector lookups, we just | |
// mutate the template in-place... | |
- var peepTemplate = msgNodes['peep-bubble'], | |
- contentTemplate = | |
- peepTemplate.getElementsByClassName('msg-peep-content')[0]; | |
- | |
- // If the address field is "From", We only show the address and display | |
- // name in the message header. | |
- if (lineClass == 'msg-envelope-from-line') { | |
- var peep = peeps[0]; | |
- // TODO: Display peep name if the address is not exist. | |
- // Do we nee to deal with that scenario? | |
- contentTemplate.textContent = peep.name || peep.address; | |
- peepTemplate.dataset.address = peep.address; | |
- peepTemplate.dataset.name = peep.name; | |
- peepTemplate.dataset.type = type; | |
- if (peep.address) { | |
- contentTemplate.classList.add('msg-peep-address'); | |
- } | |
- lineNode.appendChild(peepTemplate.cloneNode(true)); | |
- domNode.getElementsByClassName('msg-reader-header-label')[0] | |
- .textContent = peep.name || peep.address; | |
- return; | |
- } | |
+ var peepTemplate = msgNodes['peep-bubble']; | |
+ | |
for (var i = 0; i < peeps.length; i++) { | |
var peep = peeps[i]; | |
- contentTemplate.textContent = peep.name || peep.address; | |
- peepTemplate.dataset.address = peep.address; | |
- peepTemplate.dataset.name = peep.name; | |
- peepTemplate.dataset.type = type; | |
- if (!peep.name && peep.address) { | |
- contentTemplate.classList.add('msg-peep-address'); | |
- } else { | |
- contentTemplate.classList.remove('msg-peep-address'); | |
- } | |
- lineNode.appendChild(peepTemplate.cloneNode(true)); | |
+ peep.type = type; | |
+ peep.element = peepTemplate.cloneNode(true); | |
+ peep.element.peep = peep; | |
+ peep.onchange = updatePeep; | |
+ updatePeep(peep); | |
+ lineNode.appendChild(peep.element); | |
} | |
} | |
@@ -1776,6 +1815,12 @@ MessageReaderCard.prototype = { | |
}, | |
die: function() { | |
+ // Our header was makeCopy()d from the message-list and so needs to be | |
+ // explicitly removed since it is not part of a slice. | |
+ if (this.header) { | |
+ this.header.__die(); | |
+ this.header = null; | |
+ } | |
if (this.body) { | |
this.body.die(); | |
this.body = null; | |
diff --git a/apps/email/js/message_list_topbar.js b/apps/email/js/message_list_topbar.js | |
new file mode 100644 | |
index 0000000..34c2a23 | |
--- /dev/null | |
+++ b/apps/email/js/message_list_topbar.js | |
@@ -0,0 +1,164 @@ | |
+ | |
+/** | |
+ * @fileoverview This file provides a MessageListTopbar which is | |
+ * a little notification bar that tells the user | |
+ * how many new emails they've received after a sync. | |
+ */ | |
+ | |
+ | |
+/** | |
+ * @constructor | |
+ * @param {Element} scrollContainer Element containing folder messages. | |
+ * @param {number} newEmailCount The number of new messages we received. | |
+ */ | |
+function MessageListTopbar(scrollContainer, newEmailCount) { | |
+ this._scrollContainer = scrollContainer; | |
+ this._newEmailCount = newEmailCount; | |
+} | |
+ | |
+ | |
+/** | |
+ * @const {string} | |
+ */ | |
+MessageListTopbar.CLASS_NAME = 'msg-list-topbar'; | |
+ | |
+ | |
+/** | |
+ * Number of milliseconds after which the status bar should disappear. | |
+ * @const {number} | |
+ */ | |
+MessageListTopbar.DISAPPEARS_AFTER_MILLIS = 5000; | |
+ | |
+ | |
+MessageListTopbar.prototype = { | |
+ /** | |
+ * @type {Element} | |
+ * @private | |
+ */ | |
+ _el: null, | |
+ | |
+ | |
+ /** | |
+ * @type {Element} | |
+ * @private | |
+ */ | |
+ _scrollContainer: null, | |
+ | |
+ | |
+ /** | |
+ * @type {number} | |
+ * @private | |
+ */ | |
+ _newEmailCount: 0, | |
+ | |
+ | |
+ /** | |
+ * Update the div with the correct new email count and | |
+ * listen to it for mouse clicks. | |
+ * @param {Element} el The div we'll decorate. | |
+ */ | |
+ decorate: function(el) { | |
+ el.addEventListener('click', this._onclick.bind(this)); | |
+ this._el = el; | |
+ this.updateNewEmailCount(); | |
+ return this._el; | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * Show our element and set a timer to destroy it after | |
+ * DISAPPEARS_AFTER_MILLIS. | |
+ */ | |
+ render: function() { | |
+ this._el.classList.remove('collapsed'); | |
+ setTimeout( | |
+ this.destroy.bind(this), | |
+ MessageListTopbar.DISAPPEARS_AFTER_MILLIS | |
+ ); | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * Release the element and any event listeners and cleanup our data. | |
+ */ | |
+ destroy: function() { | |
+ if (this._el) { | |
+ this._el.removeEventListener('click', this._onclick.bind(this)); | |
+ this._el.classList.add('collapsed'); | |
+ this._el.textContent = ''; | |
+ this._el = null; | |
+ } | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * @return {Element} Our underlying element. | |
+ */ | |
+ getElement: function() { | |
+ return this._el; | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * @param {number} newEmailCount Optional number of new messages we received. | |
+ */ | |
+ updateNewEmailCount: function(newEmailCount) { | |
+ if (newEmailCount !== undefined) { | |
+ this._newEmailCount += newEmailCount; | |
+ } | |
+ | |
+ if (this._el !== null) { | |
+ this._el.textContent = | |
+ navigator.mozL10n.get('new-emails', { n: this._newEmailCount }); | |
+ } | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * @type {Event} evt Some mouseclick event. | |
+ */ | |
+ _onclick: function(evt) { | |
+ this.destroy(); | |
+ | |
+ var scrollTop = this._scrollContainer.scrollTop; | |
+ var dest = this._getScrollDestination(); | |
+ if (scrollTop <= dest) { | |
+ return; | |
+ } | |
+ | |
+ this._scrollUp(this._scrollContainer, dest, 0, 50); | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * Move the element up to the specified position over time by increments. | |
+ * @param {Element} el Some element to scroll. | |
+ * @param {number} dest The eventual scrollTop value. | |
+ * @param {number} timeout How long to wait between each time. | |
+ * @param {number} inc How far to scroll each time. | |
+ * @private | |
+ */ | |
+ _scrollUp: function(el, dest, timeout, inc) { | |
+ var next = el.scrollTop - inc; | |
+ if (dest >= next) { | |
+ // This is the last scroll. | |
+ el.scrollTop = dest; | |
+ return; | |
+ } | |
+ | |
+ el.scrollTop = next; | |
+ setTimeout(this._scrollUp.bind(this, el, dest, timeout, inc), timeout); | |
+ }, | |
+ | |
+ | |
+ /** | |
+ * @return {number} The point where we should scroll to calculated from | |
+ * the search box height. | |
+ * @private | |
+ */ | |
+ _getScrollDestination: function() { | |
+ var searchBar = | |
+ document.getElementsByClassName('msg-search-tease-bar')[0]; | |
+ return searchBar.offsetHeight; | |
+ } | |
+}; | |
diff --git a/apps/email/js/setup-cards.js b/apps/email/js/setup-cards.js | |
index 7c94ea8..4b08712 100644 | |
--- a/apps/email/js/setup-cards.js | |
+++ b/apps/email/js/setup-cards.js | |
@@ -648,6 +648,28 @@ function SettingsAccountCard(domNode, mode, args) { | |
domNode.getElementsByClassName('tng-account-delete')[0] | |
.addEventListener('click', this.onDelete.bind(this), false); | |
+ // Handle default account checkbox. If already a default, then the checkbox | |
+ // cannot be unchecked. The default is changed by going to an account that | |
+ // is not the default and checking that checkbox. | |
+ var defaultLabelNode = domNode.getElementsByClassName('tng-default-label')[0]; | |
+ var defaultInputNode = domNode.getElementsByClassName('tng-default-input')[0]; | |
+ if (this.account.isDefault) { | |
+ defaultInputNode.disabled = true; | |
+ defaultInputNode.checked = true; | |
+ } else { | |
+ | |
+ defaultLabelNode.addEventListener('click', function(evt) { | |
+ evt.stopPropagation(); | |
+ evt.preventBubble(); | |
+ | |
+ if (!defaultInputNode.disabled) { | |
+ defaultInputNode.disabled = true; | |
+ defaultInputNode.checked = true; | |
+ this.account.modifyAccount({ setAsDefault: true }); | |
+ } | |
+ }.bind(this), false); | |
+ } | |
+ | |
// ActiveSync, IMAP and SMTP are protocol names, no need to be localized | |
domNode.getElementsByClassName('tng-account-type')[0].textContent = | |
(this.account.type === 'activesync') ? 'ActiveSync' : 'IMAP+SMTP'; | |
diff --git a/apps/email/locales/email.en-US.properties b/apps/email/locales/email.en-US.properties | |
index 72956fa..14c4aaa 100644 | |
--- a/apps/email/locales/email.en-US.properties | |
+++ b/apps/email/locales/email.en-US.properties | |
@@ -187,7 +187,10 @@ compose-discard-message=Discard email? | |
compose-discard-confirm=Discard | |
compose-sending-message=Sending email | |
compose-send-message-failed=Sending email failed | |
- | |
+composer-attachment-large=Attachment too large | |
+composer-attachments-large=Attachments too large | |
+compose-attchment-size-exceeded=The selected attachment is too large to send with this message. | |
+compose-attchments-size-exceeded=The selected attachments are too large to send with this message. Try selecting fewer files. | |
dialog-button-ok=OK | |
attachment-size-kib={{kilobytes}}K | |
@@ -204,6 +207,8 @@ message-edit-menu-move=Move | |
message-edit-menu-delete=Delete | |
message-edit-delete-confirm=Delete email message? | |
+message-send-attachment-disabled-confirm=Attachments can not be forwarded. | |
+ | |
message-multiedit-header={[ plural(n) ]} | |
message-multiedit-header[zero] = Edit | |
message-multiedit-header[one] = {{ n }} selected | |
@@ -329,3 +334,13 @@ toaster-retryable-syncfailed=Unable to connect to server | |
form-clear-input=Remove text | |
confirm-dialog-title=Confirmation | |
+ | |
+# new-emails refers to the text shown on a notification that users | |
+# get when they receive new, unread email. The ui component that utilizes | |
+# this lives in apps/email/js/message_list_topbar.js. | |
+new-emails={[ plural(n) ]} | |
+new-emails[one]=1 New Email | |
+new-emails[two]={{ n }} New Emails | |
+new-emails[few]={{ n }} New Emails | |
+new-emails[many]={{ n }} New Emails | |
+new-emails[other]={{ n }} New Emails | |
diff --git a/apps/email/style/marquee.css b/apps/email/style/marquee.css | |
new file mode 100644 | |
index 0000000..8d2abc7 | |
--- /dev/null | |
+++ b/apps/email/style/marquee.css | |
@@ -0,0 +1,62 @@ | |
+#marquee-h-wrapper { | |
+ position: absolute; | |
+ overflow: hidden; | |
+ white-space: nowrap; | |
+ line-height: normal; | |
+ /* Subtract padding-left and amount of padding desired on the right side */ | |
+ width: calc(100% - 3rem - 2rem); | |
+} | |
+ | |
+/* --- Right to left (RTL) marquee styles --- */ | |
+.marquee-rtl-start-linear { | |
+ transform: translateX(0); | |
+ /* 12s duration of animation: 0% -> -100%; | |
+ * 3s start delay */ | |
+ animation: left-marquee 12s linear 3s 1; | |
+} | |
+ | |
+.marquee-rtl-start-ease { | |
+ transform: translateX(0); | |
+ /* 12s duration of animation: 0% -> -100%; | |
+ * 3s start delay */ | |
+ animation: left-marquee 12s ease-in 3s 1; | |
+} | |
+ | |
+.marquee-rtl { | |
+ /* 20s duration of animation: +100% -> -100%; */ | |
+ animation: left-marquee 20s linear 0s infinite; | |
+} | |
+ | |
+@keyframes left-marquee { | |
+ 100% { | |
+ transform: translateX(-100%); | |
+ } | |
+} | |
+ | |
+/* --- Alternate (alt: RTL <-> LTR) marquee styles --- */ | |
+.marquee-alt-linear { | |
+ transform: translateX(0); | |
+ animation: alt-marquee 15s linear 0s infinite; | |
+} | |
+ | |
+.marquee-alt-ease { | |
+ transform: translateX(0); | |
+ animation: alt-marquee 15s ease-in-out 0s infinite; | |
+} | |
+ | |
+@keyframes alt-marquee { | |
+ /* 3s delay in start (1.5s = 10%) */ | |
+ 20% { | |
+ transform: translateX(0); | |
+ } | |
+ 50% { | |
+ transform: translateX(-100%); | |
+ } | |
+ /* 3s delay when aligned right */ | |
+ 70% { | |
+ transform: translateX(-100%); | |
+ } | |
+ 100% { | |
+ transform: translateX(0); | |
+ } | |
+} | |
diff --git a/apps/email/style/message-cards.css b/apps/email/style/message-cards.css | |
index 88428f0..91faf7d 100644 | |
--- a/apps/email/style/message-cards.css | |
+++ b/apps/email/style/message-cards.css | |
@@ -517,14 +517,15 @@ input.msg-search-text-tease { | |
margin: 0 3rem; | |
} | |
-/* Filters selected state */ | |
-ul[role="tablist"].filter li[aria-selected="true"] { | |
- background: #fff; | |
- border: solid 1px #999; | |
- border-top: none; | |
- color: #000; | |
-} | |
- | |
-ul[role="tablist"].filter li[aria-selected="true"] a { | |
- color: #000; | |
+.msg-list-topbar { | |
+ background-color: #00aacc; | |
+ color: white; | |
+ font-size: 1.2rem; | |
+ left: 0; | |
+ line-height: 2.2rem; | |
+ padding: 0.4rem 3rem 0.4rem 3rem; | |
+ position: absolute; | |
+ top: 5rem; | |
+ width: 100%; | |
+ z-index: 10; | |
} | |
diff --git a/apps/email/style/setup-cards.css b/apps/email/style/setup-cards.css | |
index d49f309..b28e7ea 100644 | |
--- a/apps/email/style/setup-cards.css | |
+++ b/apps/email/style/setup-cards.css | |
@@ -47,7 +47,9 @@ input:focus:invalid { | |
color: black; | |
font-size: 1.4rem; | |
line-height: 1.2rem; | |
- margin: 1.5rem 3rem; | |
+ padding: 1.5rem; | |
+ margin-left: 1.5rem; | |
+ background: none; /* Overrides UA :active background-color */ | |
} | |
.sup-form-link { | |
diff --git a/apps/email/test/unit/message_list_topbar_test.js b/apps/email/test/unit/message_list_topbar_test.js | |
new file mode 100644 | |
index 0000000..9795373 | |
--- /dev/null | |
+++ b/apps/email/test/unit/message_list_topbar_test.js | |
@@ -0,0 +1,135 @@ | |
+require('/shared/js/l10n.js'); | |
+ | |
+requireApp('email/js/message_list_topbar.js'); | |
+requireApp('email/test/unit/mock_l10n.js'); | |
+ | |
+suite('MessageListTopbar', function() { | |
+ var subject, el, scrollContainer, newEmailCount, nativeMozL10n; | |
+ | |
+ setup(function() { | |
+ nativeMozL10n = navigator.mozL10n; | |
+ navigator.mozL10n = MockL10n; | |
+ // Mock the HTML | |
+ el = document.createElement('div'); | |
+ el.classList.add(MessageListTopbar.CLASS_NAME); | |
+ el.classList.add('collapsed'); | |
+ | |
+ scrollContainer = {}; | |
+ newEmailCount = 5; | |
+ subject = new MessageListTopbar(scrollContainer, newEmailCount); | |
+ }); | |
+ | |
+ teardown(function() { | |
+ navigator.mozL10n = nativeMozL10n; | |
+ }); | |
+ | |
+ suite('#constructor', function() { | |
+ test('should have been initialized appropriately', function() { | |
+ assert.deepEqual(subject._scrollContainer, scrollContainer); | |
+ assert.deepEqual(subject._newEmailCount, newEmailCount); | |
+ }); | |
+ }); | |
+ | |
+ suite('#decorate', function() { | |
+ var spy; | |
+ | |
+ setup(function() { | |
+ spy = sinon.spy(subject, 'updateNewEmailCount'); | |
+ }); | |
+ | |
+ teardown(function() { | |
+ subject.updateNewEmailCount.restore(); | |
+ }); | |
+ | |
+ test('should set _el appropriately', function() { | |
+ subject.decorate(el); | |
+ assert.equal(subject._el, el); | |
+ }); | |
+ | |
+ test('should update its new email count appropriately', function() { | |
+ subject.decorate(el); | |
+ sinon.assert.calledOnce(spy); | |
+ }); | |
+ | |
+ test('should set a click listener on the element', function() { | |
+ sinon.stub(subject, '_onclick'); | |
+ subject.decorate(el); | |
+ el.click(); | |
+ sinon.assert.calledOnce(subject._onclick); | |
+ subject._onclick.restore(); | |
+ }); | |
+ }); | |
+ | |
+ suite('#render', function() { | |
+ setup(function() { | |
+ subject.decorate(el); | |
+ }); | |
+ | |
+ test('should show _el', function() { | |
+ assert.equal(el.classList.contains('collapsed'), true); | |
+ subject.render(); | |
+ assert.equal(el.classList.contains('collapsed'), false); | |
+ }); | |
+ | |
+ test('should call destroy after DISAPPEARS_AFTER_MILLIS', function(done) { | |
+ MessageListTopbar.DISAPPEARS_AFTER_MILLIS = 0; | |
+ sinon.stub(subject, 'destroy', function() { | |
+ sinon.assert.calledOnce(subject.destroy); | |
+ subject.destroy.restore(); | |
+ done(); | |
+ }); | |
+ | |
+ subject.render(); | |
+ }); | |
+ }); | |
+ | |
+ suite('#destroy', function() { | |
+ setup(function() { | |
+ subject.decorate(el); | |
+ subject.render(); | |
+ }); | |
+ | |
+ test('should hide _el', function() { | |
+ assert.equal(el.classList.contains('collapsed'), false); | |
+ subject.destroy(); | |
+ assert.equal(el.classList.contains('collapsed'), true); | |
+ }); | |
+ | |
+ test('should empty the element', function() { | |
+ assert.notEqual(el.textContent, ''); | |
+ subject.destroy(); | |
+ assert.equal(el.textContent, ''); | |
+ }); | |
+ | |
+ test('should release its internal data', function() { | |
+ assert.notEqual(subject._el, undefined); | |
+ subject.destroy(); | |
+ assert.equal(subject._el, undefined); | |
+ }); | |
+ | |
+ test('should remove the click listener on _el', function() { | |
+ sinon.stub(subject, '_onclick'); | |
+ subject.destroy(); | |
+ el.click(); | |
+ sinon.assert.notCalled(subject._onclick); | |
+ }); | |
+ }); | |
+ | |
+ suite('#updateNewEmailCount', function() { | |
+ test('should update email count', function() { | |
+ assert.equal(subject._newEmailCount, 5); | |
+ subject.updateNewEmailCount(10); | |
+ assert.equal(subject._newEmailCount, 15); | |
+ }); | |
+ | |
+ test('should update textContent', function() { | |
+ subject._el = el; | |
+ assert.equal(subject._el.textContent, ''); | |
+ subject.updateNewEmailCount(); | |
+ assert.equal( | |
+ subject._el.textContent, | |
+ navigator.mozL10n.get('new-emails', { n: 5 }) | |
+ ); | |
+ }); | |
+ }); | |
+}); | |
diff --git a/apps/email/test/unit/mock_l10n.js b/apps/email/test/unit/mock_l10n.js | |
new file mode 100644 | |
index 0000000..6c582aa | |
--- /dev/null | |
+++ b/apps/email/test/unit/mock_l10n.js | |
@@ -0,0 +1,25 @@ | |
+(function(exports) { | |
+ 'use strict'; | |
+ | |
+ function DateTimeFormat() { | |
+ this.mInitialized = true; | |
+ } | |
+ DateTimeFormat.prototype = { | |
+ localeFormat: function mockLocaleFormat(time, strFormat) { | |
+ return '' + time; | |
+ } | |
+ }; | |
+ | |
+ var MockL10n = { | |
+ get: function get(key, params) { | |
+ if (params) { | |
+ return key + JSON.stringify(params); | |
+ } | |
+ return key; | |
+ }, | |
+ DateTimeFormat: DateTimeFormat | |
+ }; | |
+ | |
+ exports.MockL10n = MockL10n; | |
+ | |
+}(this)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment