Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
544
gaia/apps/email/js/sync.js
Executable file
544
gaia/apps/email/js/sync.js
Executable file
|
|
@ -0,0 +1,544 @@
|
|||
/*jshint browser: true */
|
||||
/*global define, console */
|
||||
'use strict';
|
||||
define(function(require) {
|
||||
|
||||
var cronSyncStartTime,
|
||||
appSelf = require('app_self'),
|
||||
evt = require('evt'),
|
||||
mozL10n = require('l10n!'),
|
||||
notificationHelper = require('shared/js/notification_helper');
|
||||
|
||||
// Version marker for the notification data format. It is a string because
|
||||
// query_string only deals in strings. If the format of the notification data
|
||||
// changes, then this version needs to be changed.
|
||||
var notificationDataVersion = '1';
|
||||
|
||||
// The expectation is that this module is called as part of model's
|
||||
// init process that calls the "model_init" module to finish its construction.
|
||||
return function syncInit(model, api) {
|
||||
var hasBeenVisible = !document.hidden,
|
||||
waitingOnCron = {};
|
||||
|
||||
// Let the back end know the app is interactive, not just
|
||||
// a quick sync and shutdown case, so that it knows it can
|
||||
// do extra work.
|
||||
if (hasBeenVisible) {
|
||||
api.setInteractive();
|
||||
}
|
||||
|
||||
// If the page is ever not hidden, then do not close it later.
|
||||
document.addEventListener('visibilitychange',
|
||||
function onVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
hasBeenVisible = true;
|
||||
api.setInteractive();
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Creates a string key from an array of string IDs. Uses a space
|
||||
// separator since that cannot show up in an ID.
|
||||
function makeAccountKey(accountIds) {
|
||||
return 'id' + accountIds.join(' ');
|
||||
}
|
||||
|
||||
var sendNotification;
|
||||
if (typeof Notification !== 'function') {
|
||||
console.log('email: notifications not available');
|
||||
sendNotification = function() {};
|
||||
} else {
|
||||
sendNotification = function(notificationId, titleL10n, bodyL10n,
|
||||
iconUrl, data, behavior) {
|
||||
console.log('Notification sent for ' + notificationId);
|
||||
|
||||
if (Notification.permission !== 'granted') {
|
||||
console.log('email: notification skipped, permission: ' +
|
||||
Notification.permission);
|
||||
return;
|
||||
}
|
||||
|
||||
data = data || {};
|
||||
|
||||
//TODO: consider setting dir and lang?
|
||||
//https://developer.mozilla.org/en-US/docs/Web/API/notification
|
||||
var notificationOptions = {
|
||||
bodyL10n: bodyL10n,
|
||||
icon: iconUrl,
|
||||
tag: notificationId,
|
||||
data: data,
|
||||
mozbehavior: {
|
||||
noscreen: true
|
||||
},
|
||||
closeOnClick: false
|
||||
};
|
||||
|
||||
if (behavior) {
|
||||
Object.keys(behavior).forEach(function(key) {
|
||||
notificationOptions.mozbehavior[key] = behavior[key];
|
||||
});
|
||||
}
|
||||
|
||||
notificationHelper.send(titleL10n, notificationOptions)
|
||||
.then(function(notification){
|
||||
// If the app is open, but in the background, when the notification
|
||||
// comes in, then we do not get notifived via our
|
||||
// mozSetMessageHandler that is set elsewhere. Instead need to
|
||||
// listen to click event and synthesize an "event" ourselves.
|
||||
notification.onclick = function() {
|
||||
evt.emit('notification', {
|
||||
clicked: true,
|
||||
imageURL: iconUrl,
|
||||
tag: notificationId,
|
||||
data: data
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
api.oncronsyncstart = function(accountIds) {
|
||||
console.log('email oncronsyncstart: ' + accountIds);
|
||||
cronSyncStartTime = Date.now();
|
||||
var accountKey = makeAccountKey(accountIds);
|
||||
waitingOnCron[accountKey] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches notification data for the notification type, ntype. This method
|
||||
* assumes there is only one ntype of notification per account.
|
||||
* @param {String} ntype The notification type, like 'sync'.
|
||||
* @return {Promise} Promise that resolves to a an object whose keys
|
||||
* are account IDs and values are notification data.
|
||||
*/
|
||||
function fetchNotificationsData(ntype) {
|
||||
if (typeof Notification !== 'function' || !Notification.get) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
return Notification.get().then(function(notifications) {
|
||||
var result = {};
|
||||
notifications.forEach(function(notification) {
|
||||
var data = notification.data;
|
||||
|
||||
// Want to avoid unexpected data formats. So if not a version match
|
||||
// then just close it since it cannot be processed as expected. This
|
||||
// means that notifications not generated by this module may be
|
||||
// closed. However, ideally only this module generates notifications,
|
||||
// for localization of concerns.
|
||||
if (!data.v || data.v !== notificationDataVersion) {
|
||||
notification.close();
|
||||
} else if (data.ntype === ntype) {
|
||||
data.notification = notification;
|
||||
result[data.accountId] = data;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, function(err) {
|
||||
// Do not care about errors, just log and keep going.
|
||||
console.error('email notification.get call failed: ' + err);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to just get some environment data for dealing with sync-based
|
||||
* notfication data. Exists to reduce the curly brace pyramid of doom and
|
||||
* to normalize existing sync notification info.
|
||||
* @param {Function} fn function to call once env info is fetched.
|
||||
*/
|
||||
function getSyncEnv(fn) {
|
||||
appSelf.latest('self', function(app) {
|
||||
model.latestOnce('account', function(currentAccount) {
|
||||
fetchNotificationsData('sync').then(
|
||||
function(existingNotificationsData) {
|
||||
mozL10n.formatValue('senders-separation-sign')
|
||||
.then(function(separator) {
|
||||
var localized = {
|
||||
separator
|
||||
};
|
||||
mozL10n.formatValue('notification-no-subject')
|
||||
.then(function(noSubject) {
|
||||
localized.noSubject = noSubject;
|
||||
fn(app, currentAccount, existingNotificationsData, localized);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of unique top names sorted by most recent sender first,
|
||||
* and limited to a max number. The max number is just to limit amount of
|
||||
* work and likely display limits.
|
||||
* @param {Array} latestInfos array of result.latestMessageInfos. Modifies
|
||||
* result.latestMessageInfos via a sort.
|
||||
* @param {Array} oldFromNames old from names from a previous notification.
|
||||
* @return {Array} a maxFromList array of most recent senders.
|
||||
*/
|
||||
function topUniqueFromNames(latestInfos, oldFromNames) {
|
||||
var names = [],
|
||||
maxCount = 3;
|
||||
|
||||
// Get the new from senders from the result. First,
|
||||
// need to sort by most recent.
|
||||
// Note that sort modifies result.latestMessageInfos
|
||||
latestInfos.sort(function(a, b) {
|
||||
return b.date - a.date;
|
||||
});
|
||||
|
||||
// Only need three unique names, and just the name, not
|
||||
// the full info object.
|
||||
latestInfos.some(function(info) {
|
||||
if (names.length > maxCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (names.indexOf(info.from) === -1) {
|
||||
names.push(info.from);
|
||||
}
|
||||
});
|
||||
|
||||
// Now add in old names to fill out a list of
|
||||
// max names.
|
||||
oldFromNames.some(function(name) {
|
||||
if (names.length > maxCount) {
|
||||
return true;
|
||||
}
|
||||
if (names.indexOf(name) === -1) {
|
||||
names.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/*
|
||||
accountsResults is an object with the following structure:
|
||||
accountIds: array of string account IDs.
|
||||
updates: array of objects includes properties:
|
||||
id: accountId,
|
||||
name: account name,
|
||||
count: number of new messages total
|
||||
latestMessageInfos: array of latest message info objects,
|
||||
with properties:
|
||||
- from
|
||||
- subject
|
||||
- accountId
|
||||
- messageSuid
|
||||
*/
|
||||
api.oncronsyncstop = function(accountsResults) {
|
||||
console.log('email oncronsyncstop: ' + accountsResults.accountIds);
|
||||
|
||||
function finishSync() {
|
||||
evt.emit('cronSyncStop', accountsResults.accountIds);
|
||||
|
||||
// Mark this accountId set as no longer waiting.
|
||||
var accountKey = makeAccountKey(accountsResults.accountIds);
|
||||
waitingOnCron[accountKey] = false;
|
||||
var stillWaiting = Object.keys(waitingOnCron).some(function(key) {
|
||||
return !!waitingOnCron[key];
|
||||
});
|
||||
|
||||
if (!hasBeenVisible && !stillWaiting) {
|
||||
console.log('sync completed in ' +
|
||||
((Date.now() - cronSyncStartTime) / 1000) +
|
||||
' seconds, closing mail app');
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
// If no sync updates, wrap it up.
|
||||
if (!accountsResults.updates) {
|
||||
finishSync();
|
||||
return;
|
||||
}
|
||||
|
||||
// There are sync updates, get environment and figure out how to notify
|
||||
// the user of the updates.
|
||||
getSyncEnv(function(
|
||||
app, currentAccount, existingNotificationsData, localized) {
|
||||
var iconUrl = notificationHelper.getIconURI(app);
|
||||
|
||||
accountsResults.updates.forEach(function(result) {
|
||||
// If the current account is being shown, then just send an update
|
||||
// to the model to indicate new messages, as the notification will
|
||||
// happen within the app for that case. The 'inboxShown' pathway
|
||||
// will be sure to close any existing notification for the current
|
||||
// account.
|
||||
if (currentAccount.id === result.id && !document.hidden) {
|
||||
model.notifyInboxMessages(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this account does not want notifications of new messages
|
||||
// or if no Notification object, stop doing work.
|
||||
if (!model.getAccount(result.id).notifyOnNew ||
|
||||
typeof Notification !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataObject, subjectL10n, bodyL10n, behavior,
|
||||
count = result.count,
|
||||
oldFromNames = [];
|
||||
|
||||
// Adjust counts/fromNames based on previous notification.
|
||||
var existingData = existingNotificationsData[result.id];
|
||||
if (existingData) {
|
||||
if (existingData.count) {
|
||||
count += parseInt(existingData.count, 10);
|
||||
}
|
||||
if (existingData.fromNames) {
|
||||
oldFromNames = existingData.fromNames;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 1) {
|
||||
// Multiple messages were synced.
|
||||
// topUniqueFromNames modifies result.latestMessageInfos
|
||||
var newFromNames = topUniqueFromNames(result.latestMessageInfos,
|
||||
oldFromNames);
|
||||
dataObject = {
|
||||
v: notificationDataVersion,
|
||||
ntype: 'sync',
|
||||
type: 'message_list',
|
||||
accountId: result.id,
|
||||
count: count,
|
||||
fromNames: newFromNames
|
||||
};
|
||||
|
||||
// If already have a notification, then do not bother with sound or
|
||||
// vibration for this update. Longer term, the notification standard
|
||||
// will have a "silent" option, but using a non-existent URL as
|
||||
// suggested in bug 1042361 in the meantime.
|
||||
if (existingData && existingData.count) {
|
||||
behavior = {
|
||||
soundFile: 'does-not-exist-to-simulate-silent',
|
||||
// Cannot use 0 since system/js/notifications.js explicitly
|
||||
// ignores [0] values. [1] is good enough for this purpose.
|
||||
vibrationPattern: [1]
|
||||
};
|
||||
}
|
||||
|
||||
if (model.getAccountCount() === 1) {
|
||||
subjectL10n = {
|
||||
id: 'new-emails-notify-one-account',
|
||||
args: { n: count }
|
||||
};
|
||||
} else {
|
||||
subjectL10n = {
|
||||
id: 'new-emails-notify-multiple-accounts',
|
||||
args: {
|
||||
n: count,
|
||||
accountName: result.address
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bodyL10n = { raw: newFromNames.join(localized.separator) };
|
||||
|
||||
} else {
|
||||
// Only one message to notify about.
|
||||
var info = result.latestMessageInfos[0];
|
||||
dataObject = {
|
||||
v: notificationDataVersion,
|
||||
ntype: 'sync',
|
||||
type: 'message_reader',
|
||||
accountId: info.accountId,
|
||||
messageSuid: info.messageSuid,
|
||||
count: 1,
|
||||
fromNames: [info.from]
|
||||
};
|
||||
|
||||
var rawSubject = info.subject || localized.noSubject;
|
||||
|
||||
if (model.getAccountCount() === 1) {
|
||||
subjectL10n = { raw: rawSubject };
|
||||
bodyL10n = { raw: info.from };
|
||||
} else {
|
||||
subjectL10n = {
|
||||
id: 'new-emails-notify-multiple-accounts',
|
||||
args: {
|
||||
n: count,
|
||||
accountName: result.address
|
||||
}
|
||||
};
|
||||
bodyL10n = {
|
||||
id: 'new-emails-notify-multiple-accounts-body',
|
||||
args: {
|
||||
from: info.from,
|
||||
subject: rawSubject
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(
|
||||
result.id,
|
||||
subjectL10n,
|
||||
bodyL10n,
|
||||
iconUrl,
|
||||
dataObject,
|
||||
behavior
|
||||
);
|
||||
});
|
||||
|
||||
finishSync();
|
||||
});
|
||||
};
|
||||
|
||||
// Background Send Notifications
|
||||
|
||||
var BACKGROUND_SEND_NOTIFICATION_ID = 'backgroundSendFailed';
|
||||
var sentAudio = null; // Lazy-loaded when first needed
|
||||
|
||||
/**
|
||||
* The API passes through background send notifications with the
|
||||
* following data (see the "sendOutboxMessages" job and/or
|
||||
* `GELAM/js/jobs/outbox.js`):
|
||||
*
|
||||
* @param {int} accountId
|
||||
* @param {string} suid
|
||||
* SUID of the message
|
||||
* @param {string} state
|
||||
* 'pending', 'syncing', 'success', or 'error'
|
||||
* @param {string} err
|
||||
* (if applicable, otherwise null)
|
||||
* @param {array} badAddresses
|
||||
* (if applicable)
|
||||
* @param {int} sendFailures
|
||||
* Count of the number of times the message failed to send.
|
||||
* @param {Boolean} emitNotifications
|
||||
* True if this message is being sent as a direct result of
|
||||
* the user sending a message from the compose window. False
|
||||
* otherwise, as in when the user "refreshes" the outbox.
|
||||
* @param {Boolean} willSendMore
|
||||
* True if we will send a subsequent message from the outbox
|
||||
* immediately after sending this message.
|
||||
*
|
||||
* Additionally, this function appends the following to that
|
||||
* structured data:
|
||||
*
|
||||
* @param {string} localizedDescription Notification text.
|
||||
*
|
||||
* If the application is in the foreground, we notify the user on
|
||||
* both success and failure. If the application is in the
|
||||
* background, we only post a system notifiaction on failure.
|
||||
*/
|
||||
api.onbackgroundsendstatus = function(data) {
|
||||
console.log('outbox: Message', data.suid, 'status =', JSON.stringify({
|
||||
state: data.state,
|
||||
err: data.err,
|
||||
sendFailures: data.sendFailures,
|
||||
emitNotifications: data.emitNotifications
|
||||
}));
|
||||
|
||||
// Grab an appropriate localized string here. This description
|
||||
// may be displayed in a number of different places, so it's
|
||||
// cleaner to do the localization here.
|
||||
|
||||
var descId;
|
||||
switch (data.state) {
|
||||
case 'pending': descId = 'background-send-pending'; break;
|
||||
case 'sending': descId = 'background-send-sending'; break;
|
||||
case 'success': descId = 'background-send-success'; break;
|
||||
case 'error':
|
||||
if ((data.badAddresses && data.badAddresses.length) ||
|
||||
data.err === 'bad-recipient') {
|
||||
descId = 'background-send-error-recipients';
|
||||
} else {
|
||||
descId = 'background-send-error';
|
||||
}
|
||||
break;
|
||||
case 'syncDone':
|
||||
// We will not display any notification for a 'syncDone'
|
||||
// message, except to stop refresh icons from spinning. No
|
||||
// need to attempt to populate a description.
|
||||
break;
|
||||
default:
|
||||
console.error('No state description for background send state "' +
|
||||
data.state + '"');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message sent successfuly, and we're sending this as a
|
||||
// side-effect of the user hitting "send" on the compose screen,
|
||||
// (i.e. emitNotifications is true), we may need to play a sound.
|
||||
if (data.state === 'success') {
|
||||
// Grab an up-to-date reading of the "play sound on send"
|
||||
// preference to decide if we're going to play a sound or not.
|
||||
model.latestOnce('acctsSlice', function() {
|
||||
var account = model.getAccount(data.accountId);
|
||||
if (!account) {
|
||||
console.error('Invalid account ID', data.accountId,
|
||||
'for a background send notification.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If email is in the background, we should still be able to
|
||||
// play audio due to having the 'audio-channel-notification'
|
||||
// permission (unless higher priority audio is playing).
|
||||
|
||||
// TODO: As of June 2014, this behavior is still in limbo;
|
||||
// see the following links for relevant discussion. We may
|
||||
// need to follow up to ensure we get the behavior we want
|
||||
// (which is to play a sound when possible, even if we're in
|
||||
// the background).
|
||||
// Thread on dev-gaia: http://goo.gl/l6REZy
|
||||
// AUDIO_COMPETING bugs: https://bugzil.la/911238
|
||||
if (account.playSoundOnSend) {
|
||||
if (!sentAudio) {
|
||||
sentAudio = new Audio('/sounds/firefox_sent.opus');
|
||||
sentAudio.mozAudioChannelType = 'notification';
|
||||
}
|
||||
sentAudio.play();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// If we are in the foreground, notify through the model, which
|
||||
// will display an in-app toast notification when appropriate.
|
||||
if (!document.hidden) {
|
||||
mozL10n.formatValue(descId).then(function(localizedDescription) {
|
||||
data.localizedDescription = localizedDescription;
|
||||
model.notifyBackgroundSendStatus(data);
|
||||
});
|
||||
}
|
||||
// Otherwise, notify with a system notification in the case of
|
||||
// an error. By design, we don't use system-level notifications
|
||||
// to notify the user on success, lest they get inundated with
|
||||
// notifications.
|
||||
else if (data.state === 'error' && data.emitNotifications) {
|
||||
appSelf.latest('self', function(app) {
|
||||
var iconUrl = notificationHelper.getIconURI(app);
|
||||
var dataObject = {
|
||||
v: notificationDataVersion,
|
||||
ntype: 'outbox',
|
||||
type: 'message_reader',
|
||||
folderType: 'outbox',
|
||||
accountId: data.accountId,
|
||||
messageSuid: data.suid
|
||||
};
|
||||
|
||||
sendNotification(
|
||||
BACKGROUND_SEND_NOTIFICATION_ID,
|
||||
'background-send-error-title',
|
||||
descId,
|
||||
iconUrl,
|
||||
dataObject
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When inbox is viewed, be sure to clear out any possible notification
|
||||
// for that account.
|
||||
evt.on('inboxShown', function(accountId) {
|
||||
fetchNotificationsData('sync').then(function(notificationsData) {
|
||||
if (notificationsData.hasOwnProperty(accountId)) {
|
||||
notificationsData[accountId].notification.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
Loading…
Reference in a new issue