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
196
gaia/apps/sms/views/shared/js/activity_handler.js
Executable file
196
gaia/apps/sms/views/shared/js/activity_handler.js
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/*global ActivityClient,
|
||||
App,
|
||||
Attachment,
|
||||
Draft,
|
||||
Drafts,
|
||||
MessageManager,
|
||||
Navigation,
|
||||
Settings,
|
||||
Utils
|
||||
*/
|
||||
/*exported ActivityHandler */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Describes available data types that can be associated with the activities.
|
||||
* @enum {string}
|
||||
*/
|
||||
const ActivityDataType = {
|
||||
IMAGE: 'image/*',
|
||||
AUDIO: 'audio/*',
|
||||
VIDEO: 'video/*',
|
||||
URL: 'url',
|
||||
VCARD: 'text/vcard'
|
||||
};
|
||||
|
||||
var ActivityHandler = {
|
||||
init: function() {
|
||||
ActivityClient.init(App.instanceId);
|
||||
|
||||
ActivityClient.on(
|
||||
'new-activity-request', this._onNewActivity.bind(this)
|
||||
);
|
||||
|
||||
ActivityClient.on(
|
||||
'share-activity-request', this._onShareActivity.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
_onNewActivity: function newHandler(activityData) {
|
||||
var viewInfo = {
|
||||
body: activityData.body,
|
||||
number: activityData.target || activityData.number,
|
||||
threadId: null
|
||||
};
|
||||
|
||||
var threadPromise;
|
||||
var focus = '';
|
||||
if (viewInfo.number) {
|
||||
// It's reasonable to focus on composer if we already have some phone
|
||||
// number or even contact to fill recipients input
|
||||
focus = 'composer';
|
||||
|
||||
/* If we have a body, we're always redirecting to the new message view,
|
||||
* because we don't want to overwrite any possible draft from an existing
|
||||
* conversation. */
|
||||
if (!viewInfo.body) {
|
||||
// try to get a thread from number
|
||||
// if no thread, promise is rejected and we try to find a contact
|
||||
threadPromise = MessageManager.findThreadFromNumber(viewInfo.number)
|
||||
.then((threadId) => viewInfo.threadId = threadId)
|
||||
// case no contact and no thread id: gobble the error
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return (threadPromise || Promise.resolve()).then(
|
||||
() => this.toView(viewInfo, focus)
|
||||
);
|
||||
},
|
||||
|
||||
_onShareActivity: function shareHandler(activityData) {
|
||||
var dataToShare = null;
|
||||
|
||||
switch(activityData.type) {
|
||||
case ActivityDataType.AUDIO:
|
||||
case ActivityDataType.VIDEO:
|
||||
case ActivityDataType.IMAGE:
|
||||
var attachments = activityData.blobs.map(function(blob, idx) {
|
||||
var attachment = new Attachment(blob, {
|
||||
name: activityData.filenames[idx],
|
||||
isDraft: true
|
||||
});
|
||||
|
||||
return attachment;
|
||||
});
|
||||
|
||||
var size = attachments.reduce(function(size, attachment) {
|
||||
if (attachment.type !== 'img') {
|
||||
size += attachment.size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}, 0);
|
||||
|
||||
if (size > Settings.mmsSizeLimitation) {
|
||||
Utils.alert({
|
||||
id: 'attached-files-too-large',
|
||||
args: {
|
||||
n: activityData.blobs.length,
|
||||
mmsSize: (Settings.mmsSizeLimitation / 1024).toFixed(0)
|
||||
}
|
||||
}).then(() => ActivityClient.postResult());
|
||||
return;
|
||||
}
|
||||
|
||||
dataToShare = attachments;
|
||||
break;
|
||||
case ActivityDataType.URL:
|
||||
dataToShare = activityData.url;
|
||||
break;
|
||||
// As for the moment we only allow to share one vcard we treat this case
|
||||
// in an specific block
|
||||
case ActivityDataType.VCARD:
|
||||
dataToShare = new Attachment(activityData.blobs[0], {
|
||||
name: activityData.filenames[0],
|
||||
isDraft: true
|
||||
});
|
||||
break;
|
||||
default:
|
||||
ActivityClient.postError(
|
||||
'Unsupported activity data type: ' + activityData.type
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataToShare) {
|
||||
ActivityClient.postError('No data to share found!');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.toView({ body: dataToShare });
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a draft for this message, and returns a draftId.
|
||||
*
|
||||
* @param {Object} message Message to save
|
||||
* @param {Number} message.threadId Conversation where this messages is
|
||||
* located.
|
||||
* @param {String} message.number The recipient as a phone number or email
|
||||
* address.
|
||||
* @param {(String|Attachment|Array.<Attachment>)} [message.body] The data to
|
||||
* add to the message's body.
|
||||
* @returns {Promise.<Number>} The new draft id.
|
||||
*/
|
||||
_storeDraftFromMessage(message) {
|
||||
var content = message.body || null;
|
||||
|
||||
var isSimpleContent = !content || typeof content === 'string';
|
||||
var isEmailAddress = message.number && Utils.isEmailAddress(message.number);
|
||||
|
||||
var draft = new Draft({
|
||||
threadId: message.threadId || null,
|
||||
recipients: message.number && [message.number] || null,
|
||||
content: !content || Array.isArray(content) ? content : [content],
|
||||
type: isSimpleContent && !isEmailAddress ? 'sms' : 'mms'
|
||||
});
|
||||
|
||||
return Drafts.request().then(
|
||||
() => Drafts.add(draft).store()
|
||||
).then(() => draft.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delivers the user to the correct view based on the params provided in the
|
||||
* "message" parameter.
|
||||
* @param {{number: string, body: string, threadId: number}} message It's
|
||||
* either a message object that belongs to a thread, or a message object from
|
||||
* the system. "number" is a string phone number to pre-populate the
|
||||
* recipients list with, "body" is an optional body to preset the compose
|
||||
* input with, "threadId" is an optional threadId corresponding to a new or
|
||||
* existing thread.
|
||||
* @param {string?} focus indicates which of 'composer' or 'recipients' field
|
||||
* we need to focus when we navigate to Thread panel.
|
||||
*/
|
||||
toView: function ah_toView(message, focus) {
|
||||
return Utils.onceDocumentIsVisible().then(() => {
|
||||
// If we have appropriate thread then let's forward user there, otherwise
|
||||
// open new message composer.
|
||||
if (message.threadId) {
|
||||
return Navigation.toPanel('thread', {
|
||||
id: message.threadId,
|
||||
focus
|
||||
});
|
||||
}
|
||||
|
||||
return this._storeDraftFromMessage(message).then(
|
||||
(draftId) => Navigation.toPanel('composer', { draftId, focus })
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
89
gaia/apps/sms/views/shared/js/activity_picker.js
Executable file
89
gaia/apps/sms/views/shared/js/activity_picker.js
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
/*global MozActivity */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
function handleActivity(activity, onsuccess, onerror) {
|
||||
// Note: the MozActivity is intentionally constructed in the caller and
|
||||
// passed down to this function in order to make it possible to pass an
|
||||
// inline option to its constructor in the caller for the sake of static
|
||||
// analysis tools. Please do not change that!
|
||||
|
||||
if (typeof onsuccess === 'function') {
|
||||
activity.onsuccess = onsuccess;
|
||||
}
|
||||
|
||||
activity.onerror = typeof onerror === 'function' ? onerror : function(error) {
|
||||
console.warn('Unhandled error spawning activity; ' + error.message);
|
||||
};
|
||||
}
|
||||
|
||||
var ActivityPicker = {
|
||||
dial: function ap_call(number, onsuccess, onerror) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'dial',
|
||||
data: {
|
||||
type: 'webtelephony/number',
|
||||
number: number
|
||||
}
|
||||
}), onsuccess, onerror);
|
||||
},
|
||||
email: function ap_email(email, onsuccess, onerror) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'mail',
|
||||
URI: 'mailto:' + email
|
||||
}
|
||||
}), onsuccess, onerror);
|
||||
},
|
||||
url: function ap_browse(url, onsuccess, onerror) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'view',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: url
|
||||
}
|
||||
}), onsuccess, onerror);
|
||||
},
|
||||
createNewContact: function ap_createNewContact(contactProps) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: contactProps
|
||||
}
|
||||
}));
|
||||
},
|
||||
addToExistingContact: function ap_addToExistingContact(contactProps) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'update',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: contactProps
|
||||
}
|
||||
}));
|
||||
},
|
||||
viewContact: function ap_viewContact(contactProps) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'open',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: contactProps
|
||||
}
|
||||
}));
|
||||
},
|
||||
openSettings: function ap_openSettings(onsuccess, onerror) {
|
||||
handleActivity(new MozActivity({
|
||||
name: 'configure',
|
||||
data: {
|
||||
target: 'device',
|
||||
section: 'messaging'
|
||||
}
|
||||
}), onsuccess, onerror);
|
||||
}
|
||||
};
|
||||
|
||||
exports.ActivityPicker = ActivityPicker;
|
||||
|
||||
}(this));
|
||||
72
gaia/apps/sms/views/shared/js/app.js
Executable file
72
gaia/apps/sms/views/shared/js/app.js
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
/*global Promise */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Class that is applied to body whenever application is ready for user.
|
||||
* @const {string}
|
||||
*/
|
||||
const APPLICATION_READY_CLASS_NAME = 'js-app-ready';
|
||||
|
||||
/**
|
||||
* Unique identifier of the app instance.
|
||||
* @type {number}
|
||||
*/
|
||||
const INSTANCE_ID = Date.now();
|
||||
|
||||
var app = {
|
||||
isReady: function app_isReady() {
|
||||
return document.body.classList.contains(APPLICATION_READY_CLASS_NAME);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets application readiness, can be called only once
|
||||
*/
|
||||
setReady: function app_setReady() {
|
||||
if (this.isReady()) {
|
||||
throw new Error('Application should be marked as ready only once!');
|
||||
}
|
||||
document.body.classList.add(APPLICATION_READY_CLASS_NAME);
|
||||
// Tell audio channel manager that we want to adjust the notification
|
||||
// channel if the user press the volumeup/volumedown buttons in Messages.
|
||||
if (navigator.mozAudioChannelManager) {
|
||||
navigator.mozAudioChannelManager.volumeControlChannel = 'notification';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns promise that will be resolved once application is ready for
|
||||
* interaction.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
whenReady: function whenReady() {
|
||||
if (this.isReady()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
var bodyClassObserver = new MutationObserver(function() {
|
||||
try {
|
||||
if (this.isReady()) {
|
||||
bodyClassObserver.disconnect();
|
||||
resolve();
|
||||
}
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
bodyClassObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
get instanceId() {
|
||||
return INSTANCE_ID;
|
||||
}
|
||||
};
|
||||
|
||||
exports.App = app;
|
||||
})(this);
|
||||
7
gaia/apps/sms/views/shared/js/bootstrap.js
vendored
Executable file
7
gaia/apps/sms/views/shared/js/bootstrap.js
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
/* global Startup */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Startup.init();
|
||||
})(self);
|
||||
308
gaia/apps/sms/views/shared/js/contact_renderer.js
Executable file
308
gaia/apps/sms/views/shared/js/contact_renderer.js
Executable file
|
|
@ -0,0 +1,308 @@
|
|||
/*global SharedComponents, Template, Utils, Settings */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var FLAVORS = {
|
||||
/*
|
||||
* renderAll controls whether all phone numbers sohuld be rendered for this
|
||||
* contact
|
||||
*
|
||||
* shouldHighlight controls whether we should generate a markup allowing
|
||||
* highlighting depending on the input
|
||||
*
|
||||
* template "main" will get the following parameters:
|
||||
* - name: the contact's title
|
||||
* - number: the phone number
|
||||
* - nameHTML: same than name if shouldHighlight is false, otherwise contains
|
||||
* markup with the highlighting
|
||||
* - phoneDetailsHTML: phone details label (type, number, carrier), also if
|
||||
* shouldHighlight is true, then number is wrapped into highlighting markup
|
||||
* - photoHTML: result of the "photo" template, if present
|
||||
* - type: phone number type information
|
||||
* - carrier: phone number carrier information
|
||||
*
|
||||
* template "photo" will get the following parameters:
|
||||
* - photoURL: the URL of the contact's first photo
|
||||
*
|
||||
*/
|
||||
suggestion: {
|
||||
renderAll: true,
|
||||
shouldHighlight: true,
|
||||
templates: {
|
||||
main: 'contact-suggestion-tmpl'
|
||||
}
|
||||
},
|
||||
suggestionUnknown: {
|
||||
renderAll: true,
|
||||
shouldHighlight: true,
|
||||
templates: {
|
||||
main: 'contact-suggestion-unknown-tmpl'
|
||||
}
|
||||
},
|
||||
prompt: {
|
||||
renderAll: false,
|
||||
shouldHighlight: false,
|
||||
templates: {
|
||||
main: 'contact-prompt-tmpl',
|
||||
photo: 'contact-photo-tmpl'
|
||||
}
|
||||
},
|
||||
'group-view': {
|
||||
renderAll: false,
|
||||
shouldHighlight: false,
|
||||
templates: {
|
||||
main: 'contact-suggestion-tmpl',
|
||||
photo: 'contact-photo-tmpl'
|
||||
}
|
||||
},
|
||||
'report-view': {
|
||||
renderAll: false,
|
||||
shouldHighlight: false,
|
||||
templates: {
|
||||
main: 'contact-report-tmpl',
|
||||
photo: 'contact-photo-tmpl'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* let's lazy load generic templates */
|
||||
var templates = {};
|
||||
|
||||
function getTemplate(id) {
|
||||
if (!templates[id]) {
|
||||
templates[id] = Template(id);
|
||||
}
|
||||
return templates[id];
|
||||
}
|
||||
|
||||
var highlightTemplateId = 'contact-highlight-tmpl';
|
||||
|
||||
// Warning: always feed this function an already escaped data
|
||||
function highlight(escapedData, regexpArray) {
|
||||
// When rendering a suggestion, we highlight the matched substring.
|
||||
// The approach is to escape the html and the search string, and
|
||||
// then replace on all "words" (whitespace bounded strings) with
|
||||
// the substring run through the highlight template.
|
||||
|
||||
// this variable will allow skipping to the next regexp
|
||||
var matchFound;
|
||||
var template = getTemplate(highlightTemplateId);
|
||||
|
||||
function loopReplaceFn(match) {
|
||||
matchFound = true;
|
||||
// The match is safe, because splitData[i] is derived from
|
||||
// escapedData
|
||||
return template.interpolate({
|
||||
str: match
|
||||
}, {
|
||||
safe: ['str']
|
||||
});
|
||||
}
|
||||
|
||||
var splitData = escapedData.split(/\s+/);
|
||||
|
||||
// For each "word"
|
||||
for (var i = 0; i < splitData.length; i++) {
|
||||
matchFound = false;
|
||||
// Loop over search term regexes
|
||||
for (var k = 0; !matchFound && k < regexpArray.length; k++) {
|
||||
splitData[i] = splitData[i].replace(regexpArray[k], loopReplaceFn);
|
||||
}
|
||||
}
|
||||
return splitData.join(' ');
|
||||
}
|
||||
|
||||
var ContactRenderer = exports.ContactRenderer = function constructor(opts) {
|
||||
if (!opts) {
|
||||
throw new Error('ContactRenderer constructor needs an options object');
|
||||
}
|
||||
|
||||
this.opts = opts;
|
||||
this.templates = {};
|
||||
for (var key in opts.templates) {
|
||||
var id = opts.templates[key];
|
||||
this.templates[key] = Template(id);
|
||||
}
|
||||
|
||||
if (this.templates.photo) {
|
||||
this.opts.renderPhoto = true;
|
||||
}
|
||||
};
|
||||
|
||||
ContactRenderer.prototype = {
|
||||
// Returns true when a contact has been rendered
|
||||
// Returns false when no contact has been rendered
|
||||
render: function cr_render(renderOpts) {
|
||||
/**
|
||||
*
|
||||
* params {
|
||||
* contact:
|
||||
* A contact object.
|
||||
*
|
||||
* input:
|
||||
* Any input value associated with the contact,
|
||||
* possibly from a search or similar operation.
|
||||
*
|
||||
* skip:
|
||||
* An array of phone numbers that should not be rendered
|
||||
*
|
||||
* target:
|
||||
* Parent node to append the rendered node to
|
||||
*
|
||||
* infoBlock:
|
||||
* Node that will be appended to the node selected by
|
||||
* infoBlockParentSelector
|
||||
*
|
||||
* infoBlockParentSelector:
|
||||
* Parent element selector for appending infoBlock as child
|
||||
*
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
var contact = renderOpts.contact;
|
||||
var input = renderOpts.input;
|
||||
input = input && input.trim();
|
||||
var target = renderOpts.target;
|
||||
var shouldHighlight = this.opts.shouldHighlight;
|
||||
var renderAll = this.opts.renderAll;
|
||||
var renderPhoto = this.opts.renderPhoto;
|
||||
var skip = renderOpts.skip || [];
|
||||
var block = renderOpts.infoBlock;
|
||||
var parentSelector = renderOpts.infoBlockParentSelector;
|
||||
|
||||
// we can't do much without a contact
|
||||
if (!contact) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't render if there is no phone number
|
||||
// TODO: Add email checking support for MMS
|
||||
var hasTel = contact.tel && contact.tel.length;
|
||||
var hasEmail = contact.email && contact.email.length;
|
||||
|
||||
if (!hasTel && !(Settings.supportEmailRecipient && hasEmail)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We search on the escaped HTML via a regular expression
|
||||
var escaped = Utils.escapeRegex(Template.escape(input));
|
||||
var escsubs = escaped.split(/\s+/);
|
||||
// Build a list of regexes used for highlighting suggestions
|
||||
var regexps = {
|
||||
name: escsubs.map(function(k) {
|
||||
// String matches occur on the beginning of a "word" to
|
||||
// maintain parity with the contact search algorithm which
|
||||
// only considers left aligned exact matches on words
|
||||
return new RegExp('^' + k, 'gi');
|
||||
}),
|
||||
number: escsubs.map(function(k) {
|
||||
// Match any of the search terms with the number
|
||||
return new RegExp(k, 'ig');
|
||||
}),
|
||||
email: escsubs.map(function(k) {
|
||||
// Match any of the search terms with the email
|
||||
return new RegExp('^' + k, 'gi');
|
||||
})
|
||||
};
|
||||
|
||||
var include = renderPhoto ? { photoURL: true } : null;
|
||||
var addresses = [];
|
||||
|
||||
if (contact.tel && contact.tel.length) {
|
||||
addresses = addresses.concat(contact.tel);
|
||||
}
|
||||
if (Settings.supportEmailRecipient &&
|
||||
contact.email && contact.email.length) {
|
||||
addresses = addresses.concat(contact.email);
|
||||
}
|
||||
var details = Utils.getContactDetails(
|
||||
addresses[0].value, contact, include
|
||||
);
|
||||
|
||||
var tempDiv = document.createElement('div');
|
||||
|
||||
addresses.forEach(function(current) {
|
||||
// Only render a contact's tel value entry for the _specified_
|
||||
// input value when not rendering all values. If the tel
|
||||
// record value _doesn't_ match, then continue.
|
||||
//
|
||||
if (!renderAll && !Utils.probablyMatches(current.value, input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If rendering for contact search result suggestions, don't
|
||||
// render contact tel records for values that are already
|
||||
// selected as recipients. This comparison should be safe,
|
||||
// as the value in this.recipients.numbers comes from the same
|
||||
// source that current.value comes from.
|
||||
if (renderAll && skip.indexOf(current.value) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = Utils.getDisplayObject(details.title, current);
|
||||
|
||||
var props = ['name', 'number'];
|
||||
if (Settings.supportEmailRecipient) {
|
||||
props.push('email');
|
||||
}
|
||||
|
||||
props.forEach(function(key) {
|
||||
var escapedData = Template.escape(data[key]);
|
||||
if (shouldHighlight) {
|
||||
escapedData = highlight(escapedData, regexps[key]);
|
||||
}
|
||||
|
||||
data[key + 'HTML'] = escapedData;
|
||||
});
|
||||
|
||||
data.phoneDetailsHTML = SharedComponents.phoneDetails({
|
||||
number: data.numberHTML,
|
||||
type: data.type,
|
||||
carrier: data.carrier
|
||||
}, {
|
||||
safe: ['number']
|
||||
}).toString();
|
||||
|
||||
// Render contact photo only for specific flavor
|
||||
data.photoHTML = renderPhoto && details.photoURL ?
|
||||
this.templates.photo.interpolate() : '';
|
||||
|
||||
// Interpolate HTML template with data and inject.
|
||||
// Known "safe" HTML values will not be re-sanitized.
|
||||
tempDiv.innerHTML = this.templates.main.interpolate(data, {
|
||||
safe: ['nameHTML', 'phoneDetailsHTML', 'srcAttr', 'photoHTML']
|
||||
});
|
||||
|
||||
var element = tempDiv.firstElementChild;
|
||||
var blockParent = element.querySelector(parentSelector);
|
||||
|
||||
if (blockParent) {
|
||||
blockParent.appendChild(block);
|
||||
}
|
||||
|
||||
if (data.photoHTML) {
|
||||
var contactPhoto = element.querySelector('.contact-photo');
|
||||
contactPhoto.style.backgroundImage =
|
||||
'url("' + encodeURI(details.photoURL) + '")';
|
||||
|
||||
Utils.asyncLoadRevokeURL(details.photoURL);
|
||||
}
|
||||
|
||||
target.appendChild(element);
|
||||
|
||||
tempDiv.textContent = '';
|
||||
}, this);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
ContactRenderer.flavor = function(flavor) {
|
||||
// we could possibly cache instances here, if we need more performance
|
||||
return new ContactRenderer(FLAVORS[flavor]);
|
||||
};
|
||||
|
||||
})(window);
|
||||
328
gaia/apps/sms/views/shared/js/contacts.js
Executable file
328
gaia/apps/sms/views/shared/js/contacts.js
Executable file
|
|
@ -0,0 +1,328 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
/*global fb, Settings, Utils */
|
||||
var unknownNumbers = [];
|
||||
|
||||
var filterFns = {
|
||||
contains: function(a, b) {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
return a.includes(b);
|
||||
},
|
||||
equality: function(a, b) {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
return a === b;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* isMatch
|
||||
*
|
||||
* Validate a contact record against a list of terms by specified fields
|
||||
*
|
||||
* @param {Object} contact a contact record object.
|
||||
*
|
||||
* @param {Object} criteria fields and terms to validate a contact.
|
||||
* - fields (fields of a contact record).
|
||||
* - terms (terms to validate against contact record field values).
|
||||
* @param {Function} filterFn function accepts 2 arguments for
|
||||
* comparison.
|
||||
*
|
||||
*/
|
||||
function isMatch(contact, criteria, filterFn) {
|
||||
/**
|
||||
* Validation Strategy
|
||||
*
|
||||
* 1. Let _found_ be a register of confirmed terms
|
||||
* 2. Let _contact_ be a contact record.
|
||||
* 3. For each _term_ [...input list], with the label _outer_
|
||||
* - For each _field_ of [givenName, familyName]
|
||||
* - For each _value_ of _contact_[ _field_ ]
|
||||
* - Let _found_[ _term_ ] be the result of calling
|
||||
* _filterFn_( _value_, _term_).
|
||||
* - If _found_[ _term_ ] is **true**, continue to
|
||||
* loop labelled _outer_
|
||||
* 4. If every value of _key_ in _found_ is **true** return **true**,
|
||||
* else return **false**
|
||||
*/
|
||||
var found = {};
|
||||
|
||||
// The outer loop is specifically labelled to allow the
|
||||
// nested condition a way out of the second and third loop.
|
||||
outer:
|
||||
for (var i = 0, ilen = criteria.terms.length; i < ilen; i++) {
|
||||
var term = criteria.terms[i];
|
||||
for (var j = 0, jlen = criteria.fields.length; j < jlen; j++) {
|
||||
var field = criteria.fields[j];
|
||||
|
||||
if (!contact[field]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var k = 0, klen = contact[field].length; k < klen; k++) {
|
||||
var value = contact[field][k];
|
||||
if (typeof value.value !== 'undefined') {
|
||||
value = value.value;
|
||||
}
|
||||
|
||||
if ((found[term] = filterFn(value.trim(), term))) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pending the publication of a list of ES6 features available in Firefox
|
||||
// and deemed "safe enough" for FirefoxOS development, the above code is a
|
||||
// stand-in for what should be written as:
|
||||
// outer:
|
||||
// for (var term of criteria.terms) {
|
||||
// for (var field of criteria.fields) {
|
||||
// for (var value of contact[field]) {
|
||||
// if (found[term] = value.toLowerCase().contains(term)) {
|
||||
// continue outer;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return Object.keys(found).every(function(key) {
|
||||
return found[key];
|
||||
});
|
||||
}
|
||||
|
||||
var rspaces = /\s+/;
|
||||
|
||||
var Contacts = {
|
||||
findBy: function contacts_findBy(filter, callback) {
|
||||
var lower = [];
|
||||
var filterValue = (filter.filterValue || '').trim();
|
||||
var terms;
|
||||
|
||||
if (!navigator.mozContacts || !filterValue.length) {
|
||||
/**
|
||||
* Bailout Strategy
|
||||
*
|
||||
* 1. A _missing_ filter.filterValue will results in an
|
||||
* error when passed to mozContacts.find, bailing out
|
||||
* early avoids the additional overhead of making a
|
||||
* request that we know will fail.
|
||||
*
|
||||
* 2. Missing navigator.mozContacts API or empty but
|
||||
* present filter.filterValue make the app useless,
|
||||
* so prevent unpredictable state by making the
|
||||
* result a no-op.
|
||||
*
|
||||
*/
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup Strategy
|
||||
*
|
||||
* 1. Create a list of terms from the filterValue string
|
||||
*
|
||||
* 2. If only 1 term, set filterValue to that term.
|
||||
*
|
||||
* 3. If more than 1 term, find the length predominate term.
|
||||
* a. let initial be an empty string
|
||||
* (Do not use default first item in terms, or it will not
|
||||
* be pushed into the lowercase list)
|
||||
* b. For each term of terms
|
||||
* i. Push lowercased term into a list of lowercased terms
|
||||
* ii. If the current term.length is greater then initial.length,
|
||||
* return the term, otherwise return the initial.
|
||||
*
|
||||
* 4. If filterValue.length is < 3, set the filterLimit to 10
|
||||
*
|
||||
* 5. Remove the length predominate term from the list of validation terms
|
||||
*
|
||||
* 6. Add back the original search value, this is needed for matching
|
||||
* multi-word queries
|
||||
*
|
||||
* 7. Make a mozContact.find request with the length predominate term
|
||||
* as the filter.filterValue.
|
||||
*
|
||||
* 8. In the mozContacts.find request success handler, filter the
|
||||
* results by evaluating each contact record against
|
||||
* the list of validation terms.
|
||||
*/
|
||||
|
||||
// Step 1
|
||||
terms = filterValue.split(rspaces);
|
||||
|
||||
filter.filterValue = terms.length === 1 ?
|
||||
// Step 2
|
||||
terms[0] :
|
||||
// Step 3
|
||||
terms.reduce(function(initial, term) {
|
||||
// Step 3.b.i
|
||||
// (used as a criteria list for isMatch)
|
||||
lower.push(term.toLowerCase());
|
||||
// Step 3.b.ii
|
||||
return term.length > initial.length ? term : initial;
|
||||
// Step 3.a
|
||||
}, '');
|
||||
|
||||
// Step 4
|
||||
if (filter.filterValue.length < 3) {
|
||||
filter.filterLimit = 10;
|
||||
}
|
||||
|
||||
// Step 5
|
||||
lower.splice(lower.indexOf(filter.filterValue.toLowerCase()), 1);
|
||||
|
||||
// Step 6
|
||||
// This would be much nicer using spread operator
|
||||
lower.push.apply(lower, terms);
|
||||
|
||||
// Step 7
|
||||
return navigator.mozContacts.find(filter).then((contacts) => {
|
||||
var results;
|
||||
|
||||
// Step 8
|
||||
if (terms.length > 1) {
|
||||
var fields = ['tel', 'givenName', 'familyName'];
|
||||
if (Settings.supportEmailRecipient) {
|
||||
fields.push('email');
|
||||
}
|
||||
var criteria = { fields: fields, terms: lower };
|
||||
|
||||
results = contacts.filter(
|
||||
(contact) => isMatch(contact, criteria, filterFns.contains)
|
||||
);
|
||||
} else {
|
||||
results = contacts;
|
||||
}
|
||||
|
||||
return results;
|
||||
}).catch((e) => {
|
||||
console.error('Got an error while looking up contacts.', e.name, e);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
findByString: function contacts_findByString(filterValue) {
|
||||
return Promise.all([
|
||||
this.findContactByString(filterValue),
|
||||
this.findByUnknown(filterValue)
|
||||
]).then(([known, unknown]) => {
|
||||
return (known || []).concat(unknown);
|
||||
});
|
||||
},
|
||||
|
||||
findContactByString: function contacts_findContactByString(filterValue) {
|
||||
var props = ['tel', 'givenName', 'familyName'];
|
||||
if (Settings.supportEmailRecipient) {
|
||||
props.push('email');
|
||||
}
|
||||
return this.findBy({
|
||||
filterBy: props,
|
||||
filterOp: 'contains',
|
||||
filterValue: filterValue
|
||||
});
|
||||
},
|
||||
|
||||
findByUnknown: function contacts_findByUnknown(filterValue) {
|
||||
var list = [];
|
||||
for (var i = 0, length = unknownNumbers.length; i < length; i++) {
|
||||
//We only need at max 3 unknown contacts
|
||||
if (list.length > 2) {
|
||||
break;
|
||||
}
|
||||
var num = unknownNumbers[i];
|
||||
if (num.includes(filterValue)) {
|
||||
var obj = {
|
||||
name: [num],
|
||||
tel: [{value: num}],
|
||||
source: 'unknown'
|
||||
};
|
||||
list.push(obj);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(list);
|
||||
},
|
||||
|
||||
findExact: function contacts_findExact(filterValue) {
|
||||
return this.findBy({
|
||||
filterBy: ['givenName', 'familyName'],
|
||||
filterOp: 'contains',
|
||||
filterValue: filterValue
|
||||
}).then((contacts) => {
|
||||
var contact = contacts.length ? contacts[0] : null;
|
||||
var criteria = {
|
||||
fields: ['name'],
|
||||
terms: [filterValue]
|
||||
};
|
||||
var isExact = false;
|
||||
|
||||
if (contact) {
|
||||
isExact = isMatch(contact, criteria, filterFns.equality);
|
||||
}
|
||||
|
||||
return isExact ? [contact] : [];
|
||||
});
|
||||
},
|
||||
|
||||
findByPhoneNumber: function contacts_findByPhone(filterValue) {
|
||||
return this.findBy({
|
||||
filterBy: ['tel'],
|
||||
filterOp: 'match',
|
||||
filterValue: filterValue.replace(/\s+/g, '')
|
||||
}).then((contacts) => {
|
||||
if (contacts.length) {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
// Facebook search
|
||||
fb.getContactByNumber(filterValue, function fbByPhone(fbContact) {
|
||||
defer.resolve(fbContact ? [fbContact] : []);
|
||||
}, function error_fbByPhone(err) {
|
||||
if (err.name !== 'DatastoreNotFound') {
|
||||
console.error('Error while retrieving fb by phone: ', err.name);
|
||||
}
|
||||
|
||||
defer.resolve([]);
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
});
|
||||
},
|
||||
|
||||
findByAddress: function contacts_findByAddress(fValue) {
|
||||
if (Settings.supportEmailRecipient && Utils.isEmailAddress(fValue)) {
|
||||
return this.findExactByEmail(fValue);
|
||||
} else {
|
||||
return this.findByPhoneNumber(fValue);
|
||||
}
|
||||
},
|
||||
|
||||
findExactByEmail: function contacts_findExactByEmail(fValue) {
|
||||
return this.findBy({
|
||||
filterBy: ['email'],
|
||||
filterOp: 'equals',
|
||||
filterValue: fValue
|
||||
});
|
||||
},
|
||||
|
||||
addUnknown: function addUnknown(number) {
|
||||
var index = unknownNumbers.indexOf(number);
|
||||
if (index === -1) {
|
||||
unknownNumbers.push(number);
|
||||
}
|
||||
},
|
||||
|
||||
clearUnknown: function clearUnknown() {
|
||||
unknownNumbers.length = 0;
|
||||
},
|
||||
|
||||
getunknownLength: function getunknownLength() {
|
||||
return unknownNumbers.length;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Contacts = Contacts;
|
||||
}(this));
|
||||
165
gaia/apps/sms/views/shared/js/dialog.js
Executable file
165
gaia/apps/sms/views/shared/js/dialog.js
Executable file
|
|
@ -0,0 +1,165 @@
|
|||
/*global WeakMap */
|
||||
|
||||
/* exported Dialog */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
Generic confirm screen. Only 'cancel/default' is mandatory.
|
||||
|
||||
Text fields are localized using commonly defined parameter structure:
|
||||
https://developer.mozilla.org/en-US/Firefox_OS/Developing_Gaia/
|
||||
localization_code_best_practices#Writing_APIs_that_operate_on_L10nIDs + 'raw'
|
||||
can be Node aw well.
|
||||
|
||||
The constructor parameter should follow the following structure:
|
||||
|
||||
{
|
||||
title: { raw: 'Non localizable title' },
|
||||
body: {
|
||||
id: 'localizableStringWithArgument',
|
||||
args: { n: count }
|
||||
},
|
||||
classes: ['specific-class'], // optional
|
||||
options: {
|
||||
// Cancel is a mandatory option. You need to define at least the text.
|
||||
cancel: {
|
||||
text: 'localizableCancelLabel'
|
||||
},
|
||||
confirm: {
|
||||
text: 'localizableRemoveLabel',
|
||||
method: function(params) {
|
||||
fooFunction(params);
|
||||
},
|
||||
params: [arg1, arg2,....],
|
||||
className: 'optionalClassName'
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// helper to localize an element given parameters
|
||||
function createLocalizedElement(tagName, valueL10n) {
|
||||
var element = document.createElement(tagName);
|
||||
|
||||
// if we passed an l10nId, use the l10n `setAttributes' method
|
||||
if (typeof valueL10n === 'string') {
|
||||
element.setAttribute('data-l10n-id', valueL10n);
|
||||
} else if (valueL10n.id) {
|
||||
document.l10n.setAttributes(element, valueL10n.id, valueL10n.args);
|
||||
// if we passed in a HTML Fragment, it is already localized
|
||||
} else if (valueL10n.raw && valueL10n.raw.nodeType) {
|
||||
element.appendChild(valueL10n.raw);
|
||||
// otherwise - stuff text in here...
|
||||
} else {
|
||||
element.textContent = valueL10n.raw;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
var Dialog = function(params) {
|
||||
// We need, at least, one cancel option string
|
||||
if (!params || !params.options || !params.options.cancel) {
|
||||
return;
|
||||
}
|
||||
var handlers = new WeakMap();
|
||||
// Create the structure
|
||||
this.form = document.createElement('form');
|
||||
this.form.dataset.type = 'confirm';
|
||||
this.form.setAttribute('role', 'dialog');
|
||||
this.form.tabIndex = -1;
|
||||
// Pick up option_menu.css styling
|
||||
this.form.dataset.subtype = 'menu';
|
||||
|
||||
if (params.classes) {
|
||||
this.form.classList.add(...params.classes);
|
||||
}
|
||||
|
||||
var infoSection = document.createElement('section');
|
||||
infoSection.appendChild(createLocalizedElement('h1', params.title));
|
||||
infoSection.appendChild(createLocalizedElement('p', params.body));
|
||||
this.form.appendChild(infoSection);
|
||||
|
||||
// Adding options. In this case we have a maximum of 2, with different styles
|
||||
// per button
|
||||
var menu = document.createElement('menu');
|
||||
// Default button (Cancel button). It's mandatory
|
||||
var cancelOption = params.options.cancel;
|
||||
var cancelButton = createLocalizedElement('button', cancelOption.text);
|
||||
handlers.set(cancelButton, cancelOption);
|
||||
|
||||
// If we have only button, let's mark it as recommended action.
|
||||
if (!params.options.confirm) {
|
||||
cancelButton.className = 'recommend';
|
||||
}
|
||||
|
||||
menu.appendChild(cancelButton);
|
||||
|
||||
if (params.options.confirm) {
|
||||
var confirmOption = params.options.confirm;
|
||||
var confirmButton = createLocalizedElement('button', confirmOption.text);
|
||||
confirmButton.className = params.options.confirm.className || 'recommend';
|
||||
handlers.set(confirmButton, confirmOption);
|
||||
menu.appendChild(confirmButton);
|
||||
}
|
||||
|
||||
this.form.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
this.form.addEventListener('transitionend', function(event) {
|
||||
var form = event.target;
|
||||
if (!form.classList.contains('visible') && form.parentNode) {
|
||||
form.remove();
|
||||
} else {
|
||||
// Focus form for accessibility
|
||||
form.focus();
|
||||
}
|
||||
|
||||
document.body.classList.remove('dialog-animating');
|
||||
});
|
||||
|
||||
menu.addEventListener('click', function(event) {
|
||||
var action = handlers.get(event.target);
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.method) {
|
||||
// Delegate operation to target method. This allows
|
||||
// for a custom "Cancel" to be provided by calling program
|
||||
action.method.apply(null, action.params || []);
|
||||
}
|
||||
|
||||
// Hide action menu when click is received
|
||||
this.hide();
|
||||
}.bind(this));
|
||||
// Appending the action menu to the form
|
||||
this.form.appendChild(menu);
|
||||
};
|
||||
|
||||
// We prototype functions to show/hide the UI of action-menu
|
||||
Dialog.prototype.show = function() {
|
||||
// Remove the focus to hide the keyboard asap
|
||||
document.activeElement && document.activeElement.blur();
|
||||
|
||||
if (!this.form.parentNode) {
|
||||
document.body.appendChild(this.form);
|
||||
|
||||
// Flush style on form so that the show transition plays once we add
|
||||
// the visible class.
|
||||
this.form.clientTop;
|
||||
}
|
||||
this.form.classList.add('visible');
|
||||
document.body.classList.add('dialog-animating');
|
||||
};
|
||||
|
||||
Dialog.prototype.hide = function() {
|
||||
this.form.classList.remove('visible');
|
||||
};
|
||||
|
||||
exports.Dialog = Dialog;
|
||||
|
||||
// end global closure
|
||||
}(this));
|
||||
|
||||
111
gaia/apps/sms/views/shared/js/error_dialog.js
Executable file
111
gaia/apps/sms/views/shared/js/error_dialog.js
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
/*global Dialog, Settings*/
|
||||
|
||||
/* exported ErrorDialog */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
/*
|
||||
Error confirm screen. Handling all the message error code and corresponding
|
||||
description/behavior.
|
||||
|
||||
Options object can contain the following properties:
|
||||
* recipients: ['foo', 'bar'] - to show the recipient information, used only
|
||||
if errorDescription.showRecipient is equal to "true";
|
||||
* confirmHandler: function() - to be executed if user selects confirm
|
||||
action in the error dialog, used only if errorDescription.executeHandler
|
||||
is equal to "true".
|
||||
}
|
||||
*/
|
||||
function ErrorDialog(errorDescription, options) {
|
||||
var prefix = errorDescription.prefix;
|
||||
|
||||
if (!prefix) {
|
||||
throw new Error('Prefix is required!');
|
||||
}
|
||||
|
||||
var dialogOptions = {
|
||||
cancel: {
|
||||
text: prefix + 'BtnOk'
|
||||
}
|
||||
};
|
||||
|
||||
if (options && options.confirmHandler && errorDescription.executeHandler) {
|
||||
dialogOptions.confirm = {
|
||||
text: prefix + 'Confirm',
|
||||
method: options.confirmHandler
|
||||
};
|
||||
}
|
||||
|
||||
var bodyL10nId = prefix + 'Body';
|
||||
var body = null;
|
||||
var additionalClasses = [];
|
||||
|
||||
if (errorDescription.showRecipient) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
var mainText = document.createElement('span');
|
||||
document.l10n.setAttributes(
|
||||
mainText, bodyL10nId,
|
||||
{ n: options.recipients.length }
|
||||
);
|
||||
fragment.appendChild(mainText);
|
||||
|
||||
var list = options.recipients.reduce((list, recipient) => {
|
||||
var li = document.createElement('li');
|
||||
li.textContent = recipient;
|
||||
|
||||
list.appendChild(li);
|
||||
|
||||
return list;
|
||||
}, document.createElement('ul'));
|
||||
|
||||
fragment.appendChild(list);
|
||||
body = { raw: fragment };
|
||||
additionalClasses.push('error-dialog-show-recipient');
|
||||
} else if (errorDescription.showDsdsStatus) {
|
||||
var messageBodyParams = null;
|
||||
|
||||
var mmsServiceId = Settings.mmsServiceId;
|
||||
if (mmsServiceId !== null) {
|
||||
// mmsServiceId = 0 => default Service is SIM 1
|
||||
// mmsServiceId = 1 => default Service is SIM 2
|
||||
|
||||
switch (mmsServiceId) {
|
||||
case 0:
|
||||
messageBodyParams = {
|
||||
activeSimId: '1',
|
||||
nonActiveSimId: '2'
|
||||
};
|
||||
break;
|
||||
case 1:
|
||||
messageBodyParams = {
|
||||
activeSimId: '2',
|
||||
nonActiveSimId: '1'
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
'MMS ServiceId(' + mmsServiceId + ') is not matched!'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error('Settings unavailable');
|
||||
}
|
||||
|
||||
body = {
|
||||
id: bodyL10nId,
|
||||
args: messageBodyParams
|
||||
};
|
||||
}
|
||||
|
||||
Dialog.call(this, {
|
||||
title: prefix + 'Title',
|
||||
body: body || bodyL10nId,
|
||||
options: dialogOptions,
|
||||
classes: ['error-dialog-' + prefix, 'error-dialog', ...additionalClasses]
|
||||
});
|
||||
}
|
||||
|
||||
ErrorDialog.prototype = Object.create(Dialog.prototype);
|
||||
|
||||
exports.ErrorDialog = ErrorDialog;
|
||||
}(this));
|
||||
81
gaia/apps/sms/views/shared/js/errors.js
Executable file
81
gaia/apps/sms/views/shared/js/errors.js
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
/* exported Errors */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Maps error code to error description object:
|
||||
* prefix: used as prefix for error message text/title l10nId,
|
||||
* showRecipient: indicates whether recipients info should be included into
|
||||
* error message displayed to the user;
|
||||
* showDsdsStatus: indicates whether DSDS status should be included into
|
||||
* error message displayed to the user;
|
||||
* executeHandler: indicates whether confirmation handler passed to the
|
||||
* error dialog should be executed once user selects confirm action.
|
||||
* @type {Map.<string, Object>}
|
||||
*/
|
||||
const ERRORS = new Map();
|
||||
|
||||
function initErrorsMap() {
|
||||
ERRORS.set('NoSimCardError', {
|
||||
prefix: 'sendMissingSimCard'
|
||||
});
|
||||
|
||||
ERRORS.set('RadioDisabledError', {
|
||||
prefix: 'sendFlightMode'
|
||||
});
|
||||
|
||||
ERRORS.set('FdnCheckError',{
|
||||
prefix: 'fdnError',
|
||||
showRecipient: true
|
||||
});
|
||||
|
||||
ERRORS.set('NonActiveSimCardError', {
|
||||
prefix: 'switchSimToRetrieve',
|
||||
showDsdsStatus: true,
|
||||
executeHandler: true
|
||||
});
|
||||
|
||||
ERRORS.set('NoSignalError', {
|
||||
prefix: 'sendNoSignalError'
|
||||
});
|
||||
|
||||
ERRORS.set('NotFoundError', {
|
||||
prefix: 'sendNotFoundError'
|
||||
});
|
||||
|
||||
ERRORS.set('InvalidAddressError', {
|
||||
prefix: 'sendInvalidAddressError'
|
||||
});
|
||||
|
||||
ERRORS.set('SimNotMatchedError', {
|
||||
prefix: 'simNotMatchedError'
|
||||
});
|
||||
|
||||
ERRORS.set('NonActiveSimCardToSendError', {
|
||||
prefix: 'switchSimToSend',
|
||||
showDsdsStatus: true,
|
||||
executeHandler: true
|
||||
});
|
||||
|
||||
ERRORS.set('RadioDisabledToDownloadError', {
|
||||
prefix: 'radioDisabledToDownload'
|
||||
});
|
||||
|
||||
// The error type below will be represented as "GeneralError" in dialog for
|
||||
// the rest of error codes (InternalError and etc.)
|
||||
ERRORS.set('UnknownError', {
|
||||
prefix: 'sendDefaultError'
|
||||
});
|
||||
}
|
||||
|
||||
exports.Errors = {
|
||||
get: function(errorCode) {
|
||||
if (!ERRORS.size) {
|
||||
initErrorsMap();
|
||||
}
|
||||
|
||||
return ERRORS.get(errorCode) || ERRORS.get('UnknownError');
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
83
gaia/apps/sms/views/shared/js/inter_instance_event_dispatcher.js
Executable file
83
gaia/apps/sms/views/shared/js/inter_instance_event_dispatcher.js
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
/* global BroadcastChannel,
|
||||
EventDispatcher
|
||||
*/
|
||||
|
||||
/* exported InterInstanceEventDispatcher */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Channel that is used to broadcast messages between message app instances.
|
||||
* @type {BroadCastChannel}
|
||||
*/
|
||||
var channel = null;
|
||||
|
||||
const BROADCAST_CHANNEL_NAME = 'messages-channel';
|
||||
const ALLOWED_EVENTS = ['draft-saved', 'draft-deleted'];
|
||||
|
||||
function postMessage(name, parameters) {
|
||||
if (!channel) {
|
||||
throw new Error('Channel is not created!');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Name should be defined!');
|
||||
}
|
||||
|
||||
channel.postMessage(parameters !== undefined ?
|
||||
{ name: name, parameters: parameters } :
|
||||
{ name: name }
|
||||
);
|
||||
}
|
||||
|
||||
const Dispatcher = EventDispatcher.mixin({
|
||||
connect: function() {
|
||||
if (channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
||||
channel.addEventListener('message', onMessage);
|
||||
|
||||
exports.addEventListener('unload', () => this.disconnect());
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.close();
|
||||
channel.removeEventListener('message', onMessage);
|
||||
|
||||
channel = null;
|
||||
}
|
||||
}, ALLOWED_EVENTS);
|
||||
|
||||
// Save and override original emit method to avoid firing events in the same
|
||||
// application instance.
|
||||
const originalEmitter = Dispatcher.emit.bind(Dispatcher);
|
||||
|
||||
Dispatcher.emit = function(eventName, parameters) {
|
||||
if (ALLOWED_EVENTS.indexOf(eventName) < 0) {
|
||||
throw new Error('Event "' + eventName + '" is not allowed!');
|
||||
}
|
||||
|
||||
postMessage(eventName, parameters);
|
||||
};
|
||||
|
||||
function onMessage(message) {
|
||||
try {
|
||||
originalEmitter(message.data.name, message.data.parameters);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error while emitting event "%s" with parameters "%s"',
|
||||
message.data && message.data.name,
|
||||
JSON.stringify(message.data && message.data.parameters),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exports.InterInstanceEventDispatcher = Dispatcher;
|
||||
})(window);
|
||||
93
gaia/apps/sms/views/shared/js/localization_helper.js
Executable file
93
gaia/apps/sms/views/shared/js/localization_helper.js
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
/* exported LocalizationHelper */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Look for attachment iframes and localize them - l20n.js doesn't do this
|
||||
* XXX: remove once bug 1020130 is fixed
|
||||
*/
|
||||
function localizeAttachmentIFrames() {
|
||||
var attachmentContainers = Array.from(document.querySelectorAll(
|
||||
'iframe.attachment-container'
|
||||
));
|
||||
|
||||
return Promise.all(
|
||||
attachmentContainers.map(localizeAttachmentIFrame)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize an attachment iframe
|
||||
*/
|
||||
function localizeAttachmentIFrame(attachmentContainer) {
|
||||
var doc = attachmentContainer.contentDocument;
|
||||
doc.documentElement.lang = document.documentElement.getAttribute('lang');
|
||||
doc.documentElement.dir = document.documentElement.getAttribute('dir');
|
||||
return document.l10n.translateFragment(doc.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for time elements with format and re-translate the date message.
|
||||
* More complex because the argument to the l10n string is itself a formatted
|
||||
* date using l10n.
|
||||
* @param {boolean} isTimeFormatChanged Indicates that time format has been
|
||||
* changed and we need to update only elements with time aware formats.
|
||||
*/
|
||||
function localizeDateTime(isTimeFormatChanged) {
|
||||
var dateTimeElements = document.querySelectorAll('.l10n-contains-date');
|
||||
|
||||
Array.forEach(dateTimeElements, function(element) {
|
||||
if (!element.dataset.l10nDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
var formatL10nId = element.dataset.l10nDateFormat;
|
||||
|
||||
if (!formatL10nId) {
|
||||
return;
|
||||
}
|
||||
|
||||
formatL10nId = JSON.parse(formatL10nId);
|
||||
|
||||
if (isTimeFormatChanged && !formatL10nId.hour) {
|
||||
return;
|
||||
}
|
||||
formatL10nId.hour12 = navigator.mozHour12;
|
||||
|
||||
var formatter = new Intl.DateTimeFormat(
|
||||
navigator.languages, formatL10nId
|
||||
);
|
||||
var localeData = formatter.format(new Date(+element.dataset.l10nDate));
|
||||
|
||||
if (element.hasAttribute('data-l10n-id') &&
|
||||
element.hasAttribute('data-l10n-args')) {
|
||||
var l10nAttrs = document.l10n.getAttributes(element);
|
||||
l10nAttrs.args.date = localeData;
|
||||
document.l10n.setAttributes(element, l10nAttrs.id, l10nAttrs.args);
|
||||
} else {
|
||||
element.textContent = localeData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.LocalizationHelper = {
|
||||
init: function () {
|
||||
function localize() {
|
||||
localizeDateTime();
|
||||
return localizeAttachmentIFrames();
|
||||
}
|
||||
|
||||
// After startup, call localize every time the language changes
|
||||
document.addEventListener('DOMRetranslated', localize);
|
||||
|
||||
// This event is fired when time format (12h/24h) is changed
|
||||
window.addEventListener(
|
||||
'timeformatchange',
|
||||
localizeDateTime.bind(null, /* isTimeFormatChanged */ true)
|
||||
);
|
||||
|
||||
// Call localize during startup
|
||||
return document.l10n.ready.then(localize);
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
906
gaia/apps/sms/views/shared/js/navigation.js
Executable file
906
gaia/apps/sms/views/shared/js/navigation.js
Executable file
|
|
@ -0,0 +1,906 @@
|
|||
/*global
|
||||
EventDispatcher,
|
||||
Utils
|
||||
*/
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
/* The factory will be used in unit tests. */
|
||||
var NavigationFactory = window.NavigationFactory = function(window) {
|
||||
|
||||
/**
|
||||
* @function
|
||||
* The debug function translates to console.log in debug mode.
|
||||
*
|
||||
* @param {String} arg1 The first value to be displayed to the console.
|
||||
* @param {...*} args The other values to be displayed to the console, or the
|
||||
* parameters to the first string.
|
||||
*/
|
||||
const debug = [
|
||||
() => {},
|
||||
(arg1, ...args) => console.log('[Navigation] ' + arg1, ...args),
|
||||
(arg1, ...args) =>
|
||||
console.log('[Navigation] (%s) ' + arg1, Date.now(), ...args),
|
||||
][0];
|
||||
|
||||
/**
|
||||
* This contains an array of view characteristics, necessary to handle
|
||||
* navigation requests.
|
||||
* Each item has the following properties:
|
||||
* - name: this is the name of the view. This will be used also in the URL
|
||||
* hash if the same HTML file holds several views.
|
||||
* - url: this is the URL used to match the current location and find the
|
||||
* current view.
|
||||
* - behavior: this is the actual JavaScript object that manages this view.
|
||||
* - previous (optional): this is the name of the previous view; this is
|
||||
* necessary to decide if we do a forward/backward animation.
|
||||
* - partOf (optional): this is used when a view is part of another view's
|
||||
* HTML document. In that case one view is "master" and the other are only
|
||||
* part of the master view.
|
||||
*
|
||||
* NGA_VIEWS holds the configuration for NGA split views.
|
||||
*/
|
||||
const NGA_VIEWS = Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'thread',
|
||||
url: 'views/conversation/index.html',
|
||||
behavior: 'ConversationView',
|
||||
previous: 'thread-list'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'thread-list',
|
||||
url: 'views/inbox/index.html',
|
||||
behavior: 'InboxView'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'report-view',
|
||||
behavior: 'ReportView',
|
||||
previous: 'thread',
|
||||
partOf: 'thread'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'composer',
|
||||
behavior: 'ConversationView',
|
||||
previous: 'thread-list',
|
||||
partOf: 'thread'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'group-view',
|
||||
behavior: 'GroupView',
|
||||
previous: 'thread',
|
||||
partOf: 'thread'
|
||||
})
|
||||
]);
|
||||
|
||||
/**
|
||||
* OA_VIEWS holds the configuration for the old non-split architecture.
|
||||
*/
|
||||
const OA_VIEWS = Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'thread-list',
|
||||
url: 'index.html',
|
||||
behavior: 'InboxView'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'thread',
|
||||
behavior: 'ConversationView',
|
||||
previous: 'thread-list',
|
||||
partOf: 'thread-list'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'composer',
|
||||
behavior: 'ConversationView',
|
||||
previous: 'thread-list',
|
||||
partOf: 'thread-list'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'group-view',
|
||||
behavior: 'GroupView',
|
||||
previous: 'thread',
|
||||
partOf: 'thread-list'
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'report-view',
|
||||
behavior: 'ReportView',
|
||||
previous: 'thread',
|
||||
partOf: 'thread-list'
|
||||
})
|
||||
]);
|
||||
|
||||
const VIEWS = isUsingOldArchitecture() ? OA_VIEWS : NGA_VIEWS;
|
||||
|
||||
const priv = {
|
||||
pendingInit: Symbol('pendingInit')
|
||||
};
|
||||
|
||||
var currentState;
|
||||
var navigationTransition;
|
||||
|
||||
/**
|
||||
* Navigate to this URL.
|
||||
*
|
||||
* @param {String} url URL to load.
|
||||
*/
|
||||
function setLocation(url) {
|
||||
debug('Setting location', url);
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* We're using the old architecture if we're not in a split document.
|
||||
*
|
||||
* @returns {Boolean} true if we're using the old architecture.
|
||||
*/
|
||||
function isUsingOldArchitecture() {
|
||||
debug('isUsingOldArchitecture()');
|
||||
var pathname = window.location.pathname;
|
||||
var result = (pathname === '/' || pathname === '/index.html');
|
||||
debug('isUsingOldArchitecture() =>', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses hash to find a view name.
|
||||
* It takes the part of the hash between '/' or the start of the string, and '?'
|
||||
* or the end of the string.
|
||||
*
|
||||
* Examples:
|
||||
* #/conversation?id=5 => conversation
|
||||
* #/composer?recipient=+33615647897 => composer
|
||||
* #/composer => composer
|
||||
*
|
||||
* @param {String} hash The string to analyze.
|
||||
* @returns {String} The analyzed string. It is not guaranteed to be an existing
|
||||
* view name.
|
||||
*/
|
||||
function findViewNameFromHash(hash) {
|
||||
debug('findViewNameFromHash(%s)', hash);
|
||||
if (!hash || !hash.startsWith('#/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var endOfViewName = hash.indexOf('?');
|
||||
if (endOfViewName < 0) {
|
||||
endOfViewName = undefined;
|
||||
}
|
||||
|
||||
return hash.slice(2, endOfViewName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the correct view from the location.
|
||||
*
|
||||
* First it tries to find the view using the location's hash.
|
||||
* If findViewNameFromHash returns something, we try to match an existing view
|
||||
* from this name, then we check whether the found view is indeed part of this
|
||||
* document using the full URL.
|
||||
*
|
||||
* If findViewNameFromHash returns nothing, we try to find a view from the full
|
||||
* URL.
|
||||
*
|
||||
* @param {Location} location The content of window.location.
|
||||
* @returns {Object} The found view, or null.
|
||||
*/
|
||||
function findViewFromLocation(location) {
|
||||
debug('findViewFromLocation(%s)', location.href);
|
||||
|
||||
var pathName = location.pathname;
|
||||
if (pathName.endsWith('/')) {
|
||||
pathName += 'index.html';
|
||||
}
|
||||
|
||||
var hash = location.hash;
|
||||
var viewNameFromHash = findViewNameFromHash(hash);
|
||||
var viewFromHash = findViewFromName(viewNameFromHash);
|
||||
if (viewNameFromHash && !viewFromHash) {
|
||||
console.error('We found no view from the hash %s', hash);
|
||||
return null;
|
||||
}
|
||||
|
||||
var viewFromPath = VIEWS.find(
|
||||
(view) => pathName.endsWith('/' + view.url)
|
||||
) || null;
|
||||
|
||||
if (!viewFromPath) {
|
||||
console.error('We found no view from the path %s', pathName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewFromHash && viewFromHash.partOf &&
|
||||
viewFromHash.partOf !== viewFromPath.name) {
|
||||
console.error(
|
||||
'The view %s found from the hash %s is not part of view %s',
|
||||
viewFromHash.name, hash, viewFromPath.name
|
||||
);
|
||||
viewFromHash = null;
|
||||
}
|
||||
|
||||
return viewFromHash || viewFromPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a view from its name.
|
||||
*
|
||||
* @param {String} name Name of the view to find.
|
||||
* @returns {Object} The found view, or null.
|
||||
*/
|
||||
function findViewFromName(name) {
|
||||
debug('findViewFromName(%s)', name);
|
||||
var view = VIEWS.find((view) => view.name === name) || null;
|
||||
|
||||
debug('findViewFromName() => %s', view && view.name);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the container view of a view.
|
||||
*
|
||||
* @param {Object} view A view or a subview.
|
||||
* @returns {Object} the container view of the parameter view. It can be the
|
||||
* same object if the parameter is itself the container view.
|
||||
*/
|
||||
function findContainerView(view) {
|
||||
debug('findContainerView(%s)', view.name);
|
||||
if (view.partOf) {
|
||||
debug('findContainerView: %s is part of view %s', view.name, view.partOf);
|
||||
view = findViewFromName(view.partOf);
|
||||
}
|
||||
|
||||
debug('findContainerView() => %s', view.name);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Defer}
|
||||
* Used at startup time to indicate when the app loaded all dependencies.
|
||||
* Will be nullified once the app is ready.
|
||||
* @see Navigation.setReady
|
||||
* @see waitForReady
|
||||
*/
|
||||
var readyDefer = Utils.Promise.defer();
|
||||
|
||||
/**
|
||||
* @returns {Promise} Resolved when the app is ready, rejected if the app is
|
||||
* ready at call time.
|
||||
*/
|
||||
function waitForReady() {
|
||||
if (!readyDefer) {
|
||||
return Promise.reject(
|
||||
new Error('waitForReady was called but status is ready already.')
|
||||
);
|
||||
}
|
||||
|
||||
return readyDefer.promise;
|
||||
}
|
||||
|
||||
function startNavigationFromCurrentLocation() {
|
||||
var location = window.location;
|
||||
debug('startNavigationFromCurrentLocation(), location=%s', location);
|
||||
|
||||
var newPanel = findViewFromLocation(location);
|
||||
if (!newPanel) {
|
||||
return Promise.reject(
|
||||
new Error('Couldn\'t find a view from location ' + location.href)
|
||||
);
|
||||
}
|
||||
|
||||
var args = Utils.params(decodeURIComponent(location.hash));
|
||||
|
||||
debug(
|
||||
'startNavigationFromCurrentLocation:',
|
||||
'found panel', newPanel.name, 'with args', args
|
||||
);
|
||||
|
||||
return startNavigation({ newPanel, args });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the state to handle the navigation.
|
||||
* Generally we're waiting for any ongoing transition to end before starting a
|
||||
* new one. However if the ongoing transition was started with opts.partial ==
|
||||
* true, we merely augment the ongoing transition with the new information,
|
||||
* especially arguments.
|
||||
*
|
||||
* @param {Object} opts options
|
||||
* @param {String} [opts.newPanel] The panel we'll switch to. This can be
|
||||
* omitted in case we only want to execute beforeLeave.
|
||||
* @param {Object} [opts.args] The arguments we'll use for the next panel.
|
||||
* @param {Boolean} [opts.partial] If present and true, this new navigation
|
||||
* request is a partial one missing information (eg arguments) and we'll call
|
||||
* startNavigation once again without this property once we'll know the missing
|
||||
* bits.
|
||||
*
|
||||
* @returns {Promise} Resolved when we could start the navigation (when a
|
||||
* previous ongoing one is finished).
|
||||
*/
|
||||
function startNavigation(opts) {
|
||||
var { newPanel, args, partial } = opts;
|
||||
|
||||
args = args || {};
|
||||
|
||||
debug(
|
||||
'startNavigation(newPanel=%s, partial=%s)',
|
||||
newPanel && newPanel.name, partial
|
||||
);
|
||||
if (navigationTransition && (!navigationTransition.partial || partial)) {
|
||||
debug('startNavigation: previous navigation exists, deferring.');
|
||||
return navigationTransition.defer.promise.then(
|
||||
() => startNavigation(opts)
|
||||
);
|
||||
}
|
||||
|
||||
var newContainerView = newPanel && findContainerView(newPanel);
|
||||
var containerView = currentState && findContainerView(currentState.panel);
|
||||
var leaveDocument = currentState && containerView !== newContainerView;
|
||||
|
||||
if (newPanel && !leaveDocument) {
|
||||
var behaviorObject = window[newPanel.behavior];
|
||||
var viewReady =
|
||||
behaviorObject &&
|
||||
(typeof behaviorObject.isReady !== 'function' ||
|
||||
behaviorObject.isReady());
|
||||
|
||||
if (!viewReady) {
|
||||
// This panel is not ready yet, let's wait until the app is ready.
|
||||
return waitForReady().then(
|
||||
() => startNavigation(opts)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the focus before navigating.
|
||||
document.activeElement.blur();
|
||||
|
||||
if (!navigationTransition) {
|
||||
navigationTransition = {
|
||||
stepsRun: [],
|
||||
defer: Utils.Promise.defer()
|
||||
};
|
||||
}
|
||||
|
||||
navigationTransition.partial = partial;
|
||||
navigationTransition.leaveDocument = leaveDocument;
|
||||
|
||||
if ('meta' in args) {
|
||||
throw new Error('Arguments cannot have a `meta` property.');
|
||||
}
|
||||
|
||||
navigationTransition.transitionArgs = {
|
||||
meta: {
|
||||
next: newPanel ? {
|
||||
panel: newPanel.name,
|
||||
args
|
||||
} : null,
|
||||
prev: currentState ? {
|
||||
panel: currentState.panel.name,
|
||||
args: currentState.args
|
||||
} : null
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(navigationTransition.transitionArgs, args);
|
||||
|
||||
navigationTransition.states = {
|
||||
from: currentState,
|
||||
to: {
|
||||
panel: newPanel,
|
||||
args
|
||||
}
|
||||
};
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currentState variable to its new value.
|
||||
*/
|
||||
function setFutureState() {
|
||||
debug('setFutureState()');
|
||||
currentState = navigationTransition.states.to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the navigation so that new navigations can take place.
|
||||
* This cleans up the navigation states.
|
||||
*/
|
||||
function endNavigation() {
|
||||
debug('endNavigation()');
|
||||
if (navigationTransition) {
|
||||
navigationTransition.defer.resolve();
|
||||
navigationTransition = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to catch errors at the end of navigation promise
|
||||
* chains. We're ending the navigation if this happens, so that we can start a
|
||||
* new one afterwards.
|
||||
*
|
||||
* @param {Error} [e] The exception to display, if any.
|
||||
*/
|
||||
function onNavigationError(e = new Error('RejectedPromise')) {
|
||||
console.error('Navigation error', e);
|
||||
endNavigation();
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used as a rejection handler, to swallow errors and log them.
|
||||
*
|
||||
* @param {Exception} [e] The exception to display, if any.
|
||||
*/
|
||||
function catchStepError(e) {
|
||||
if (e) {
|
||||
console.error(
|
||||
'[Navigation] got an exception: (%s) %s',
|
||||
e, e.stack && e.stack.replace(/\n/g, '|')
|
||||
);
|
||||
} else {
|
||||
debug('[Navigation] got a rejected promise');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the lifecycle function for `stepName` on the right object.
|
||||
*
|
||||
* @param {String} stepName The step to execute: one of 'beforeLeave',
|
||||
* 'beforeEnter', 'afterLeave', 'afterEnter'.
|
||||
* @returns {Promise} Resolved when the step returns a value or a resolved
|
||||
* promise, rejected when the step either throws or returns a rejected promise.
|
||||
* Or a resolved promise if no step was executed (because it was already
|
||||
* executed or there was no function to execute).
|
||||
*/
|
||||
function executeNavigationStep(stepName) {
|
||||
debug('executeNavigationStep(%s)', stepName);
|
||||
if (!navigationTransition) {
|
||||
return Promise.reject(new Error(
|
||||
'Trying to execute the navigation step ' + stepName +
|
||||
' but no transition is started yet.'
|
||||
));
|
||||
}
|
||||
|
||||
if (navigationTransition.stepsRun.indexOf(stepName) >= 0) {
|
||||
// this step has already run (case of back -> onHashChange)
|
||||
debug('executeNavigationStep: step %s has already run.', stepName);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var stateKey = stepName.endsWith('Leave') ? 'from' : 'to';
|
||||
var state = navigationTransition.states[stateKey];
|
||||
if (!state) {
|
||||
debug('Nothing to execute for step', stepName);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var panel = state.panel;
|
||||
|
||||
var resultPromise = Promise.resolve();
|
||||
var viewObject = window[panel.behavior];
|
||||
if (viewObject[stepName]) {
|
||||
// encapsulating as a then handler to catch exceptions.
|
||||
resultPromise = resultPromise.then(
|
||||
() => viewObject[stepName](navigationTransition.transitionArgs)
|
||||
);
|
||||
}
|
||||
|
||||
return resultPromise.then(() => {
|
||||
navigationTransition.stepsRun.push(stepName);
|
||||
debug(`Finished executeNavigationStep(${stepName})`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in `waitForPanelAnimation` to help solving race conditions.
|
||||
* @type {Defer}
|
||||
*/
|
||||
var previousAnimationDefer;
|
||||
|
||||
/**
|
||||
* Wait for the end of an animation on `panelElement`.
|
||||
*
|
||||
* @param {Element} panelElement The element to watch.
|
||||
* @returns {Promise} Resolves when the animation ends.
|
||||
*/
|
||||
function waitForPanelAnimation(panelElement) {
|
||||
if (previousAnimationDefer) {
|
||||
previousAnimationDefer.reject(new Error('A new animation started'));
|
||||
}
|
||||
|
||||
var defer = Utils.Promise.defer();
|
||||
previousAnimationDefer = defer;
|
||||
|
||||
var timeout;
|
||||
|
||||
function onAnimationEnd(e) {
|
||||
panelElement.removeEventListener('animationend', onAnimationEnd);
|
||||
clearTimeout(timeout);
|
||||
debug('animationend', e && e.type);
|
||||
defer.resolve();
|
||||
}
|
||||
|
||||
panelElement.addEventListener('animationend', onAnimationEnd);
|
||||
timeout = setTimeout(onAnimationEnd, 500);
|
||||
|
||||
return defer.promise.then(() => previousAnimationDefer = null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides current panel, shows new panel.
|
||||
* It will use an animation if we change panels in the same document.
|
||||
*
|
||||
* @returns {Promise} Resolves when the animation is over, if there is an
|
||||
* animation. Otherwise returns a resolved promise.
|
||||
*/
|
||||
function switchPanel() {
|
||||
debug('switchPanel()');
|
||||
if (!navigationTransition) {
|
||||
throw new Error(
|
||||
'Trying to switch panel but no transition is started yet.'
|
||||
);
|
||||
}
|
||||
|
||||
var from = navigationTransition.states.from;
|
||||
var to = navigationTransition.states.to;
|
||||
|
||||
var oldView;
|
||||
var oldPanelElement;
|
||||
|
||||
if (from) {
|
||||
oldView = from.panel;
|
||||
oldPanelElement = document.querySelector(`.panel-${oldView.behavior}`);
|
||||
if (!oldPanelElement) {
|
||||
throw new Error(
|
||||
'Couldn\'t find the container element for view ' + oldView.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var newView = to.panel;
|
||||
if (!newView) {
|
||||
throw new Error(
|
||||
'Trying to switch panel, but no new view is defined!'
|
||||
);
|
||||
}
|
||||
|
||||
var newPanelElement = document.querySelector(`.panel-${newView.behavior}`);
|
||||
if (!newPanelElement) {
|
||||
return Promise.reject(new Error(
|
||||
'Couldn\'t find the container element for view ' + newView.name
|
||||
));
|
||||
}
|
||||
|
||||
if (newPanelElement === oldPanelElement) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var isGoingBack = oldView && oldView.previous === newView.name;
|
||||
|
||||
newPanelElement.style = '';
|
||||
newPanelElement.classList.remove('panel-hidden');
|
||||
newPanelElement.classList.add('panel-active');
|
||||
|
||||
var doSlideAnimation =
|
||||
from && from.panel &&
|
||||
from.panel.behavior !== newView.behavior;
|
||||
|
||||
var doFadeInAnimation = !from || !from.panel;
|
||||
|
||||
var animationPromise;
|
||||
if (doSlideAnimation) {
|
||||
var shouldGoRight = isGoingBack ^ (document.dir === 'rtl');
|
||||
newPanelElement.style.animationName =
|
||||
shouldGoRight ? 'left-to-current' : 'right-to-current';
|
||||
oldPanelElement.style.animationName =
|
||||
shouldGoRight ? 'current-to-right' : 'current-to-left';
|
||||
} else if (doFadeInAnimation) {
|
||||
newPanelElement.style.animationName = 'fade-in';
|
||||
}
|
||||
|
||||
if (doFadeInAnimation || doSlideAnimation) {
|
||||
animationPromise = waitForPanelAnimation(newPanelElement).catch(
|
||||
() => {}
|
||||
).then(() => {
|
||||
if (oldPanelElement) {
|
||||
oldPanelElement.style = '';
|
||||
}
|
||||
newPanelElement.style = '';
|
||||
});
|
||||
}
|
||||
|
||||
return (animationPromise || Promise.resolve()).then(() => {
|
||||
if (oldPanelElement) {
|
||||
oldPanelElement.classList.remove('panel-active');
|
||||
oldPanelElement.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
newPanelElement.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to hash changes.
|
||||
*
|
||||
* This is the heart of the navigation logic and handles running the various
|
||||
* navigation steps.
|
||||
* It also handles any unwanted errors. (TODO: needs to tune this a little
|
||||
* more).
|
||||
*/
|
||||
function onHashChange() {
|
||||
debug('onHashChange, location=', window.location);
|
||||
|
||||
startNavigationFromCurrentLocation().then(
|
||||
() => executeNavigationStep('beforeLeave')
|
||||
).then(
|
||||
() => executeNavigationStep('beforeEnter').catch(catchStepError)
|
||||
).then(
|
||||
setFutureState
|
||||
).then(
|
||||
switchPanel
|
||||
).then(
|
||||
() => executeNavigationStep('afterEnter').catch(catchStepError)
|
||||
).then(
|
||||
() => executeNavigationStep('afterLeave').catch(catchStepError)
|
||||
).then(
|
||||
endNavigation
|
||||
).then(
|
||||
() => Navigation.emit('navigated')
|
||||
).catch(onNavigationError); // TODO reset to previous hash or call back ?
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the hashchange event handler.
|
||||
*/
|
||||
function attachHistoryListener() {
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the hashchange event handler.
|
||||
*/
|
||||
function detachHistoryListener() {
|
||||
window.removeEventListener('hashchange', onHashChange);
|
||||
}
|
||||
|
||||
var Navigation = {
|
||||
/**
|
||||
* Calls `window.back` after running the `beforeLeave` step.
|
||||
*
|
||||
* We'll either (1) stay in the same document, or (2) move back to an earlier
|
||||
* document.
|
||||
*
|
||||
* In case 1), this will only change the hash, and then the onHashChange event
|
||||
* handler takes care of the end of the transition.
|
||||
* In case 2), the full URL will change, and the new window's init method will
|
||||
* take care of the end of the transition.
|
||||
*
|
||||
* @returns {Promise} Resolves when the action is done, if we stay in the same
|
||||
* document.
|
||||
*/
|
||||
back() {
|
||||
debug('back()');
|
||||
return startNavigation({ partial: true }).then(
|
||||
() => executeNavigationStep('beforeLeave')
|
||||
).then(
|
||||
() => window.history.back()
|
||||
).then(
|
||||
() => navigationTransition.defer.promise,
|
||||
onNavigationError
|
||||
);
|
||||
// todo reset navigation on "pageshow" event ?
|
||||
},
|
||||
|
||||
/**
|
||||
* Lifecycle methods are generally called in this order:
|
||||
* - previousPanel.beforeLeave
|
||||
* - nextPanel.beforeEnter
|
||||
* - nextPanel.afterEnter
|
||||
* - previousPanel.afterLeave
|
||||
*
|
||||
* If we stay in the same document and enter a new panel, the panels are slid
|
||||
* between beforeEnter and afterLeave.
|
||||
*
|
||||
* With the new architecture, the navigation can happen in one of 2 cases:
|
||||
* 1. either we stay in the same document,
|
||||
* 2. or we move to another document.
|
||||
*
|
||||
* In case 1), we only change the hash, and then the onHashChange event
|
||||
* handler takes care of the end of the transition.
|
||||
* In case 2), we change the full URL, and the new window's init method will
|
||||
* take care of the end of the transition.
|
||||
*
|
||||
* In all cases:
|
||||
* - beforeLeave can prevent changing the location and thus the
|
||||
* navigation. It is expected that beforeLeave does not change any state but
|
||||
* merely checks whether the transition can take place.
|
||||
* - beforeEnter will be called before the view is displayed. Therefore it can
|
||||
* make visual changes but it can't prevent a view from being entered. All
|
||||
* rejections and exceptions will be swallowed and output to the console.
|
||||
* - afterEnter does left-over actions
|
||||
* - afterLeave needs to clean up the panel, reclaim memory. It's called only
|
||||
* when the user stays in the same document.
|
||||
*
|
||||
* toPanel itself returns a Promise that will be resolved if the transition
|
||||
* happened and rejected otherwise.
|
||||
*
|
||||
* @param {String} viewName The view to load.
|
||||
* @param {Object} [args] The arguments to pass to the view.
|
||||
* @returns {Promise} Resolves when the transition is over, if we stay in the
|
||||
* same document.
|
||||
*/
|
||||
toPanel(viewName, args) {
|
||||
debug('toPanel(%s)', viewName);
|
||||
var view = findViewFromName(viewName);
|
||||
|
||||
if (!view) {
|
||||
return Promise.reject(
|
||||
new Error(`Couldn't find view ${viewName}`)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isCurrentPanel(viewName, args)) {
|
||||
debug('View %s is the current view.', viewName);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (navigationTransition) {
|
||||
debug('toPanel: previous navigation exists, deferring.');
|
||||
return navigationTransition.defer.promise.then(
|
||||
() => this.toPanel(viewName, args)
|
||||
);
|
||||
}
|
||||
|
||||
// Set pendingInit to false if we start to navigate to other panel.
|
||||
this[priv.pendingInit] = false;
|
||||
|
||||
var hash = '#';
|
||||
if (view.partOf) {
|
||||
hash += '/' + viewName;
|
||||
}
|
||||
hash = Utils.url(hash, args);
|
||||
|
||||
return startNavigation(
|
||||
{ newPanel: view, args, partial: true }
|
||||
).then(
|
||||
() => executeNavigationStep('beforeLeave')
|
||||
).then(() => {
|
||||
var nextLocation;
|
||||
if (navigationTransition.leaveDocument) {
|
||||
var containerView = findContainerView(view);
|
||||
nextLocation = '/' + containerView.url + hash;
|
||||
} else {
|
||||
nextLocation = hash;
|
||||
}
|
||||
|
||||
setLocation(nextLocation);
|
||||
}).then(
|
||||
() => navigationTransition.defer.promise,
|
||||
onNavigationError
|
||||
);
|
||||
// TODO do something on pageshow?
|
||||
//
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called when a view is started.
|
||||
*
|
||||
* It tries to find the right view from the location, and then initiates a
|
||||
* navigation from this.
|
||||
*/
|
||||
init() {
|
||||
debug('init()');
|
||||
|
||||
attachHistoryListener();
|
||||
|
||||
// Early reject when app is started via notification
|
||||
if (navigator.mozHasPendingMessage('notification')) {
|
||||
this[priv.pendingInit] = true;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
this[priv.pendingInit] = false;
|
||||
|
||||
return startNavigationFromCurrentLocation().then(
|
||||
// right away as we don't execute anything on the previous panel, and we
|
||||
// need a state at startup.
|
||||
setFutureState
|
||||
).then(
|
||||
() => executeNavigationStep('beforeEnter').catch(catchStepError)
|
||||
).then(
|
||||
switchPanel
|
||||
).then(
|
||||
() => executeNavigationStep('afterEnter').catch(catchStepError)
|
||||
).then(
|
||||
endNavigation
|
||||
).then(
|
||||
() => Navigation.emit('navigated')
|
||||
).catch(onNavigationError);
|
||||
},
|
||||
|
||||
[priv.pendingInit]: false,
|
||||
|
||||
hasPendingInit() {
|
||||
return this[priv.pendingInit];
|
||||
},
|
||||
|
||||
/* will be used by tests */
|
||||
cleanup() {
|
||||
detachHistoryListener();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if current location.hash corresponds to default panel. This is
|
||||
* sometimes called before we resolve the state, in that case resolve
|
||||
* directly using findViewNameFromHash. Otherwise we use the resolved state.
|
||||
*
|
||||
* @returns {boolean} True if current location.hash corresponds to default
|
||||
* panel.
|
||||
*/
|
||||
isDefaultPanel() {
|
||||
return currentState ?
|
||||
!currentState.panel.partOf :
|
||||
!findViewNameFromHash(window.location.hash);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called with a first argument that is not an object, it returns whether the
|
||||
* current panel is this first argument.
|
||||
*
|
||||
* Called with 2 arguments, the first one is not an object, the second one
|
||||
* must be an object. It returns whether the current panel is the first
|
||||
* argument, and for each property of the second argument, whether its value
|
||||
* is equal to the value for the current panel's arguments' same property.
|
||||
*
|
||||
* @param {String} panelName Checks if the current panel is this panelName.
|
||||
* @param {Object} [args] Checks against the current panel enter
|
||||
* arguments.
|
||||
* @returns {boolean} Result of the comparison.
|
||||
*/
|
||||
isCurrentPanel(panelName, args) {
|
||||
debug('isCurrentPanel(%s)', panelName);
|
||||
if (!currentState || !panelName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (panelName !== currentState.panel.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var currentArgs = currentState.args || {};
|
||||
|
||||
for (var arg in args) {
|
||||
if (args[arg] != currentArgs[arg]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by startup scripts to indicate when all lazy loaded files are
|
||||
* downloaded.
|
||||
*
|
||||
* TODO: lazyload the necessary files directly in this file.
|
||||
*/
|
||||
setReady() {
|
||||
debug('setReady()');
|
||||
|
||||
Array.from(document.querySelectorAll('.panel-hidden')).forEach((panel) => {
|
||||
panel.classList.remove('panel-hidden');
|
||||
});
|
||||
readyDefer.resolve();
|
||||
readyDefer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return EventDispatcher.mixin(Navigation, ['navigated']);
|
||||
|
||||
};
|
||||
|
||||
window.Navigation = NavigationFactory(window);
|
||||
})(window);
|
||||
101
gaia/apps/sms/views/shared/js/notify.js
Executable file
101
gaia/apps/sms/views/shared/js/notify.js
Executable file
|
|
@ -0,0 +1,101 @@
|
|||
/*global SettingsURL */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var SETTINGS = {
|
||||
notificationVolume: 'audio.volume.notification',
|
||||
vibration: 'vibration.enabled',
|
||||
ringtone: 'notification.ringtone'
|
||||
};
|
||||
|
||||
function entry(key, value) {
|
||||
if (urls[key]) {
|
||||
return new SettingsURL().set(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// one day we'll be able to use ES6 initializers instead
|
||||
var urls = {};
|
||||
urls[SETTINGS.ringtone] = 1;
|
||||
|
||||
function getSetting(key) {
|
||||
if (!navigator.mozSettings) {
|
||||
return Promise.reject(new Error('The mozSettings API is not available.'));
|
||||
}
|
||||
|
||||
var request = navigator.mozSettings.createLock().get(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = function() {
|
||||
resolve(entry(key, request.result[key]));
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
reject(new Error('Error while retrieving ' + key));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getSettings(settings) {
|
||||
return Promise.all(
|
||||
settings.map(getSetting)
|
||||
).catch((e) => {
|
||||
// catch and log errors
|
||||
console.error('Error while retrieving settings', e.message, e);
|
||||
return settings.map(() => null);
|
||||
}).then((results) => {
|
||||
return settings.reduce((result, setting, i) => {
|
||||
result[setting] = results[i];
|
||||
return result;
|
||||
}, {});
|
||||
});
|
||||
}
|
||||
|
||||
function ringtone(ringtoneFile) {
|
||||
var ringtonePlayer = new Audio();
|
||||
ringtonePlayer.src = ringtoneFile;
|
||||
ringtonePlayer.mozAudioChannelType = 'notification';
|
||||
ringtonePlayer.play();
|
||||
window.setTimeout(function smsRingtoneEnder() {
|
||||
ringtonePlayer.pause();
|
||||
ringtonePlayer.removeAttribute('src');
|
||||
ringtonePlayer.load();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function vibrate() {
|
||||
// vibration only works when App is in the foreground
|
||||
if (document.hidden) {
|
||||
window.addEventListener('visibilitychange', function waitOn() {
|
||||
window.removeEventListener('visibilitychange', waitOn);
|
||||
navigator.vibrate([200, 200, 200, 200]);
|
||||
});
|
||||
} else {
|
||||
navigator.vibrate([200, 200, 200, 200]);
|
||||
}
|
||||
}
|
||||
|
||||
var Notify = {
|
||||
ringtone: function notification_ringtone() {
|
||||
return getSettings(
|
||||
[SETTINGS.notificationVolume, SETTINGS.ringtone]
|
||||
).then((settings) => {
|
||||
if (settings[SETTINGS.notificationVolume] &&
|
||||
settings[SETTINGS.ringtone]) {
|
||||
ringtone(settings[SETTINGS.ringtone]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
vibrate: function notification_vibrate() {
|
||||
return getSettings([SETTINGS.vibration]).then((settings) => {
|
||||
if (settings[SETTINGS.vibration]) {
|
||||
vibrate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Notify = Notify;
|
||||
}(this));
|
||||
138
gaia/apps/sms/views/shared/js/selection_handler.js
Executable file
138
gaia/apps/sms/views/shared/js/selection_handler.js
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
/*exported SelectionHandler */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const REQUIRED_KEYS = [
|
||||
// elements
|
||||
'container',
|
||||
'checkUncheckAllButton',
|
||||
// methods
|
||||
'updateSelectionStatus',
|
||||
'getIdIterator',
|
||||
'isInEditMode'
|
||||
];
|
||||
|
||||
var SelectionHandler = function constructor(options) {
|
||||
// Set the necessary properties and method for selection module and throw
|
||||
// error if not existed in options.
|
||||
if (typeof options !== 'object') {
|
||||
throw new Error('options should be a valid object');
|
||||
}
|
||||
|
||||
REQUIRED_KEYS.forEach((key) => {
|
||||
if (options[key] === undefined) {
|
||||
throw new Error('Selection options does not provide required key ' + key);
|
||||
}
|
||||
|
||||
this[key] = options[key];
|
||||
});
|
||||
|
||||
this.selected = new Set();
|
||||
|
||||
this.container.addEventListener(
|
||||
'click', this.onSelected.bind(this)
|
||||
);
|
||||
this.checkUncheckAllButton.addEventListener(
|
||||
'click', this.toggleCheckedAll.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
SelectionHandler.prototype = {
|
||||
get selectedCount() {
|
||||
return this.selected.size;
|
||||
},
|
||||
|
||||
get selectedList() {
|
||||
return Array.from(this.selected);
|
||||
},
|
||||
|
||||
get inputIdSet() {
|
||||
var itemIdSet = new Set();
|
||||
for(var id of this.getIdIterator()) {
|
||||
itemIdSet.add(id.toString());
|
||||
}
|
||||
return itemIdSet;
|
||||
},
|
||||
|
||||
allSelected: function sel_allSelected() {
|
||||
/*jshint unused: false */
|
||||
var allItemCount = 0;
|
||||
var selectedCount = this.selectedCount;
|
||||
for (var id of this.getIdIterator()) {
|
||||
if (++allItemCount > selectedCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onSelected: function sel_onSelected(event) {
|
||||
if (!this.isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
var target = event.target;
|
||||
var value = target.value;
|
||||
var existed = this.selected.has(value);
|
||||
|
||||
if (target.checked && !existed) {
|
||||
this.selected.add(value);
|
||||
} else if (!target.checked && existed) {
|
||||
this.selected.delete(value);
|
||||
} else {
|
||||
// Don't emit event if no selection change
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectionStatus();
|
||||
},
|
||||
|
||||
select: function sel_select(id) {
|
||||
this.selected.add('' + id);
|
||||
},
|
||||
|
||||
unselect: function sel_unselect(id) {
|
||||
this.selected.delete('' + id);
|
||||
},
|
||||
|
||||
// Update check status of input elements in the container
|
||||
updateCheckboxes: function sel_updateCheckboxes() {
|
||||
var inputs = this.container.querySelectorAll('input[type=checkbox]');
|
||||
var length = inputs.length;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
inputs[i].checked = this.selected.has(inputs[i].value);
|
||||
}
|
||||
},
|
||||
|
||||
// if no message or few are checked : select all the messages
|
||||
// and if all messages are checked : deselect them all.
|
||||
toggleCheckedAll: function sel_toggleCheckedAll() {
|
||||
var allInputs = this.inputIdSet;
|
||||
var allSelected = (this.selected.size === allInputs.size);
|
||||
|
||||
if (allSelected) {
|
||||
this.selected.clear();
|
||||
} else {
|
||||
this.selected = allInputs;
|
||||
}
|
||||
|
||||
this.updateCheckboxes();
|
||||
|
||||
this.updateSelectionStatus();
|
||||
},
|
||||
|
||||
cleanForm: function sel_cleanForm() {
|
||||
// Reset all inputs
|
||||
this.selected.clear();
|
||||
|
||||
this.updateCheckboxes();
|
||||
|
||||
// Reset vars for deleting methods
|
||||
this.updateSelectionStatus();
|
||||
}
|
||||
};
|
||||
|
||||
exports.SelectionHandler = SelectionHandler;
|
||||
|
||||
}(this));
|
||||
180
gaia/apps/sms/views/shared/js/settings.js
Executable file
180
gaia/apps/sms/views/shared/js/settings.js
Executable file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Message app settings related value and utilities.
|
||||
*/
|
||||
|
||||
/* exported Settings */
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
var Settings = {
|
||||
SERVICE_ID_KEYS: {
|
||||
mmsServiceId: 'ril.mms.defaultServiceId',
|
||||
smsServiceId: 'ril.sms.defaultServiceId'
|
||||
},
|
||||
|
||||
READ_AHEAD_THREADS_KEY: 'ril.sms.maxReadAheadEntries',
|
||||
|
||||
// we evaluate to 5KB the size overhead of wrapping a payload in a MMS
|
||||
MMS_SIZE_OVERHEAD: 5 * 1024,
|
||||
|
||||
_serviceIds: null,
|
||||
|
||||
// we need to remove this when email functionality is ready.
|
||||
supportEmailRecipient: true,
|
||||
|
||||
// We set the default maximum concatenated number of our SMS app to 10
|
||||
// based on:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=813686#c0
|
||||
maxConcatenatedMessages: 10,
|
||||
mmsSizeLimitation: 295 * 1024, // Default mms message size limitation is 295K
|
||||
mmsServiceId: null, // Default mms service SIM ID
|
||||
smsServiceId: null, // Default sms service SIM ID
|
||||
|
||||
init: function settings_init() {
|
||||
var keyHandlerSet = {
|
||||
'dom.mms.operatorSizeLimitation': this.initMmsSizeLimitation.bind(this),
|
||||
'operatorResource.sms.maxConcat':
|
||||
this.initSmsMaxConcatenatedMsg.bind(this)
|
||||
};
|
||||
var settings = navigator.mozSettings;
|
||||
var conns = navigator.mozMobileConnections;
|
||||
|
||||
function setHandlerMap(key) {
|
||||
var req = settings.createLock().get(key);
|
||||
req.onsuccess = function settings_getSizeSuccess() {
|
||||
var handler = keyHandlerSet[key];
|
||||
handler(req.result[key]);
|
||||
};
|
||||
}
|
||||
|
||||
this._serviceIds = [];
|
||||
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only DSDS will need to handle mmsServiceId
|
||||
if (conns && conns.length > 1) {
|
||||
for (var prop in this.SERVICE_ID_KEYS) {
|
||||
var setting = this.SERVICE_ID_KEYS[prop];
|
||||
keyHandlerSet[setting] = this.initServiceId.bind(this, setting, prop);
|
||||
}
|
||||
|
||||
// Cache all existing serviceIds
|
||||
for (var i = 0, l = conns.length; i < l; i++) {
|
||||
this._serviceIds.push(conns[i].iccId);
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in keyHandlerSet) {
|
||||
setHandlerMap(key);
|
||||
}
|
||||
},
|
||||
|
||||
//Set Maximum concatenated number of our SMS
|
||||
initSmsMaxConcatenatedMsg: function initSmsMaxConcatenatedMsg(num) {
|
||||
if (num && !isNaN(num)) {
|
||||
this.maxConcatenatedMessages = num;
|
||||
}
|
||||
},
|
||||
|
||||
// Set MMS size limitation:
|
||||
// If operator does not specify MMS message size, we leave the decision to
|
||||
// MessageManager and return nothing if we can't get size limitation from db
|
||||
initMmsSizeLimitation: function initMmsSizeLimitation(size) {
|
||||
if (size && !isNaN(size)) {
|
||||
this.mmsSizeLimitation = size - this.MMS_SIZE_OVERHEAD;
|
||||
}
|
||||
},
|
||||
|
||||
// Set default mms service SIM ID and add observer:
|
||||
// In DSDS scenario, if we notify user to switch to subscription to retrieve
|
||||
// the MMS from non-active subscription, we'll need current mmsServiceId
|
||||
// information to tell user the active/non-active subscription
|
||||
initServiceId: function initMmsServiceId(settingName, propName, id) {
|
||||
if (id !== undefined) {
|
||||
Settings[propName] = id;
|
||||
}
|
||||
navigator.mozSettings.addObserver(settingName, function(e) {
|
||||
Settings[propName] = e.settingValue;
|
||||
});
|
||||
},
|
||||
|
||||
setMmsSimServiceId: function setSimServiceId(id) {
|
||||
// DSDS: mms & data are both necessary for connection switch.
|
||||
navigator.mozSettings.createLock().set({
|
||||
'ril.mms.defaultServiceId': id,
|
||||
'ril.data.defaultServiceId': id
|
||||
});
|
||||
},
|
||||
|
||||
switchMmsSimHandler: function switchSimHandler(targetId) {
|
||||
var conn = window.navigator.mozMobileConnections[targetId];
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (conn) {
|
||||
if (conn.data.state === 'registered') {
|
||||
// Call resolve directly if state is registered already
|
||||
resolve();
|
||||
} else {
|
||||
// Listen to MobileConnections datachange to make sure we can start
|
||||
// to retrieve mms only when data.state is registered. But we can't
|
||||
// guarantee datachange event will work in other device.
|
||||
conn.addEventListener('datachange', function onDataChange() {
|
||||
if (conn.data.state === 'registered') {
|
||||
conn.removeEventListener('datachange', onDataChange);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.setMmsSimServiceId(targetId);
|
||||
}
|
||||
} else {
|
||||
reject('Invalid connection');
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* returns true if the device has more than 1 SIM port
|
||||
*/
|
||||
isDualSimDevice: function isDualSimDevice() {
|
||||
return this._serviceIds && this._serviceIds.length > 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the device has more than 1 SIM port and at least 2 SIMs are
|
||||
* inserted.
|
||||
*/
|
||||
hasSeveralSim: function hasSeveralSim() {
|
||||
if (!this.isDualSimDevice()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var simCount = this._serviceIds.reduce(function(simCount, iccId) {
|
||||
return iccId === null ? simCount : simCount + 1;
|
||||
}, 0);
|
||||
|
||||
return simCount > 1;
|
||||
},
|
||||
|
||||
getServiceIdByIccId: function getServiceIdByIccId(iccId) {
|
||||
if (!this._serviceIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var index = this._serviceIds.indexOf(iccId);
|
||||
|
||||
return index > -1 ? index : null;
|
||||
},
|
||||
|
||||
setReadAheadThreadRetrieval: function(value) {
|
||||
if (!navigator.mozSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
var setting = {};
|
||||
setting[this.READ_AHEAD_THREADS_KEY] = value;
|
||||
navigator.mozSettings.createLock().set(setting);
|
||||
}
|
||||
};
|
||||
35
gaia/apps/sms/views/shared/js/shared_components.js
Executable file
35
gaia/apps/sms/views/shared/js/shared_components.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
/*global Map, Template*/
|
||||
|
||||
/*exported SharedComponents */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var cachedTemplates = new Map();
|
||||
|
||||
var getTemplate = function(id) {
|
||||
if (!cachedTemplates.has(id)) {
|
||||
cachedTemplates.set(id, Template(id));
|
||||
}
|
||||
return cachedTemplates.get(id);
|
||||
};
|
||||
|
||||
var SharedComponents = {
|
||||
phoneDetails: function(details, interpolationOptions) {
|
||||
if (!details) {
|
||||
throw new Error('Phone details are required!');
|
||||
}
|
||||
|
||||
return getTemplate('phone-details-tmpl').prepare({
|
||||
type: details.type || '',
|
||||
number: details.number || '',
|
||||
carrier: details.carrier || '',
|
||||
hasTypeClass: !!details.type ? 'has-phone-type' : '',
|
||||
hasCarrierClass: !!details.carrier ? 'has-phone-carrier' : ''
|
||||
}, interpolationOptions);
|
||||
}
|
||||
};
|
||||
|
||||
exports.SharedComponents = SharedComponents;
|
||||
})(window);
|
||||
18
gaia/apps/sms/views/shared/js/shim_host_bootstrap.js
Executable file
18
gaia/apps/sms/views/shared/js/shim_host_bootstrap.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
/* global ActivityShim,
|
||||
MozMobileConnectionsShim,
|
||||
MozMobileMessageShim,
|
||||
MozSettingsShim
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.bootstrap = function(appInstanceId) {
|
||||
// We can't handle system messages inside iframe right now, so we access
|
||||
// parent window here. This should be removed once bug 818000 is resolved.
|
||||
ActivityShim.init(appInstanceId, parent.mozSetMessageHandler);
|
||||
MozSettingsShim.init(navigator.mozSettings);
|
||||
MozMobileMessageShim.init(appInstanceId, navigator.mozMobileMessage);
|
||||
MozMobileConnectionsShim.init(appInstanceId, navigator.mozMobileConnections);
|
||||
};
|
||||
})(self);
|
||||
165
gaia/apps/sms/views/shared/js/silent_sms.js
Executable file
165
gaia/apps/sms/views/shared/js/silent_sms.js
Executable file
|
|
@ -0,0 +1,165 @@
|
|||
/* global asyncStorage, Promise, TaskRunner */
|
||||
/* exported SilentSms */
|
||||
'use strict';
|
||||
|
||||
var SilentSms = (function () {
|
||||
var NO_ALARM_ID = -1;
|
||||
// The maximum amount of time that the SmsManager waits for a message before
|
||||
// disabling automatically the silent mode is five minutes.
|
||||
var SERVICE_TIMEOUT = 5 * 60 * 1000;
|
||||
var taskQueue;
|
||||
|
||||
function init() {
|
||||
taskQueue = new TaskRunner();
|
||||
if (!window.navigator.mozSetMessageHandler) {
|
||||
// bailout if we're in Firefox
|
||||
return;
|
||||
}
|
||||
|
||||
// Msg from Usage (activation / deactivation)
|
||||
window.navigator.mozSetMessageHandler('connection',
|
||||
function onConnected(request) {
|
||||
if (request.keyword !== 'costcontrolSmsQuery') {
|
||||
return;
|
||||
}
|
||||
var port = request.port;
|
||||
port.onmessage = function onReceivedMessage(evt) {
|
||||
var data = evt.data;
|
||||
switch (data.action) {
|
||||
case 'enable':
|
||||
taskQueue.push(function() {
|
||||
return SilentSms.enableSilentModeFor(data.smsNumbers, data.type)
|
||||
.then(updateUsageSilentSms)
|
||||
.then(port.postMessage.bind(port, 'silentSMSEnabled'));
|
||||
});
|
||||
break;
|
||||
case 'disable':
|
||||
taskQueue.push(function() {
|
||||
return SilentSms.disableSilentModeFor(data.type)
|
||||
.then(updateUsageSilentSms)
|
||||
.then(port.postMessage.bind(port, 'silentSMSDisabled'));
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Deactivated silentSms mode by alarm
|
||||
window.navigator.mozSetMessageHandler('alarm', function _onAlarm(alarm) {
|
||||
if (alarm.data.alarmType === 'silentSmsTimeout') {
|
||||
SilentSms.disableSilentModeFor(alarm.data.type)
|
||||
.then(updateUsageSilentSms);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getUsageSilentSms() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
asyncStorage.getItem('usageSilentSms', function(usageSilentSms) {
|
||||
resolve(usageSilentSms || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeAlarmTimeout(alarmId) {
|
||||
// There is already an alarm, remove it
|
||||
if (alarmId && alarmId !== NO_ALARM_ID) {
|
||||
navigator.mozAlarms.remove(alarmId);
|
||||
}
|
||||
}
|
||||
|
||||
function addAlarmTimeout(type, delay) {
|
||||
return new Promise(function doEnablingSilentMode(resolve, reject) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + delay);
|
||||
var request = navigator.mozAlarms.add(date, 'ignoreTimezone',
|
||||
{alarmType: 'silentSmsTimeout',
|
||||
type: type });
|
||||
request.onsuccess = function _alarmSet(evt) {
|
||||
var alarmId = evt.target.result;
|
||||
resolve(alarmId);
|
||||
};
|
||||
request.onerror = function _onError() {
|
||||
var errorMsg = 'Failed to set timeout for ' + type + 'request';
|
||||
reject(new Error(errorMsg));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// The enabling silentMode process consist on two steps:
|
||||
// 1. Enabling silentMode for the provided number
|
||||
// 2. Activating alarm for TimeOut
|
||||
function enableSilentModeFor(smsNumbers, type) {
|
||||
function _removeAlarmTimeout(usageSilentSms) {
|
||||
if (usageSilentSms && usageSilentSms[type] &&
|
||||
usageSilentSms[type].alarmId !== NO_ALARM_ID) {
|
||||
removeAlarmTimeout(usageSilentSms[type].alarmId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleErrorAddingAlarm(error) {
|
||||
console.error(error.toString());
|
||||
return NO_ALARM_ID;
|
||||
}
|
||||
|
||||
return getUsageSilentSms().then(function(usageSilentSms) {
|
||||
function refreshUsageSilentSms(alarmId) {
|
||||
usageSilentSms[type] = {
|
||||
smsNumbers: smsNumbers,
|
||||
alarmId: alarmId
|
||||
};
|
||||
return usageSilentSms;
|
||||
}
|
||||
_removeAlarmTimeout(usageSilentSms);
|
||||
return addAlarmTimeout(type, SERVICE_TIMEOUT)
|
||||
.catch(handleErrorAddingAlarm)
|
||||
.then(refreshUsageSilentSms);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function disableSilentModeFor(type) {
|
||||
return getUsageSilentSms().then(function(usageSilentSms) {
|
||||
if (usageSilentSms && usageSilentSms[type] &&
|
||||
usageSilentSms[type].alarmId !== NO_ALARM_ID) {
|
||||
removeAlarmTimeout(usageSilentSms[type].alarmId);
|
||||
}
|
||||
usageSilentSms[type] = null;
|
||||
return usageSilentSms;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUsageSilentSms(usageSilentSms) {
|
||||
usageSilentSms.smsIndex = getIndex(usageSilentSms);
|
||||
asyncStorage.setItem('usageSilentSms', usageSilentSms);
|
||||
}
|
||||
|
||||
function getIndex(usageSilentSms) {
|
||||
var newIndex = [];
|
||||
Object.keys(usageSilentSms).forEach(function(silentRequest) {
|
||||
var silentSmsRequest = usageSilentSms[silentRequest];
|
||||
if (silentSmsRequest && silentSmsRequest.hasOwnProperty('smsNumbers')) {
|
||||
newIndex = newIndex.concat(silentSmsRequest.smsNumbers);
|
||||
}
|
||||
});
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
function checkSilentModeFor(smsNumber) {
|
||||
return taskQueue.push(function() {
|
||||
function checkSilentMode(usageSilentSms) {
|
||||
return usageSilentSms && usageSilentSms.smsIndex &&
|
||||
usageSilentSms.smsIndex.indexOf(smsNumber) !== -1;
|
||||
}
|
||||
return getUsageSilentSms().then(checkSilentMode);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
enableSilentModeFor: enableSilentModeFor,
|
||||
disableSilentModeFor: disableSilentModeFor,
|
||||
checkSilentModeFor: checkSilentModeFor
|
||||
};
|
||||
}());
|
||||
406
gaia/apps/sms/views/shared/js/smil.js
Executable file
406
gaia/apps/sms/views/shared/js/smil.js
Executable file
|
|
@ -0,0 +1,406 @@
|
|||
/*global Utils, WBMP, TextEncoder */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// characters not allowed in smil filenames
|
||||
var unsafeFilenamePattern = /[^a-zA-Z0-9_#.()?&%-]/g;
|
||||
|
||||
// This encoder is aimed for encoding the string by 'utf-8'.
|
||||
var encoder = new TextEncoder('UTF-8');
|
||||
|
||||
// For utilizing sending/receiving DOM API, we need to handle 2 basic object
|
||||
// first: SMIL document and attachment. SMIL document is used for
|
||||
// representing the layout of the mms message, and attachment contains media
|
||||
// source and text. In mms message layout, one message could be split into
|
||||
// one or more slides. One slide could contain at most one media with/without
|
||||
// a text string. For example:
|
||||
|
||||
/*
|
||||
Layout of a mms message with 3 slides
|
||||
===================
|
||||
---------
|
||||
| img 1 |
|
||||
--------- <- Slide 1
|
||||
string 1
|
||||
|
||||
===================
|
||||
---------
|
||||
| img 2 |
|
||||
--------- <- Slide 2
|
||||
|
||||
|
||||
===================
|
||||
-----------
|
||||
| video 1 |
|
||||
----------- <- Slide 3
|
||||
string 2
|
||||
|
||||
===================
|
||||
|
||||
*/
|
||||
|
||||
// In this case, all the media files(img1/img2/video1) and text string
|
||||
// (string1/string2) will be converted into 5 attachments, and SMIL should
|
||||
// illustrate that this mms message contains 3 slides and the attachment's
|
||||
// name/layout... in each slide. Here we provide parse and generate utils
|
||||
// for developers to focus on the necessary information in slides.
|
||||
|
||||
|
||||
// this is a reduce function applied to the array of slides passed to
|
||||
// SMIL.generate - data will have an attachments and slides array
|
||||
// slide will be a single "slide", which can have text and an attachment
|
||||
// See the description of these formats in the comment before
|
||||
function SMIL_generateSlides(data, slide, slideIndex) {
|
||||
// default duration to 5 seconds per slide
|
||||
const DURATION = 5000;
|
||||
|
||||
var id;
|
||||
var blobType;
|
||||
// each slide can have a piece of media and/or text
|
||||
var media = '';
|
||||
var text = '';
|
||||
var name = '';
|
||||
if (slide.blob) {
|
||||
blobType = Utils.typeFromMimeType(slide.blob.type);
|
||||
if (blobType) {
|
||||
var tagName = tagNameFromBlobType(blobType);
|
||||
var region = 'region="Image"';
|
||||
// For attachment type 'ref' the region is not set
|
||||
if (tagName === 'ref') {
|
||||
region = '';
|
||||
}
|
||||
name = slide.name.substr(slide.name.lastIndexOf('/') + 1);
|
||||
// just to be safe, remove any non-standard characters from the filename
|
||||
name = name.replace(unsafeFilenamePattern, '#');
|
||||
name = SMIL_generateUniqueLocation(data, name);
|
||||
media = '<' + tagName + ' src="' + name + '" ' + region + '/>';
|
||||
data.attachments.push({
|
||||
id: '<' + name + '>',
|
||||
location: name,
|
||||
content: slide.blob
|
||||
});
|
||||
}
|
||||
}
|
||||
if (slide.text) {
|
||||
// Set text region.
|
||||
id = 'text_' + slideIndex + '.txt';
|
||||
text = '<text src="' + id + '" region="Text"/>';
|
||||
|
||||
// The text of the content blob should always be encoded by 'utf-8'.
|
||||
data.attachments.push({
|
||||
id: '<' + id + '>',
|
||||
location: id,
|
||||
content: new Blob([encoder.encode(slide.text)], {type: 'text/plain'})
|
||||
});
|
||||
}
|
||||
data.parts.push('<par dur="' + DURATION + 'ms">' + media + text + '</par>');
|
||||
return data;
|
||||
}
|
||||
|
||||
// Return a proper tag name depending on the blob type
|
||||
function tagNameFromBlobType(blobType) {
|
||||
var out;
|
||||
|
||||
switch(blobType) {
|
||||
case 'vcard':
|
||||
out = 'ref';
|
||||
break;
|
||||
|
||||
default:
|
||||
out = blobType;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function SMIL_generateUniqueLocation(data, location) {
|
||||
var extension, name, result;
|
||||
|
||||
// Need to add reference spec here
|
||||
var FILENAME_LIMIT = 40;
|
||||
|
||||
// if the location is already being used by the attachment
|
||||
function SMIL_uniqueLocationMatches(attachment) {
|
||||
return attachment.location === result;
|
||||
}
|
||||
|
||||
// Check if a file extension exists
|
||||
// Cache index of last non-extension portion of the filename
|
||||
var index = location.lastIndexOf('.');
|
||||
if (index === -1) {
|
||||
name = location;
|
||||
// Set to empty string so we can check length and append it to things
|
||||
extension = '';
|
||||
} else {
|
||||
// Cache potential file extension
|
||||
extension = location.slice(index);
|
||||
name = location.slice(0, index);
|
||||
}
|
||||
|
||||
// First truncate below the limit to make de-duplicating worthwhile
|
||||
if (name.length + extension.length > FILENAME_LIMIT) {
|
||||
name = name.slice(0, FILENAME_LIMIT - extension.length);
|
||||
}
|
||||
result = name + extension;
|
||||
|
||||
var duplicateIndex = 2;
|
||||
// If result is identical to any other attached files
|
||||
// add a duplicate marker and recheck the length
|
||||
while (data.attachments.some(SMIL_uniqueLocationMatches)) {
|
||||
var truncIndex = 0;
|
||||
// Construct a de-deuplicated name with the extention and
|
||||
// update duplicate index in case de-duplicated name is already chosen
|
||||
result = name + '_' + duplicateIndex++ + extension;
|
||||
// Truncate until the name (no longer needs to be de-duplicated)
|
||||
// and extension are below the limit
|
||||
while (result.length > FILENAME_LIMIT) {
|
||||
duplicateIndex = 2;
|
||||
name = name.slice(0, --truncIndex);
|
||||
result = name + extension;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
window.SMIL = {
|
||||
|
||||
// SMIL.parse - takes a message from the DOM API's and converts to a
|
||||
// simple array format:
|
||||
|
||||
// message.smil = valid SMIL string, or falsey
|
||||
// message.attachments = []
|
||||
// message.attachments[].content = blob
|
||||
// message.attachments[].location = src attr in smil
|
||||
|
||||
// Promise.resolve(parsedArray):
|
||||
// parsedArray = []
|
||||
// parsedArray[].text = 'plain text'
|
||||
// parsedArray[].name = name of key
|
||||
// parsedArray[].blob = data blob
|
||||
|
||||
parse: function SMIL_parse(message) {
|
||||
var smil = message.smil;
|
||||
var attachments = message.attachments;
|
||||
var slides = [];
|
||||
var attachmentsNotFound = false;
|
||||
var smilMismatched = false;
|
||||
var doc;
|
||||
var parTags;
|
||||
|
||||
function readTextBlob(blob) {
|
||||
// short circuit on null blobs
|
||||
if (!blob) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
var textReader = new FileReader();
|
||||
textReader.onload = function(event) {
|
||||
defer.resolve(event.target.result);
|
||||
};
|
||||
textReader.onerror = function(event) {
|
||||
console.error('Error reading text blob');
|
||||
defer.resolve('');
|
||||
};
|
||||
|
||||
// The text blob must be encoded as 'utf-8' by Gecko.
|
||||
textReader.readAsText(blob, 'UTF-8');
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function findAttachment(name) {
|
||||
var index = 0;
|
||||
var length = attachments.length;
|
||||
|
||||
// strip the cid: prefix from some MMS encoders
|
||||
name = name.replace(/^cid:/, '');
|
||||
|
||||
for (; index < length; index++) {
|
||||
if (attachments[index].location === name ||
|
||||
attachments[index].id === '<' + name + '>') {
|
||||
return attachments[index];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function convertWbmpToPng(slide) {
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
var reader;
|
||||
slide.name = slide.name.slice(0, -5) + '.png';
|
||||
reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
WBMP.decode(event.target.result, function callback(blob) {
|
||||
slide.blob = blob;
|
||||
defer.resolve();
|
||||
});
|
||||
};
|
||||
reader.onerror = function(event) {
|
||||
console.error('Error reading text blob');
|
||||
defer.resolve();
|
||||
};
|
||||
reader.readAsArrayBuffer(slide.blob);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
// handle mms messages without smil
|
||||
// Display the attachments of the mms message in order
|
||||
function SMIL_parseWithoutSMIL(attachment, idx) {
|
||||
var blob = attachment.content;
|
||||
if (!blob) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var result;
|
||||
|
||||
var type = Utils.typeFromMimeType(blob.type);
|
||||
|
||||
// handle text blobs (plain text blob only) by reading them and
|
||||
// converting to text on the last slide
|
||||
if (type === 'text' && blob.type === 'text/plain') {
|
||||
result = readTextBlob(blob).then((text) => {
|
||||
slides[idx] = {
|
||||
text: text
|
||||
};
|
||||
});
|
||||
|
||||
// make sure the type was something we want, otherwise ignore it
|
||||
} else if (type) {
|
||||
var slide = { name: attachment.location, blob: attachment.content };
|
||||
if (slide.name && slide.name.slice(-5) === '.wbmp') {
|
||||
result = convertWbmpToPng(slide);
|
||||
}
|
||||
slides[idx] = slide;
|
||||
}
|
||||
|
||||
return result || Promise.resolve();
|
||||
}
|
||||
|
||||
function SMIL_parseHandleParTag(par, index) {
|
||||
// stop parsing as soon as we fail to find one attachment
|
||||
if (attachmentsNotFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaElements = par.querySelectorAll('img, video, audio, ref');
|
||||
var textElement = par.querySelector('text');
|
||||
var attachment, src;
|
||||
|
||||
var promises = Array.from(mediaElements).map((element) => {
|
||||
var result;
|
||||
|
||||
var slide = {};
|
||||
src = element.getAttribute('src');
|
||||
attachment = findAttachment(src);
|
||||
if (attachment) {
|
||||
// every media attachment starts its own slide in our format
|
||||
slide = { name: attachment.location, blob: attachment.content };
|
||||
slides.push(slide);
|
||||
if (slide.name && slide.name.slice(-5) === '.wbmp') {
|
||||
result = convertWbmpToPng(slide);
|
||||
}
|
||||
} else {
|
||||
attachmentsNotFound = true;
|
||||
}
|
||||
|
||||
return result || Promise.resolve();
|
||||
});
|
||||
|
||||
if (textElement) {
|
||||
src = textElement.getAttribute('src');
|
||||
attachment = findAttachment(src);
|
||||
|
||||
if (attachment) {
|
||||
|
||||
// check for text on the last slide
|
||||
var slide = slides[slides.length - 1];
|
||||
|
||||
// if the last slide doesn't exist, or the last slide has text
|
||||
// already, we create a new slide to store the text
|
||||
if (!slide || typeof slide.text !== 'undefined') {
|
||||
slide = {};
|
||||
slides.push(slide);
|
||||
}
|
||||
// Init slide text to avoid text replaced by later blob
|
||||
slide.text = '';
|
||||
|
||||
// read the text blob, and store it in the "slide" this function
|
||||
// will hold onto
|
||||
promises.push(
|
||||
readTextBlob(attachment.content).then(
|
||||
(text) => slide.text = text
|
||||
)
|
||||
);
|
||||
} else {
|
||||
attachmentsNotFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
var result;
|
||||
|
||||
// handle MMS messages with SMIL
|
||||
if (smil) {
|
||||
doc = (new DOMParser()).parseFromString(smil, 'application/xml');
|
||||
parTags = doc.documentElement.getElementsByTagName('par');
|
||||
|
||||
// Check if attachments are all listed in smil.
|
||||
var elements = Array.reduce(
|
||||
parTags, (count, par) => count + par.childElementCount, 0
|
||||
);
|
||||
|
||||
if (elements !== attachments.length) {
|
||||
smilMismatched = true;
|
||||
} else {
|
||||
result = Promise.all(Array.from(parTags).map(SMIL_parseHandleParTag));
|
||||
}
|
||||
}
|
||||
|
||||
// handle MMS attachments without SMIL / malformed SMIL
|
||||
if (!smil || attachmentsNotFound || !slides.length || smilMismatched) {
|
||||
// reset slides in the attachments not found case
|
||||
slides = Array(attachments.length);
|
||||
result = Promise.all(attachments.map(SMIL_parseWithoutSMIL));
|
||||
}
|
||||
|
||||
return (result || Promise.resolve()).then(() => slides);
|
||||
},
|
||||
|
||||
// SMIL.generate - takes a array with slides and return smil string and
|
||||
// attachment array.
|
||||
generate: function SMIL_generate(slides) {
|
||||
const HEADER = '<head><layout>' +
|
||||
'<root-layout width="320px" height="480px"/>' +
|
||||
'<region id="Image" left="0px" top="0px"' +
|
||||
' width="320px" height="320px" fit="meet"/>' +
|
||||
'<region id="Text" left="0px" top="320px"' +
|
||||
' width="320px" height="160px" fit="meet"/>' +
|
||||
'</layout></head>';
|
||||
|
||||
// generate html from each slide while storing the actual attachment
|
||||
// in the attachments array.
|
||||
var data = slides.reduce(SMIL_generateSlides, {
|
||||
attachments: [],
|
||||
parts: []
|
||||
});
|
||||
|
||||
// join the html parts together
|
||||
data.smil = '<smil>' + HEADER + '<body>' +
|
||||
data.parts.join('') +
|
||||
'</body></smil>';
|
||||
|
||||
// the API doesn't care about 'parts', clean it up
|
||||
delete data.parts;
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
192
gaia/apps/sms/views/shared/js/startup.js
Executable file
192
gaia/apps/sms/views/shared/js/startup.js
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*global ActivityHandler,
|
||||
App,
|
||||
ConversationView,
|
||||
Drafts,
|
||||
InboxView,
|
||||
Information,
|
||||
InterInstanceEventDispatcher,
|
||||
LazyLoader,
|
||||
LocalizationHelper,
|
||||
MessageManager,
|
||||
MessagingClient,
|
||||
MozMobileConnectionsClient,
|
||||
Navigation,
|
||||
Settings,
|
||||
SilentSms,
|
||||
SystemMessageHandler,
|
||||
TimeHeaders,
|
||||
Utils
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
/**
|
||||
* @function
|
||||
* The debug function translates to console.log in debug mode.
|
||||
*
|
||||
* @param {String} arg1 The first value to be displayed to the console.
|
||||
* @param {...*} args The other values to be displayed to the console, or the
|
||||
* parameters to the first string.
|
||||
*/
|
||||
var debug = 0 ?
|
||||
(arg1, ...args) => console.log('[Startup] ' + arg1, ...args) :
|
||||
() => {};
|
||||
|
||||
var Startup = exports.Startup = {
|
||||
_lazyLoadScripts: [
|
||||
'/shared/js/settings_listener.js',
|
||||
'/shared/js/mime_mapper.js',
|
||||
'/shared/js/notification_helper.js',
|
||||
'/shared/js/option_menu.js',
|
||||
'/shared/js/gesture_detector.js',
|
||||
'/shared/js/settings_url.js',
|
||||
'/shared/js/mobile_operator.js',
|
||||
'/shared/js/multi_sim_action_button.js',
|
||||
'/shared/js/image_utils.js',
|
||||
'/shared/elements/gaia_sim_picker/script.js',
|
||||
'/views/shared/js/waiting_screen.js',
|
||||
'/views/shared/js/errors.js',
|
||||
'/views/shared/js/dialog.js',
|
||||
'/views/shared/js/error_dialog.js',
|
||||
'/views/conversation/js/link_helper.js',
|
||||
'/views/conversation/js/link_action_handler.js',
|
||||
'/views/shared/js/contact_renderer.js',
|
||||
'/views/shared/js/activity_picker.js',
|
||||
'/views/conversation/js/information.js',
|
||||
'/views/shared/js/shared_components.js',
|
||||
'/views/shared/js/task_runner.js',
|
||||
'/views/shared/js/silent_sms.js',
|
||||
'/views/conversation/js/recipients.js',
|
||||
'/views/conversation/js/attachment.js',
|
||||
'/views/conversation/js/attachment_renderer.js',
|
||||
'/views/conversation/js/conversation.js',
|
||||
'/views/conversation/js/subject_composer.js',
|
||||
'/views/conversation/js/compose.js',
|
||||
'/views/shared/js/wbmp.js',
|
||||
'/views/shared/js/smil.js',
|
||||
'/views/shared/js/notify.js',
|
||||
'/views/shared/js/activity_handler.js',
|
||||
'/views/shared/js/system_message_handler.js',
|
||||
'/views/shared/js/localization_helper.js',
|
||||
'/lib/bridge/client.js',
|
||||
'/services/js/activity/activity_client.js',
|
||||
'/services/js/messaging/messaging_client.js',
|
||||
'/services/js/moz_mobile_connections/moz_mobile_connections_client.js'
|
||||
],
|
||||
|
||||
_lazyLoadStyles: [
|
||||
'/shared/style/confirm.css',
|
||||
'/shared/style/status.css',
|
||||
'/shared/style/input_areas.css',
|
||||
'/shared/style/progress_activity.css',
|
||||
'/views/shared/style/composer.css',
|
||||
'/views/conversation/style/message.css',
|
||||
'/views/conversation/style/report_view.css',
|
||||
'/views/conversation/style/attachment.css',
|
||||
'/views/conversation/style/conversation.css',
|
||||
'/views/new_message/style/new_message.css',
|
||||
'/views/new_message/style/recipients.css'
|
||||
],
|
||||
|
||||
_lazyLoadInit: function() {
|
||||
var lazyLoadPromise = LazyLoader.load(
|
||||
[...this._lazyLoadScripts, ...this._lazyLoadStyles]).then(() => {
|
||||
|
||||
LocalizationHelper.init();
|
||||
|
||||
InterInstanceEventDispatcher.connect();
|
||||
|
||||
// dispatch contentInteractive when all the modules initialized
|
||||
SilentSms.init();
|
||||
|
||||
// When app is run as inline activity we don't want to handle any other
|
||||
// system messages other than "activity" system message.
|
||||
if (navigator.mozHasPendingMessage('activity')) {
|
||||
ActivityHandler.init();
|
||||
} else {
|
||||
SystemMessageHandler.init();
|
||||
}
|
||||
|
||||
// Init UI Managers
|
||||
TimeHeaders.init();
|
||||
ConversationView.init();
|
||||
MessagingClient.init(App.instanceId);
|
||||
MozMobileConnectionsClient.init(App.instanceId);
|
||||
Information.initDefaultViews();
|
||||
|
||||
Navigation.setReady();
|
||||
|
||||
window.performance.mark('contentInteractive');
|
||||
|
||||
// Fetch mmsSizeLimitation and max concat
|
||||
Settings.init();
|
||||
|
||||
window.performance.mark('objectsInitEnd');
|
||||
});
|
||||
return lazyLoadPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* We wait for the DOMContentLoaded event in the event sequence. After we
|
||||
* loaded the first panel of threads, we lazy load all non-critical JS files.
|
||||
* As a result, if the 'load' event was not sent yet, this will delay it even
|
||||
* more until all these non-critical JS files are loaded. This is fine.
|
||||
*/
|
||||
init: function() {
|
||||
var loaded = function() {
|
||||
window.removeEventListener('DOMContentLoaded', loaded);
|
||||
|
||||
window.performance.mark('navigationLoaded');
|
||||
|
||||
Utils.initializeShimHost(App.instanceId);
|
||||
|
||||
MessageManager.init();
|
||||
Drafts.init();
|
||||
InboxView.init();
|
||||
Navigation.init();
|
||||
|
||||
InboxView.once('fully-loaded', () => {
|
||||
window.performance.mark('fullyLoaded');
|
||||
|
||||
App.setReady();
|
||||
});
|
||||
|
||||
InboxView.once('visually-loaded', () => {
|
||||
window.performance.mark('visuallyLoaded');
|
||||
});
|
||||
|
||||
// If initial panel is default one and app isn't run from notification,
|
||||
// then just navigate to it, otherwise we can delay default panel
|
||||
// initialization until we navigate to requested non-default panel.
|
||||
if (Navigation.isDefaultPanel() &&
|
||||
!navigator.mozHasPendingMessage('notification') &&
|
||||
!navigator.mozHasPendingMessage('activity')) {
|
||||
debug('Rendering threads now.');
|
||||
|
||||
InboxView.once('visually-loaded', () => {
|
||||
this._lazyLoadInit();
|
||||
});
|
||||
|
||||
InboxView.renderThreads();
|
||||
} else {
|
||||
debug('Not using default panel, waiting for navigated event');
|
||||
Navigation.once('navigated', () => InboxView.renderThreads());
|
||||
|
||||
this._lazyLoadInit();
|
||||
}
|
||||
|
||||
// dispatch navigationInteractive when thread list related modules
|
||||
// initialized
|
||||
window.performance.mark('navigationInteractive');
|
||||
}.bind(this);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', loaded);
|
||||
}
|
||||
};
|
||||
|
||||
Startup.init();
|
||||
})(window);
|
||||
348
gaia/apps/sms/views/shared/js/system_message_handler.js
Executable file
348
gaia/apps/sms/views/shared/js/system_message_handler.js
Executable file
|
|
@ -0,0 +1,348 @@
|
|||
/*global Contacts,
|
||||
MessageManager,
|
||||
Navigation,
|
||||
NotificationHelper,
|
||||
Notify,
|
||||
Settings,
|
||||
SilentSms,
|
||||
SMIL,
|
||||
Utils
|
||||
*/
|
||||
/*exported SystemMessageHandler */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Number of milliseconds after which currently acquired wake lock will be
|
||||
* automatically released.
|
||||
* @type {Number}
|
||||
* @const
|
||||
*/
|
||||
const AUTO_WAKE_LOCK_UNLOCK_TIMEOUT = 30 * 1000;
|
||||
|
||||
const priv = {
|
||||
wakeLock: Symbol('wakeLock'),
|
||||
autoWakeLockUnlockTimeout: Symbol('autoWakeLockUnlockTimeout')
|
||||
};
|
||||
|
||||
/**
|
||||
* Object that is responsible for handling "sms-received" and "notification"
|
||||
* system messages.
|
||||
*/
|
||||
var SystemMessageHandler = {
|
||||
/**
|
||||
* Wake lock instance.
|
||||
* @type {MozWakeLock}
|
||||
* @private
|
||||
*/
|
||||
[priv.wakeLock]: null,
|
||||
|
||||
/**
|
||||
* Timeout that will trigger to release of currently active wake lock.
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
[priv.autoWakeLockUnlockTimeout]: null,
|
||||
|
||||
/**
|
||||
* Initializes system message handlers, if app is run as activity, no
|
||||
* system message handlers will be registered.
|
||||
*/
|
||||
init() {
|
||||
if (!navigator.mozSetMessageHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.mozSetMessageHandler(
|
||||
'sms-received', this.onSmsReceivedSystemMessage.bind(this)
|
||||
);
|
||||
|
||||
navigator.mozSetMessageHandler(
|
||||
'notification', this.onNotificationSystemMessage.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests application wake lock.
|
||||
* @private
|
||||
*/
|
||||
requestWakeLock() {
|
||||
this[priv.wakeLock] = navigator.requestWakeLock('cpu');
|
||||
|
||||
this[priv.autoWakeLockUnlockTimeout] = setTimeout(
|
||||
() => this.releaseWakeLock(), AUTO_WAKE_LOCK_UNLOCK_TIMEOUT
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Releases currently active application wake lock.
|
||||
* @private
|
||||
*/
|
||||
releaseWakeLock() {
|
||||
var autoWakeLockUnlockTimeout = this[priv.autoWakeLockUnlockTimeout];
|
||||
if (autoWakeLockUnlockTimeout) {
|
||||
clearTimeout(autoWakeLockUnlockTimeout);
|
||||
this[priv.autoWakeLockUnlockTimeout] = null;
|
||||
}
|
||||
|
||||
var wakeLock = this[priv.wakeLock];
|
||||
if (wakeLock) {
|
||||
wakeLock.unlock();
|
||||
this[priv.wakeLock] = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepares notification body for the message
|
||||
* @param {MozMmsMessage|MozSmsMessage} message Message to prepare
|
||||
* notification body for.
|
||||
* @returns {Promise.<{raw: string}|string>} L10n object that will serve as
|
||||
* notification body.
|
||||
* @private
|
||||
*/
|
||||
getNotificationBody(message) {
|
||||
// In case of sms, we want to display text content in notification.
|
||||
if (message.type === 'sms') {
|
||||
return Promise.resolve({ raw: message.body || '' });
|
||||
}
|
||||
|
||||
var mmsNotificationBodyL10n = null;
|
||||
|
||||
// Here we can only have one sender, so deliveryInfo[0].deliveryStatus
|
||||
// => message status from sender.
|
||||
if (message.deliveryInfo[0].deliveryStatus !== 'success') {
|
||||
// If the delivery status is manual/rejected/error, we need to apply
|
||||
// specific text to notify user that message is not downloaded.
|
||||
mmsNotificationBodyL10n = 'notDownloaded-title';
|
||||
} else if (message.subject) {
|
||||
// If subject exist, we display subject in notification.
|
||||
mmsNotificationBodyL10n = { raw: message.subject };
|
||||
}
|
||||
|
||||
return mmsNotificationBodyL10n ?
|
||||
Promise.resolve(mmsNotificationBodyL10n) :
|
||||
SMIL.parse(message).then((slideArray) => {
|
||||
// If MMS has at least one non-empty text slide, let's display that
|
||||
// text in notification otherwise use predefined 'mms message'.
|
||||
var textSlide = slideArray.find((slide) => !!slide.text);
|
||||
return textSlide ? { raw: textSlide.text } : 'mms-message';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepares notification title for the message
|
||||
* @param {MozMmsMessage|MozSmsMessage} message Message to prepare
|
||||
* notification title for.
|
||||
* @returns {Promise.<{raw: string}|{id: string, args: Object}>} L10n object
|
||||
* that will serve as notification title.
|
||||
* @private
|
||||
*/
|
||||
getNotificationTitle(message) {
|
||||
return Contacts.findByAddress(message.sender).then((contacts) => {
|
||||
var sender = contacts.length && contacts[0].name &&
|
||||
contacts[0].name.length && contacts[0].name[0] ||
|
||||
message.sender;
|
||||
|
||||
if (Settings.hasSeveralSim() && message.iccId) {
|
||||
return Utils.getSimNameByIccId(message.iccId).then(
|
||||
(simName) => ({
|
||||
id: 'dsds-notification-title-with-sim',
|
||||
args: { sim: simName, sender: sender }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { raw: sender };
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves correct application icon to use within notifications.
|
||||
* @returns {string} Application icon URI.
|
||||
* @private
|
||||
*/
|
||||
getNotificationIcon() {
|
||||
return navigator.mozApps.getSelf().then(
|
||||
(app) => NotificationHelper.getIconURI(app)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes received class-0 message (flash message).
|
||||
* @param {MozMmsMessage|MozSmsMessage} message Received class-0 message.
|
||||
* @returns {Promise.<void>} Promise that is resolved once message is fully
|
||||
* handled.
|
||||
* @private
|
||||
*/
|
||||
onFlashMessageReceived(message) {
|
||||
// We have to remove message from DB as it should not be kept in Inbox.
|
||||
return MessageManager.deleteMessages(message.id).then(
|
||||
() => navigator.mozApps.getSelf()
|
||||
).then((app) =>{
|
||||
app.launch();
|
||||
|
||||
Notify.ringtone();
|
||||
Notify.vibrate();
|
||||
|
||||
Utils.alert({ raw: message.body || '' }, { raw: message.sender });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles received class-1, class-2 or class-3 message.
|
||||
* @param {MozMmsMessage|MozSmsMessage} message Received message.
|
||||
* @returns {Promise.<void>} Promise that is resolved once message is fully
|
||||
* handled.
|
||||
* @private
|
||||
*/
|
||||
onMessageReceived(message) {
|
||||
// If message type is mms and pending on server, ignore the notification
|
||||
// because it will be retrieved from server automatically. Handle other
|
||||
// manual/error status as manual download and dispatch notification.
|
||||
// Please ref mxr for all the possible delivery status:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/mms/src/ril/
|
||||
// MmsService.js#62
|
||||
// Here we can only have one sender, so deliveryInfo[0].deliveryStatus
|
||||
// => message status from sender.
|
||||
if (message.type === 'mms' &&
|
||||
message.deliveryInfo[0].deliveryStatus === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
var isInConversation = Navigation.isCurrentPanel(
|
||||
'thread', { id: message.threadId }
|
||||
);
|
||||
|
||||
// If app is already opened and we're in the right conversation, let's
|
||||
// just notify user about new message.
|
||||
if (!document.hidden && isInConversation) {
|
||||
Notify.ringtone();
|
||||
Notify.vibrate();
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.getNotificationTitle(message),
|
||||
this.getNotificationBody(message),
|
||||
this.getNotificationIcon()
|
||||
]).then(([titleL10n, bodyL10n, icon]) => {
|
||||
// Force the helper to leave close behavior as it is because it should
|
||||
// be handled in Utils.closeNotificationsForThread later.
|
||||
return NotificationHelper.send(titleL10n, {
|
||||
icon: icon,
|
||||
bodyL10n: bodyL10n,
|
||||
tag: 'threadId:' + message.threadId,
|
||||
data: { id: message.id, threadId: message.threadId },
|
||||
closeOnClick: false
|
||||
});
|
||||
}).then((notification) => {
|
||||
notification.addEventListener(
|
||||
'click', () => this.onNotificationClicked(message)
|
||||
);
|
||||
|
||||
// Close notification if we are already in conversation view and app
|
||||
// becomes visible.
|
||||
if (document.hidden && isInConversation) {
|
||||
Utils.onceDocumentIsVisible().then(() => notification.close());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fires once notification is clicked.
|
||||
* @param {{ id: string, threadId: string }} notification Notification
|
||||
* arbitrary data.
|
||||
* @returns {Promise.<void>} Promise that is resolved once notification
|
||||
* click is handled.
|
||||
* @private
|
||||
*/
|
||||
onNotificationClicked({ id, threadId }) {
|
||||
return navigator.mozApps.getSelf().then((app) => {
|
||||
app.launch();
|
||||
|
||||
// If we're currently in the target conversation, just do nothing.
|
||||
if (Navigation.isCurrentPanel('thread', { id: threadId })) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Validate if message still exists before opening message thread
|
||||
//See issue https://bugzilla.mozilla.org/show_bug.cgi?id=837029
|
||||
return MessageManager.getMessage(id).then(
|
||||
(message) => Navigation.toPanel('thread', { id: message.threadId }),
|
||||
() => {
|
||||
if (Navigation.hasPendingInit()) {
|
||||
Navigation.init();
|
||||
}
|
||||
Utils.alert('deleted-sms');
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles "sms-received" system message.
|
||||
* @param {MozMmsMessage|MozSmsMessage} message Received message.
|
||||
* @returns {Promise.<void>} Promise that is resolved once message is fully
|
||||
* handled.
|
||||
* @private
|
||||
*/
|
||||
onSmsReceivedSystemMessage(message) {
|
||||
// Acquire the cpu wake lock when we receive an SMS. This raises the
|
||||
// priority of this process above vanilla background apps, making it less
|
||||
// likely to be killed on OOM. It also prevents the device from going to
|
||||
// sleep before the user is notified of the new message.
|
||||
//
|
||||
// We'll release it once we display a notification to the user. We also
|
||||
// release the lock after 30s, in case we never run the notification code
|
||||
// for some reason.
|
||||
this.requestWakeLock();
|
||||
|
||||
var handleReceivedMessagePromise;
|
||||
if (message.messageClass === 'class-0') {
|
||||
handleReceivedMessagePromise = this.onFlashMessageReceived(message);
|
||||
} else {
|
||||
handleReceivedMessagePromise = SilentSms.checkSilentModeFor(
|
||||
message.sender
|
||||
).then((isSilent) => {
|
||||
if (!isSilent) {
|
||||
return this.onMessageReceived(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return handleReceivedMessagePromise.then(() => this.releaseWakeLock());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles "notification" system message.
|
||||
* @param {Notification} notification Notification object.
|
||||
* @returns {Promise.<void>} Promise that is resolved once notification
|
||||
* system message is handled.
|
||||
* @private
|
||||
*/
|
||||
onNotificationSystemMessage(notification) {
|
||||
// When notification is removed from notification tray, notification
|
||||
// system message will still be fired, but "clicked" property will be
|
||||
// equal to false. This should change once bug 1139363 is landed. When
|
||||
// user clicks on notification we'll get two system messages, first to
|
||||
// notify app that notification is clicked and then, once we show
|
||||
// Conversation view to the user, we remove that notification from the
|
||||
// tray that causes the second system message with "clicked" set to false.
|
||||
if (!notification.clicked || !notification.data) {
|
||||
// Force closing the window for notification removal case
|
||||
// because navigation didn't init correctly if pending notification
|
||||
// message existed.
|
||||
if (Navigation.hasPendingInit()) {
|
||||
window.close();
|
||||
return Promise.reject(new Error('Notification has been dismissed.'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// At the moment notification.data is { id, threadId }.
|
||||
return this.onNotificationClicked(notification.data);
|
||||
}
|
||||
};
|
||||
|
||||
exports.SystemMessageHandler = SystemMessageHandler;
|
||||
})(self);
|
||||
10
gaia/apps/sms/views/shared/js/task_runner.js
Executable file
10
gaia/apps/sms/views/shared/js/task_runner.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
/* global Promise */
|
||||
'use strict';
|
||||
|
||||
function TaskRunner() {
|
||||
this._currentTask = Promise.resolve();
|
||||
}
|
||||
|
||||
TaskRunner.prototype.push = function(task) {
|
||||
return (this._currentTask = this._currentTask.then(task, task));
|
||||
};
|
||||
93
gaia/apps/sms/views/shared/js/time_headers.js
Executable file
93
gaia/apps/sms/views/shared/js/time_headers.js
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
/*global Utils */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var updateTimer = null;
|
||||
|
||||
var TimeHeaders = {
|
||||
init: function th_init() {
|
||||
onvisibilityChange();
|
||||
document.addEventListener('visibilitychange', onvisibilityChange);
|
||||
window.addEventListener('timeformatchange', ontimeFormatChange);
|
||||
window.addEventListener('languagechange', onlanguageChange);
|
||||
},
|
||||
startScheduler: function th_startScheduler() {
|
||||
var now = Date.now(),
|
||||
nextTimeout = new Date(now + 60000);
|
||||
nextTimeout.setSeconds(0);
|
||||
nextTimeout.setMilliseconds(0);
|
||||
|
||||
// stop updateTimer first
|
||||
this.stopScheduler();
|
||||
|
||||
// then register a new one
|
||||
updateTimer = setTimeout(function() {
|
||||
this.updateAll('header[data-time-update=repeat]');
|
||||
this.startScheduler();
|
||||
}.bind(this), nextTimeout.getTime() - now);
|
||||
},
|
||||
stopScheduler: function th_stopScheduler() {
|
||||
clearTimeout(updateTimer);
|
||||
},
|
||||
updateAll: function th_updateAll(selector) {
|
||||
selector = selector || '[data-time-update]';
|
||||
var elements = document.querySelectorAll(selector),
|
||||
length = elements.length,
|
||||
i;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
this.update(elements[i]);
|
||||
}
|
||||
},
|
||||
update: function th_update(element) {
|
||||
var ts = element.dataset.time;
|
||||
if (!ts) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only date
|
||||
if (element.dataset.dateOnly === 'true') {
|
||||
Utils.setHeaderDate({
|
||||
time: ts,
|
||||
element: element,
|
||||
withTime: false
|
||||
});
|
||||
|
||||
// only time
|
||||
} else if (element.dataset.timeOnly === 'true') {
|
||||
element.removeAttribute('data-l10n-id');
|
||||
element.textContent = Utils.getFormattedHour(ts);
|
||||
|
||||
// date + time
|
||||
} else {
|
||||
Utils.setHeaderDate({
|
||||
time: ts,
|
||||
element: element,
|
||||
withTime: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function onvisibilityChange() {
|
||||
if (document.hidden) {
|
||||
TimeHeaders.stopScheduler();
|
||||
}
|
||||
else {
|
||||
TimeHeaders.updateAll();
|
||||
TimeHeaders.startScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
function ontimeFormatChange() {
|
||||
Utils.resetDateFormatters();
|
||||
TimeHeaders.updateAll();
|
||||
}
|
||||
|
||||
function onlanguageChange() {
|
||||
Utils.resetDateFormatters();
|
||||
TimeHeaders.updateAll();
|
||||
}
|
||||
|
||||
exports.TimeHeaders = TimeHeaders;
|
||||
}(this));
|
||||
980
gaia/apps/sms/views/shared/js/utils.js
Executable file
980
gaia/apps/sms/views/shared/js/utils.js
Executable file
|
|
@ -0,0 +1,980 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* globals ContactPhotoHelper,
|
||||
Dialog,
|
||||
Notification,
|
||||
Settings
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
var rdashes = /-(.)/g;
|
||||
var rescape = /[.?*+^$[\]\\(){}|-]/g;
|
||||
var rparams = /([^?=&]+)(?:=([^&]*))?/g;
|
||||
var rnondialablechars = /[^,#+\*\d]/g;
|
||||
var rmail = /[\w-]+@[\w\-]/;
|
||||
|
||||
var Utils = {
|
||||
date: {
|
||||
shared: new Date(),
|
||||
formatters: {
|
||||
'time': {
|
||||
options: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
formatter: null
|
||||
},
|
||||
'full': {
|
||||
options: {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
},
|
||||
formatter: null
|
||||
},
|
||||
'full-wt': {
|
||||
options: {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
formatter: null
|
||||
},
|
||||
'day': {
|
||||
options: {
|
||||
weekday: 'long',
|
||||
},
|
||||
formatter: null
|
||||
},
|
||||
'day-wt': {
|
||||
options: {
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
formatter: null
|
||||
},
|
||||
},
|
||||
},
|
||||
escapeRegex: function ut_escapeRegex(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return str.replace(rescape, '\\$&');
|
||||
},
|
||||
getFormattedHour: function ut_getFormattedHour(time) {
|
||||
this.date.shared.setTime(+time);
|
||||
return this._getFormatter('time', false).format(this.date.shared);
|
||||
},
|
||||
getDayDate: function re_getDayDate(time) {
|
||||
this.date.shared.setTime(+time);
|
||||
this.date.shared.setHours(0, 0, 0, 0);
|
||||
return this.date.shared.getTime();
|
||||
},
|
||||
resetDateFormatters: function ut_resetDateFormatters() {
|
||||
// remove cached formatters.
|
||||
// It should happen on timeformatchange and languagechange
|
||||
for (var type in this.date.formatters) {
|
||||
this.date.formatters[type].formatter = null;
|
||||
}
|
||||
},
|
||||
_getFormatter: function ut_getFormatter(type, withTime) {
|
||||
if (withTime) {
|
||||
type += '-wt';
|
||||
}
|
||||
|
||||
// Cache the formatter for better performance
|
||||
if (!this.date.formatters[type].formatter) {
|
||||
var options = Object.assign(
|
||||
{},
|
||||
this.date.formatters[type].options
|
||||
);
|
||||
if (options.hour) {
|
||||
options.hour12 = navigator.mozHour12;
|
||||
}
|
||||
this.date.formatters[type].formatter = Intl.DateTimeFormat(
|
||||
navigator.languages,
|
||||
options
|
||||
);
|
||||
}
|
||||
return this.date.formatters[type].formatter;
|
||||
},
|
||||
setHeaderDate: function ut_setHeaderDate({time, element, withTime}) {
|
||||
|
||||
var formatter;
|
||||
var today = Utils.getDayDate(Date.now());
|
||||
var otherDay = Utils.getDayDate(time);
|
||||
var dayDiff = (today - otherDay) / 86400000;
|
||||
this.date.shared.setTime(+time);
|
||||
|
||||
if (isNaN(dayDiff)) {
|
||||
element.setAttribute('data-l10n-id', 'incorrectDate');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dayDiff < 0) {
|
||||
// future time
|
||||
formatter = this._getFormatter('full', withTime);
|
||||
element.removeAttribute('data-l10n-id');
|
||||
element.textContent = formatter.format(this.date.shared);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dayDiff === 0) {
|
||||
if (withTime) {
|
||||
document.l10n.setAttributes(
|
||||
element, 'todayWithTime', {
|
||||
time: this.getFormattedHour(this.date.shared)
|
||||
});
|
||||
} else {
|
||||
element.setAttribute('data-l10n-id', 'today');
|
||||
}
|
||||
} else if (dayDiff === 1) {
|
||||
if (withTime) {
|
||||
document.l10n.setAttributes(
|
||||
element, 'yesterdayWithTime', {
|
||||
time: this.getFormattedHour(this.date.shared)
|
||||
});
|
||||
} else {
|
||||
element.setAttribute('data-l10n-id', 'yesterday');
|
||||
}
|
||||
} else if (dayDiff < 6) {
|
||||
// day
|
||||
formatter = this._getFormatter('day', withTime);
|
||||
element.removeAttribute('data-l10n-id');
|
||||
element.textContent = formatter.format(this.date.shared);
|
||||
} else {
|
||||
formatter = this._getFormatter('full', withTime);
|
||||
element.removeAttribute('data-l10n-id');
|
||||
element.textContent = formatter.format(this.date.shared);
|
||||
}
|
||||
},
|
||||
|
||||
// We will apply createObjectURL for details.photoURL if contact image exist
|
||||
// Please remember to revoke the photoURL after utilizing it.
|
||||
getContactDetails:
|
||||
function ut_getContactDetails(number, contacts, include) {
|
||||
var details = {};
|
||||
|
||||
include = include || {};
|
||||
|
||||
function updateDetails(contact) {
|
||||
var name, phone, i, length, subscriber, org;
|
||||
name = contact.name[0];
|
||||
org = contact.org && contact.org[0];
|
||||
length = contact.tel ? contact.tel.length : 0;
|
||||
subscriber = number.length > 7 ? number.substr(-8) : number;
|
||||
|
||||
// Check which of the contacts phone number are we using
|
||||
for (i = 0; i < length; i++) {
|
||||
// Based on E.164 (http://en.wikipedia.org/wiki/E.164)
|
||||
if (contact.tel[i].value.indexOf(subscriber) !== -1) {
|
||||
phone = contact.tel[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add data values for contact activity interaction
|
||||
details.isContact = true;
|
||||
|
||||
// Add photo
|
||||
if (include.photoURL) {
|
||||
var photo = ContactPhotoHelper.getThumbnail(contact);
|
||||
if (photo) {
|
||||
details.photoURL = window.URL.createObjectURL(photo);
|
||||
}
|
||||
}
|
||||
|
||||
details.name = name;
|
||||
// We pick the first discovered org name as the phone number's detail
|
||||
// org information.
|
||||
details.org = details.org || org;
|
||||
}
|
||||
|
||||
// In no contact or contact with empty information cases, we will leave
|
||||
// the title as the empty string and let caller to decide the title.
|
||||
if (!contacts || (Array.isArray(contacts) && contacts.length === 0)) {
|
||||
details.title = '';
|
||||
} else if (!Array.isArray(contacts)) {
|
||||
updateDetails(contacts);
|
||||
details.title = details.name || details.org;
|
||||
} else {
|
||||
// Rule for fetching details with multiple contact entries:
|
||||
// 1) If we got more than 1 contact entry, find another entry if
|
||||
// current entry got no name/company.
|
||||
// 2) If we could not get any information from all the entries,
|
||||
// just display phone number.
|
||||
for (var i = 0, l = contacts.length; i < l; i++) {
|
||||
updateDetails(contacts[i]);
|
||||
if (details.name) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
details.title = details.name || details.org;
|
||||
}
|
||||
|
||||
return details;
|
||||
},
|
||||
|
||||
/**
|
||||
* Based on input number tries to extract more phone details like phone
|
||||
* type, full phone number and phone carrier.
|
||||
* 1. If a phone number has carrier associated with it then both "type" and
|
||||
* "carrier" will be returned;
|
||||
*
|
||||
* 2. If there is no carrier associated with the phone number then "type"
|
||||
* and "phone number" will be returned;
|
||||
*
|
||||
* 3. If for some reason a single contact has two phone numbers with the
|
||||
* same type and the same carrier then "type" and "phone number" will be
|
||||
* returned;
|
||||
*
|
||||
* note: The argument "tels" can actually contain "emails" too.
|
||||
*
|
||||
*/
|
||||
getPhoneDetails: function ut_getPhoneDetails(input, tels) {
|
||||
var length = tels.length;
|
||||
var hasUniqueCarriers = true;
|
||||
var hasUniqueTypes = true;
|
||||
var found, tel, type, carrier;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
tel = tels[i];
|
||||
|
||||
if (tel.value && Utils.probablyMatches(tel.value, input)) {
|
||||
found = tel;
|
||||
}
|
||||
|
||||
if (carrier && carrier === tel.carrier) {
|
||||
hasUniqueCarriers = false;
|
||||
}
|
||||
|
||||
if (type && type === tel.type[0]) {
|
||||
hasUniqueTypes = false;
|
||||
}
|
||||
|
||||
carrier = tel.carrier;
|
||||
type = (tel.type && tel.type[0]) || '';
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: (found.type && found.type[0]) || '',
|
||||
carrier: hasUniqueCarriers || hasUniqueTypes ? found.carrier : '',
|
||||
number: found.value
|
||||
};
|
||||
},
|
||||
|
||||
// Based on "non-dialables" in https://github.com/andreasgal/PhoneNumber.js
|
||||
//
|
||||
// @param {String} input Value to remove nondialiable chars from.
|
||||
//
|
||||
removeNonDialables: function ut_removeNonDialables(input) {
|
||||
return input.replace(rnondialablechars, '');
|
||||
},
|
||||
// @param {String} a First recipient field.
|
||||
// @param {String} b Second recipient field
|
||||
//
|
||||
// Based on...
|
||||
// - ITU-T E.123 (http://www.itu.int/rec/T-REC-E.123-200102-I/)
|
||||
// - ITU-T E.164 (http://www.itu.int/rec/T-REC-E.164-201011-I/)
|
||||
//
|
||||
// ...It would appear that a maximally-minimal
|
||||
// 7 digit comparison is safe.
|
||||
probablyMatches: function ut_probablyMatches(a, b) {
|
||||
var service = navigator.mozPhoneNumberService;
|
||||
|
||||
// String comparison starts here
|
||||
if (typeof a !== 'string' || typeof b !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Settings.supportEmailRecipient &&
|
||||
Utils.isEmailAddress(a) &&
|
||||
Utils.isEmailAddress(b)) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (service && service.normalize) {
|
||||
a = service.normalize(a);
|
||||
b = service.normalize(b);
|
||||
} else {
|
||||
a = Utils.removeNonDialables(a);
|
||||
b = Utils.removeNonDialables(b);
|
||||
}
|
||||
|
||||
return a === b || a.slice(-7) === b.slice(-7);
|
||||
},
|
||||
|
||||
/**
|
||||
* multiRecipientMatch
|
||||
*
|
||||
* Check multi-repients without regard to order
|
||||
*
|
||||
* @param {(String|string[])} a First recipient field.
|
||||
* @param {(String|string[])} b Second recipient field.
|
||||
*
|
||||
* @return {Boolean} Return true if all recipients match.
|
||||
*/
|
||||
multiRecipientMatch: function ut_multiRecipientMatch(a, b) {
|
||||
// When ES6 syntax is allowed, replace with
|
||||
// multiRecipientMatch([...a], [...b])
|
||||
a = [].concat(a);
|
||||
b = [].concat(b);
|
||||
var blen = b.length;
|
||||
if (a.length !== blen) {
|
||||
return false;
|
||||
}
|
||||
// Check each recipient in a against each in b
|
||||
// Allows for any order (and fails early)
|
||||
return a.every(function(number) {
|
||||
for (var i = 0; i < blen; i++) {
|
||||
if (Utils.probablyMatches(number, b[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Default image size limitation is set to 295KB for MMS user story.
|
||||
// If limit is not given or bigger than default 295KB, default value need
|
||||
// to be applied here for size checking. Parameters could be:
|
||||
// (blob, callback) : Resizing image to default limit 295k.
|
||||
// (blob, limit, callback) : Resizing image to given limitation.
|
||||
getResizedImgBlob: function ut_getResizedImgBlob(blob, limit, callback) {
|
||||
var defaultLimit = 295 * 1024;
|
||||
if (typeof limit === 'function') {
|
||||
callback = limit;
|
||||
limit = defaultLimit;
|
||||
}
|
||||
limit = limit === 0 ? defaultLimit : Math.min(limit, defaultLimit);
|
||||
|
||||
if (blob.size < limit) {
|
||||
setTimeout(function blobCb() {
|
||||
callback(blob);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var ratio = Math.sqrt(blob.size / limit);
|
||||
Utils._resizeImageBlobWithRatio({
|
||||
blob: blob,
|
||||
limit: limit,
|
||||
ratio: ratio,
|
||||
callback: callback
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets localization details for attachment size label.
|
||||
* @param size Attachment blob size in bytes.
|
||||
* @returns {{l10nId: string, l10nArgs: {n: string}}}
|
||||
*/
|
||||
getSizeForL10n: function ut_getSizeForL10n(size) {
|
||||
// blob size with unit (B or KB or MB)
|
||||
var sizeKB = size / 1024;
|
||||
var sizeMB = sizeKB / 1024;
|
||||
if (size < 1000) {
|
||||
return {
|
||||
l10nId: 'attachmentSizeB',
|
||||
l10nArgs: { n: size }
|
||||
};
|
||||
} else if (sizeKB < 1000) {
|
||||
return {
|
||||
l10nId: 'attachmentSizeKB',
|
||||
l10nArgs: { n: sizeKB.toFixed(1) }
|
||||
};
|
||||
}
|
||||
return {
|
||||
l10nId: 'attachmentSizeMB',
|
||||
l10nArgs: { n: sizeMB.toFixed(1) }
|
||||
};
|
||||
},
|
||||
|
||||
// resizeImageBlobWithRatio have additional ratio to force image
|
||||
// resize to smaller size to avoid edge case about quality adjustment
|
||||
// not working.
|
||||
// For JPG images, a ratio between 2 and 8 will be set to a close
|
||||
// power of 2. A ratio between 1 and 2 will be set to 2. A ratio bigger
|
||||
// than 8 will be rounded to the closest bigger integer.
|
||||
//
|
||||
_resizeImageBlobWithRatio: function ut_resizeImageBlobWithRatio(obj) {
|
||||
var blob = obj.blob;
|
||||
var callback = obj.callback;
|
||||
var limit = obj.limit;
|
||||
var ratio = Math.ceil(obj.ratio);
|
||||
var qualities = [0.65, 0.5, 0.25];
|
||||
|
||||
var sampleSize = 1;
|
||||
var sampleSizeHash = '';
|
||||
|
||||
if (blob.size < limit || ratio <= 1) {
|
||||
setTimeout(function blobCb() {
|
||||
callback(blob);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (blob.type === 'image/jpeg') {
|
||||
if (ratio >= 8) {
|
||||
sampleSize = 8;
|
||||
} else {
|
||||
sampleSize = ratio = Utils.getClosestSampleSize(ratio);
|
||||
}
|
||||
|
||||
sampleSizeHash = '#-moz-samplesize=' + sampleSize;
|
||||
}
|
||||
|
||||
var img = document.createElement('img');
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
img.src = url + sampleSizeHash;
|
||||
|
||||
img.onload = function onBlobLoaded() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
var targetWidth = img.width * sampleSize / ratio;
|
||||
var targetHeight = img.height * sampleSize / ratio;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
var context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
context.drawImage(img, 0, 0, targetWidth, targetHeight);
|
||||
img.src = '';
|
||||
// Bug 889765: Since we couldn't know the quality of the original jpg
|
||||
// The 'resized' image might have a bigger size because it was saved
|
||||
// with quality or dpi. Here we will adjust the jpg quality(or resize
|
||||
// blob again if low quality blob size still exceed limit) to make
|
||||
// sure the size won't exceed the limitation.
|
||||
var level = 0;
|
||||
|
||||
function cleanup() {
|
||||
canvas.width = canvas.height = 0;
|
||||
canvas = null;
|
||||
}
|
||||
|
||||
function ensureSizeLimit(resizedBlob) {
|
||||
if (resizedBlob.size < limit) {
|
||||
cleanup();
|
||||
|
||||
// using a setTimeout so that used objects can be garbage collected
|
||||
// right now
|
||||
setTimeout(callback.bind(null, resizedBlob));
|
||||
} else {
|
||||
resizedBlob = null; // we don't need it anymore
|
||||
// Reduce image quality for match limitation. Here we set quality
|
||||
// to 0.65, 0.5 and 0.25 for image blob resizing.
|
||||
// (Default image quality is 0.92 for jpeg)
|
||||
if (level < qualities.length) {
|
||||
canvas.toBlob(ensureSizeLimit, 'image/jpeg',
|
||||
qualities[level++]);
|
||||
} else {
|
||||
// We will resize the blob if image quality = 0.25 still exceed
|
||||
// size limitation.
|
||||
cleanup();
|
||||
|
||||
// using a setTimeout so that used objects can be garbage
|
||||
// collected right now
|
||||
setTimeout(
|
||||
Utils._resizeImageBlobWithRatio.bind(Utils, {
|
||||
blob: blob,
|
||||
limit: limit,
|
||||
ratio: ratio * 2,
|
||||
callback: callback
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.toBlob(ensureSizeLimit, blob.type);
|
||||
};
|
||||
},
|
||||
|
||||
getClosestSampleSize: function ut_getClosestSampleSize(ratio) {
|
||||
if (ratio >= 8) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
if (ratio >= 4) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (ratio >= 2) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
},
|
||||
|
||||
camelCase: function ut_camelCase(str) {
|
||||
return str.replace(rdashes, function replacer(str, p1) {
|
||||
return p1.toUpperCase();
|
||||
});
|
||||
},
|
||||
typeFromMimeType: function ut_typeFromMimeType(mime) {
|
||||
var MAX_MIME_TYPE_LENGTH = 256; // ought to be enough for anybody
|
||||
if (typeof mime !== 'string' || mime.length > MAX_MIME_TYPE_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var index = mime.indexOf('/');
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
var mainPart = mime.slice(0, index);
|
||||
var secondPart = mime.substr(index + 1).toLowerCase();
|
||||
|
||||
switch (mainPart) {
|
||||
case 'image':
|
||||
return 'img';
|
||||
case 'text':
|
||||
if(secondPart.indexOf('vcard') !== -1) {
|
||||
return 'vcard';
|
||||
}
|
||||
if (secondPart !== 'plain') {
|
||||
return 'ref';
|
||||
}
|
||||
return mainPart;
|
||||
case 'video':
|
||||
case 'audio':
|
||||
case 'application':
|
||||
return mainPart;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
params: function(input) {
|
||||
var parsed = {};
|
||||
input.replace(rparams, function($0, $1, $2) {
|
||||
if ($2 === 'true') {
|
||||
$2 = true;
|
||||
} else if ($2 === 'false') {
|
||||
$2 = false;
|
||||
}
|
||||
parsed[$1] = $2;
|
||||
});
|
||||
return parsed;
|
||||
},
|
||||
url(base, params) {
|
||||
if (base.indexOf('?') === -1) {
|
||||
base += '?';
|
||||
} else {
|
||||
base += '&';
|
||||
}
|
||||
|
||||
for (var key in params) {
|
||||
if (params[key] == null) { // null or undefined
|
||||
continue;
|
||||
}
|
||||
|
||||
base +=
|
||||
encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]) + '&';
|
||||
}
|
||||
|
||||
return base.slice(0, -1);
|
||||
},
|
||||
basicContact: function(number, records) {
|
||||
var record;
|
||||
if (Array.isArray(records)) {
|
||||
if (records.length > 0) {
|
||||
record = records[0];
|
||||
}
|
||||
} else if (records !== null) {
|
||||
record = records;
|
||||
}
|
||||
|
||||
// Only exit when no record and no phone number case.
|
||||
if (!record && !number) {
|
||||
return;
|
||||
}
|
||||
|
||||
var telLength = (record && record.tel) ? record.tel.length : 0;
|
||||
var tel;
|
||||
// Look for the right tel. A record can contains more than
|
||||
// one record, so we need to identify which one is the right one.
|
||||
for (var i = 0; i < telLength; i++) {
|
||||
if (record.tel[i].value === number) {
|
||||
tel = record.tel[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If after looking there is no tel. matching, we apply
|
||||
// directly the number
|
||||
if (!tel) {
|
||||
tel = {type: [''], value: number, carrier: ''};
|
||||
}
|
||||
// Get the title in the standard way
|
||||
var details = Utils.getContactDetails(tel, record);
|
||||
var info = Utils.getDisplayObject(details.title || null, tel);
|
||||
|
||||
return info;
|
||||
},
|
||||
|
||||
/*
|
||||
Given a title for a contact, a the current information for
|
||||
an specific phone, of that contact, creates an object with
|
||||
all the information needed to display data.
|
||||
*/
|
||||
getDisplayObject: function(theTitle, tel) {
|
||||
var number = tel.value;
|
||||
var type = tel.type && tel.type.length ? tel.type[0] : '';
|
||||
var data = {
|
||||
name: theTitle || number,
|
||||
number: number,
|
||||
type: type,
|
||||
carrier: tel.carrier || ''
|
||||
};
|
||||
|
||||
if (Settings.supportEmailRecipient) {
|
||||
data.email = number;
|
||||
data.emailHTML = '';
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
/*
|
||||
TODO: It's workaround to avoid url revoke bug. Need platform fixing
|
||||
to remove the async load/remove.(Please ref bug 972245)
|
||||
*/
|
||||
asyncLoadRevokeURL: function(url) {
|
||||
setTimeout(function() {
|
||||
var image = new Image();
|
||||
image.src = url;
|
||||
image.onload = image.onerror = function revokePhotoURL() {
|
||||
window.URL.revokeObjectURL(this.src);
|
||||
};
|
||||
});
|
||||
},
|
||||
/*
|
||||
TODO: Email Address check.
|
||||
*/
|
||||
isEmailAddress: function(email) {
|
||||
return rmail.test(email);
|
||||
},
|
||||
/**
|
||||
* Helper function for removing notifications. It will fetch notification
|
||||
* using threadId, and close them from the returned list.
|
||||
* @param {String|Number} threadId of the target message.
|
||||
* @returns {Promise.<undefined>} Return always successfully resolved
|
||||
* Promise with notification.close method called or console.error message.
|
||||
*/
|
||||
closeNotificationsForThread: function(threadId) {
|
||||
var targetTag = 'threadId:' + threadId;
|
||||
|
||||
return Notification.get({tag: targetTag})
|
||||
.then(
|
||||
function onSuccess(notifications) {
|
||||
for (var i = 0; i < notifications.length; i++) {
|
||||
notifications[i].close();
|
||||
}
|
||||
}
|
||||
).catch(function onError(reason) {
|
||||
console.error('Notification.get(tag: ' + targetTag + '): ', reason);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a function that will call specified "func" function only after it
|
||||
* stops being called for a specified wait time.
|
||||
* @param {function} func Function to call.
|
||||
* @param {number} waitTime Number of milliseconds to wait before calling
|
||||
* actual "func" function once debounced function stops being called.
|
||||
* @returns {function}
|
||||
*/
|
||||
debounce: function(func, waitTime) {
|
||||
var timeout, args, context;
|
||||
|
||||
var executeLater = function() {
|
||||
func.apply(context, args);
|
||||
timeout = context = args = null;
|
||||
};
|
||||
|
||||
return function() {
|
||||
context = this;
|
||||
args = arguments;
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(executeLater, waitTime);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a rate limited function, useful to reduce the number of function
|
||||
* calls when listening to user input (keypress) or browser events.
|
||||
* @params {function} func - The function that should be rate-limited
|
||||
* @params {number=300} delay - Min amount of time (ms) between two calls
|
||||
* @params {Object} options - Parameters for managing edge calls.
|
||||
* Attention : when both preventFirstCall and preventLastCall are set to
|
||||
* true, `func` may never be called.
|
||||
* @param {Boolean=false} options.preventFirstCall - Prevent first
|
||||
* call when true.
|
||||
* @param {Boolean=false} options.preventLastCall - Prevent last trailing
|
||||
* call when true.
|
||||
* @returns {function} - The rate limited function that will call the
|
||||
* original function
|
||||
*/
|
||||
throttle: function(func, delay = 300, options = {}) {
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
var preventFirstCall = !!options.preventFirstCall;
|
||||
var preventLastCall = !!options.preventLastCall;
|
||||
|
||||
if(typeof func !== 'function'){
|
||||
throw new Error('func must be a Function');
|
||||
}
|
||||
|
||||
if(typeof delay !== 'number' || delay < 0){
|
||||
throw new Error('delay must be a positive number');
|
||||
}
|
||||
|
||||
return function(...args) {
|
||||
var now = Date.now();
|
||||
if (!previous && preventFirstCall){
|
||||
previous = now;
|
||||
}
|
||||
var remaining = delay - (now - previous);
|
||||
if (remaining <= 0) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
func.apply(this, args);
|
||||
} else if (!timeout && !preventLastCall) {
|
||||
timeout = setTimeout(() => {
|
||||
previous = preventFirstCall ? 0 : Date.now();
|
||||
timeout = null;
|
||||
|
||||
func.apply(this, args);
|
||||
}, remaining);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows modal alert dialog with single OK button to dismiss it.
|
||||
* @param {string|{ raw: string|Node }|{id: string, args: Object }} message
|
||||
* Message displayed in the alert. 1. If "message" is string then it's
|
||||
* considered as l10n identifier; 2. if "message" is object with "raw"
|
||||
* property then "raw" property is used as non-localizable string or as a
|
||||
* complete Node; 3. If "message" is object with "id" and/or "args"
|
||||
* properties then "id" is considered as l10n identifier and "args" as l10n
|
||||
* string arguments.
|
||||
* @param {string|{ raw: string|Node }|{id: string, args: Object }} title
|
||||
* Optional dialog title, if not passed - default title is used. For
|
||||
* possible parameter value see "message" parameter description above.
|
||||
* @returns {Promise} Return promise is always successfully resolved.
|
||||
*/
|
||||
alert: function (message, title) {
|
||||
var deferred = this.Promise.defer();
|
||||
|
||||
var dialog = new Dialog({
|
||||
title: title || 'modal-dialog-default-title',
|
||||
body: message,
|
||||
options: {
|
||||
cancel: {
|
||||
text: 'modal-dialog-ok-button',
|
||||
method: deferred.resolve
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows modal confirm dialog with two buttons, first acts as cancel action,
|
||||
* second one - as confirm action.
|
||||
* @param {string|{ raw: string|Node }|{id: string, args: Object }} message
|
||||
* Message displayed in the confirm. 1. If "message" is string then it's
|
||||
* considered as l10n identifier; 2. if "message" is object with "raw"
|
||||
* property then "raw" property is used as non-localizable string or as a
|
||||
* complete Node; 3. If "message" is object with "id" and/or "args"
|
||||
* properties then "id" is considered as l10n identifier and "args" as l10n
|
||||
* string arguments.
|
||||
* @param {string|{ raw: string|Node }|{id: string, args: Object }} title
|
||||
* Optional dialog title, if not passed - default title is used. For
|
||||
* possible parameter value see "message" parameter description above.
|
||||
* @param {{ text: string|{ raw: string|Node }|{id: string, args: Object },
|
||||
* className: string}} confirmOptions Optional customizations for confirm
|
||||
* button, custom text(for possible parameter values see "message"
|
||||
* parameter description above) and custom class name.
|
||||
* @returns {Promise} Returned promise is resolved when user tap on confirm
|
||||
* button and rejected when user taps on cancel button.
|
||||
*/
|
||||
confirm: function (message, title, confirmOptions) {
|
||||
var confirmButtonText = confirmOptions && confirmOptions.text ||
|
||||
'modal-dialog-ok-button';
|
||||
var confirmButtonClassName = confirmOptions && confirmOptions.className ||
|
||||
'recommend';
|
||||
|
||||
var deferred = this.Promise.defer();
|
||||
var dialog = new Dialog({
|
||||
title: title || 'modal-dialog-default-title',
|
||||
body: message,
|
||||
options: {
|
||||
cancel: {
|
||||
text: 'modal-dialog-cancel-button',
|
||||
method: deferred.reject
|
||||
},
|
||||
|
||||
confirm: {
|
||||
text: confirmButtonText,
|
||||
className: confirmButtonClassName,
|
||||
method: deferred.resolve
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Promise related utilities
|
||||
*/
|
||||
Promise: {
|
||||
/**
|
||||
* The Defer type is useful when creating promises.
|
||||
* @typedef {Object} Defer
|
||||
* @property {function(*)} resolve The Promise's resolve function.
|
||||
* @property {function(*)} reject The Promise's reject function.
|
||||
* @property {Promise} promise The actual promise.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns object that contains promise and related resolve\reject methods
|
||||
* to avoid wrapping long or complex code into single Promise constructor.
|
||||
* @returns {Defer}
|
||||
*/
|
||||
defer: function() {
|
||||
var deferred = {};
|
||||
|
||||
deferred.promise = new Promise(function(resolve, reject) {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps a generator function that yields Promises in a way that generator
|
||||
* flow is paused until yielded Promise is resolved, so that consumer gets
|
||||
* Promise result instead of Promise instance itself.
|
||||
* See https://www.promisejs.org/generators/ as the reference.
|
||||
* @param {function(...*): Iterator} generatorFunction Generator function
|
||||
* that yields Promises.
|
||||
* @return {function}
|
||||
*/
|
||||
async: function(generatorFunction) {
|
||||
return function asyncGenerator() {
|
||||
var generator = generatorFunction.apply(this, arguments);
|
||||
|
||||
function handle(result) {
|
||||
if (result.done) {
|
||||
return Promise.resolve(result.value);
|
||||
}
|
||||
|
||||
return Promise.resolve(result.value).then(
|
||||
(result) => handle(generator.next(result)),
|
||||
(error) => handle(generator.throw(error))
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return handle(generator.next());
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the localized SIM name for multi-SIM scenario or empty string for
|
||||
* single-SIM scenario
|
||||
* @param {Number} iccId Integrate circuit card identity for SIM.
|
||||
* @return {Promise} Return the async translation result like SIM1 or SIM2
|
||||
* (locale dependent) depending on the iccId or empty string for single-sim
|
||||
* scenario.
|
||||
*/
|
||||
getSimNameByIccId(iccId) {
|
||||
var index = Settings.getServiceIdByIccId(iccId);
|
||||
|
||||
if (index === null) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
return document.l10n.formatValue('sim-id-label', { id: index + 1 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes shim host iframe with the current app instance id.
|
||||
* @param {string} appInstanceId Unique identifier of the app instance.
|
||||
* @return {Promise.<void>} Resolves once shim host is initialized.
|
||||
*/
|
||||
initializeShimHost(appInstanceId) {
|
||||
var shimHostIframe = document.querySelector('.shim-host');
|
||||
|
||||
var promise = typeof shimHostIframe.contentWindow.bootstrap === 'function' ?
|
||||
Promise.resolve() :
|
||||
new Promise((resolve) => {
|
||||
shimHostIframe.addEventListener('load', function onLoad() {
|
||||
shimHostIframe.removeEventListener('load', onLoad);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return promise.then(
|
||||
() => {
|
||||
// Workaround for the Gecko XPCOM issue discussed in bug 1233552, in
|
||||
// short if we pass handler function created inside nested iframe
|
||||
// to the mozSetMessageHandler from the main window, then the handler
|
||||
// could never be called in some cases (e.g. slow environment).
|
||||
window.mozSetMessageHandler = function(type, handler) {
|
||||
navigator.mozSetMessageHandler(type, (request) => handler(request));
|
||||
};
|
||||
shimHostIframe.contentWindow.bootstrap(appInstanceId);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns Promise instance that is resolved instantly if document is
|
||||
* visible (document.hidden === false), othewise it's resolved only once
|
||||
* `visibilitychange` event is fired.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
onceDocumentIsVisible() {
|
||||
if (!document.hidden) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var defer = this.Promise.defer();
|
||||
|
||||
document.addEventListener('visibilitychange', function waitVisibility() {
|
||||
document.removeEventListener('visibilitychange', waitVisibility);
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Utils = Utils;
|
||||
|
||||
}(this));
|
||||
23
gaia/apps/sms/views/shared/js/waiting_screen.js
Executable file
23
gaia/apps/sms/views/shared/js/waiting_screen.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/*exported WaitingScreen */
|
||||
|
||||
'use strict';
|
||||
|
||||
var WaitingScreen = {
|
||||
get loading() {
|
||||
delete this.loading;
|
||||
return (this.loading = document.getElementById('loading'));
|
||||
},
|
||||
get loadingHeader() {
|
||||
delete this.loadingHeader;
|
||||
return (this.loadingHeader = document.getElementById('loading-header'));
|
||||
},
|
||||
show: function ws_show() {
|
||||
this.loading.classList.add('show-loading');
|
||||
},
|
||||
hide: function ws_hide() {
|
||||
this.loading.classList.remove('show-loading');
|
||||
}
|
||||
};
|
||||
104
gaia/apps/sms/views/shared/js/wbmp.js
Executable file
104
gaia/apps/sms/views/shared/js/wbmp.js
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
// see http://www.wapforum.org/what/technical/SPEC-WAESpec-19990524.pdf
|
||||
|
||||
/*exported WBMP */
|
||||
|
||||
'use strict';
|
||||
|
||||
var WBMP = (function(document) {
|
||||
function decode(arrayBuffer, callback) {
|
||||
// Pointer into the byte stream.
|
||||
var bytes = new Uint8Array(arrayBuffer);
|
||||
var ptr = 0;
|
||||
|
||||
// Read unsigned octet from the byte stream.
|
||||
function readOctet() {
|
||||
return bytes[ptr++] & 0xff;
|
||||
}
|
||||
|
||||
// Read unsigned multi-byte integer (6.3.1) from the byte stream.
|
||||
function readMultiByteInteger() {
|
||||
var result = 0;
|
||||
while (true) {
|
||||
if (result & 0xfe000000) {
|
||||
throw 'error parsing integer';
|
||||
}
|
||||
|
||||
var b = bytes[ptr++];
|
||||
result = (result << 7) | (b & 0x7f);
|
||||
if (!(b & 0x80)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write a pixel
|
||||
function write(data, w, bit) {
|
||||
var color = bit ? 255 : 0;
|
||||
data[w] = color;
|
||||
data[w + 1] = color;
|
||||
data[w + 2] = color;
|
||||
data[w + 3] = 255;
|
||||
}
|
||||
|
||||
try {
|
||||
// We only support image type 0: B/W, no compression
|
||||
if (readMultiByteInteger() !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't expect any extended headers here.
|
||||
if (readOctet() !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var width = readMultiByteInteger();
|
||||
var height = readMultiByteInteger();
|
||||
// Reject incorrect image dimensions.
|
||||
if (width === 0 || width > 65535 || height === 0 || height > 65535) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a canvas to draw the pixels into.
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', width);
|
||||
canvas.setAttribute('height', height);
|
||||
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
var imageData = ctx.createImageData(width, height);
|
||||
var data = imageData.data;
|
||||
// Decode the image.
|
||||
for (var y = 0; y < height; ++y) {
|
||||
for (var x = 0; x < width; x += 8) {
|
||||
var bits = bytes[ptr++];
|
||||
var w = (y * width + x) * 4;
|
||||
write(data, w, bits & 0x80);
|
||||
write(data, w + 4, bits & 0x40);
|
||||
write(data, w + 8, bits & 0x20);
|
||||
write(data, w + 12, bits & 0x10);
|
||||
write(data, w + 16, bits & 0x08);
|
||||
write(data, w + 20, bits & 0x04);
|
||||
write(data, w + 24, bits & 0x02);
|
||||
write(data, w + 28, bits & 0x01);
|
||||
}
|
||||
}
|
||||
|
||||
if (ptr > bytes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the canvas pixels.
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
// Convert to an image.
|
||||
canvas.toBlob(callback);
|
||||
} catch (e) {
|
||||
// Error occured.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
decode: decode
|
||||
};
|
||||
})(document);
|
||||
Loading…
Reference in a new issue