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.
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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
/* global Startup */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Startup.init();
|
||||
})(self);
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
21
gaia/apps/sms/views/shared/shim_host.html
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Messages Shim Host</title>
|
||||
<!-- Firefox Desktop shims -->
|
||||
<script src="/desktop-mock/navigator_moz_mobilemessage_shim.js"></script>
|
||||
<script src="/desktop-mock/navigator_moz_settings_shim.js"></script>
|
||||
<script src="/desktop-mock/navigator_moz_mobile_connections.js"></script>
|
||||
<!-- Shim scripts -->
|
||||
<script src="/lib/bridge/service.js"></script>
|
||||
<script src="/lib/bridge/plugins/stream/service.js"></script>
|
||||
<script src="/services/js/bridge_service_mixin.js"></script>
|
||||
<script src="/services/js/activity/activity_shim.js"></script>
|
||||
<script src="/services/js/moz_settings/moz_settings_shim.js"></script>
|
||||
<script src="/services/js/moz_mobile_message/moz_mobile_message_shim.js"></script>
|
||||
<script src="/services/js/moz_mobile_connections/moz_mobile_connections_shim.js"></script>
|
||||
<script src="/views/shared/js/shim_host_bootstrap.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
25
gaia/apps/sms/views/shared/style/common.css
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Structure of app
|
||||
*/
|
||||
|
||||
.view-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: -moz-calc(100% - 5rem);
|
||||
width: 100%;
|
||||
/* This box-sizing is necessary to prevent vertical scrolling in views that
|
||||
* specify vertical paddings and/or borders. */
|
||||
-moz-box-sizing: border-box;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
/* Style of highlighted text. */
|
||||
.highlight {
|
||||
background-color: #b2f2ff;
|
||||
}
|
||||
|
||||
/* Hidden elements */
|
||||
.hide, [data-template] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
278
gaia/apps/sms/views/shared/style/composer.css
Executable file
|
|
@ -0,0 +1,278 @@
|
|||
#messages-compose-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: auto; /* overrides building blocks */
|
||||
min-height: 4rem;
|
||||
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Overwrite. No more fixed height */
|
||||
form[role="search"] p input,
|
||||
form[role="search"] p textarea,
|
||||
form[role="search"] button[type="submit"] + p > textarea {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Input now are contentEditable containers. We define
|
||||
* placeholder management here as well.
|
||||
*/
|
||||
|
||||
#messages-input {
|
||||
position: relative; /* to position its generated content */
|
||||
|
||||
overflow: auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-inline-end: 0.5rem;
|
||||
|
||||
color: #474747;
|
||||
border-inline-end: 0.1rem solid #ccc;
|
||||
|
||||
font-size: 1.7rem;
|
||||
line-height: 2.1rem;
|
||||
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
|
||||
#messages-input.placeholder {
|
||||
unicode-bidi: unset;
|
||||
}
|
||||
|
||||
#messages-input:after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
color: #9b9b9b;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#messages-input.placeholder:after {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Overwrite BB because of the previous rule */
|
||||
form[role="search"] button[type="submit"]:after {
|
||||
content: "";
|
||||
background: none;
|
||||
}
|
||||
|
||||
.mms-label-line {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
background-color: #e1f0ec;
|
||||
}
|
||||
|
||||
.subject-composer {
|
||||
position: relative;
|
||||
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
padding: 1rem 0 0.3rem;
|
||||
}
|
||||
|
||||
.mms-label {
|
||||
padding: 1rem 0 0.3rem;
|
||||
}
|
||||
|
||||
.mms-label,
|
||||
.message-counter {
|
||||
display: none;
|
||||
flex: none;
|
||||
text-align: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 6rem;
|
||||
|
||||
color: #575757;
|
||||
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
padding-top: .7rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. "MMS" label is displayed near the subject field when subject input is
|
||||
* visible and message type is MMS;
|
||||
* 2. Remaining characters counter is displayed in bottom section if character
|
||||
* count exceeds predefined threshold and message type is SMS.
|
||||
*/
|
||||
[data-message-type='mms'] .mms-label,
|
||||
[data-message-type='sms'] .message-counter:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-composer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.subject-composer-input {
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-height: 2.2rem;
|
||||
padding: 0;
|
||||
padding-bottom: 0.1rem;
|
||||
padding-inline-start: 1.5rem;
|
||||
|
||||
color: #008787;
|
||||
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.subject-composer-placeholder {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
/* adding both left/right so that it behaves correctly in RTL */
|
||||
left: 1.5rem;
|
||||
right: 1.5rem;
|
||||
|
||||
color: #00adad;
|
||||
font-size: 1.7rem;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.composer-message-input-container {
|
||||
position: relative;
|
||||
|
||||
flex: auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 4rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.composer-button-container {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
width: 6rem;
|
||||
|
||||
-moz-user-select: none; /* without this, tapping on the element selects its content */
|
||||
}
|
||||
|
||||
.messages-attach-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#messages-send-button {
|
||||
height: 4rem;
|
||||
overflow: visible;
|
||||
background: #fff no-repeat center center / 3rem;
|
||||
|
||||
align-self: flex-end;
|
||||
}
|
||||
html[dir="ltr"] #messages-send-button {
|
||||
background-image: url(images/icons/actionicon_sms_send_30x30.png);
|
||||
/* When we put the icon at the center, an optical illusion makes it look to be
|
||||
* slightly to the left, so we offset the button by 2px to the right to
|
||||
* counteract this illusion */
|
||||
transform: translate(.2rem);
|
||||
}
|
||||
html[dir="rtl"] #messages-send-button {
|
||||
background-image: url(images/icons/actionicon_sms_send_30x30_rtl.png);
|
||||
transform: translate(-.2rem);
|
||||
}
|
||||
|
||||
#messages-send-button:after {
|
||||
display: none
|
||||
}
|
||||
|
||||
#messages-send-button[disabled] {
|
||||
background-blend-mode: luminosity;
|
||||
opacity: 0.625;
|
||||
}
|
||||
|
||||
#messages-attach-button {
|
||||
align-self: flex-end;
|
||||
flex: none;
|
||||
|
||||
box-sizing: content-box;
|
||||
width: 3rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-inline-start: 0.5rem;
|
||||
padding-inline-end: 1rem;
|
||||
|
||||
background: transparent
|
||||
url('images/icons/actionicon_sms_add_attachment_30x30.png')
|
||||
no-repeat center / 3rem;
|
||||
}
|
||||
|
||||
#messages-attach-button[disabled] {
|
||||
background-blend-mode: luminosity;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/*
|
||||
DSDS
|
||||
*/
|
||||
|
||||
.sim-indication {
|
||||
position: absolute;
|
||||
offset-inline-end: 1.8rem;
|
||||
bottom: 0.4rem;
|
||||
|
||||
display: none;
|
||||
line-height: 1;
|
||||
|
||||
color: #5f5f5f;
|
||||
font-size: 1.4rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.sim-indication:before {
|
||||
position: absolute;
|
||||
/* 2.6rem from the end; means .8rem from .sim-indication */
|
||||
offset-inline-end: .8rem;
|
||||
bottom: -0.1rem;
|
||||
|
||||
content: '...';
|
||||
|
||||
color: #b2b2b2;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
button[disabled] .sim-indication {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.has-preferred-sim .sim-indication {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override Building Block [Input areas] styles
|
||||
* We should get rid of all [role="search"] Messages rules in bug 1055357.
|
||||
*/
|
||||
form.bottom[role="search"] {
|
||||
z-index: 3;
|
||||
}
|
||||
108
gaia/apps/sms/views/shared/style/edit-mode.css
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* This file contains styles shared between views when they're in edit mode.
|
||||
* View edit mode is activated via Options -> Select threads/messages.
|
||||
*/
|
||||
|
||||
|
||||
/* Override Building Block [Edit mode] styles */
|
||||
form[role="dialog"][data-type="edit"] gaia-header {
|
||||
transform: translateY(-110%);
|
||||
}
|
||||
|
||||
form[role="dialog"][data-type="edit"] > menu {
|
||||
transform: translateY(110%);
|
||||
}
|
||||
|
||||
/* Add a margin at the end of the last thread or message node to avoid overlay
|
||||
* with the edit mode menu */
|
||||
.edit .edit-container {
|
||||
padding-bottom: 7rem;
|
||||
}
|
||||
|
||||
#main-wrapper.edit [data-type="list"] li a {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit .bottom-tab[role="region"] {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Override Building Block Tabs styles */
|
||||
form[role="dialog"][data-type="edit"] .bb-tablist:before {
|
||||
bottom: 5.5rem ;
|
||||
}
|
||||
|
||||
form[role="dialog"][data-type="edit"] .bb-tablist > li {
|
||||
height: 5.5rem ;
|
||||
}
|
||||
|
||||
.bb-tablist > li button {
|
||||
background-position: 50% 50% ;
|
||||
}
|
||||
|
||||
.bb-tablist > li button[data-action="delete-button"] {
|
||||
background-image: url("images/icons/delete.png");
|
||||
}
|
||||
|
||||
/* Lists: edit mode */
|
||||
html[dir="ltr"] .edit [data-type="list"] li > a {
|
||||
transform: translateX(-1.5rem);
|
||||
}
|
||||
html[dir="rtl"] .edit [data-type="list"] li > a {
|
||||
transform: translateX(1.5rem);
|
||||
}
|
||||
|
||||
.edit [data-type="list"] li > label {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
html[dir="ltr"] .edit [data-type="list"] li > label {
|
||||
transform: translateX(2rem);
|
||||
}
|
||||
html[dir="rtl"] .edit [data-type="list"] li > label {
|
||||
transform: translateX(-2rem);
|
||||
}
|
||||
|
||||
.edit [data-type="list"] li > .pack-checkbox input ~ span:after {
|
||||
opacity: 1;
|
||||
}
|
||||
html[dir="ltr"] .edit [data-type="list"] li > .pack-checkbox input ~ span:after {
|
||||
transform: translateX(2.5rem);
|
||||
}
|
||||
html[dir="rtl"] .edit [data-type="list"] li > .pack-checkbox input ~ span:after {
|
||||
transform: translateX(-2.5rem);
|
||||
}
|
||||
|
||||
.edit [data-type="list"] li > a > p {
|
||||
margin-inline-end: 3.4rem;
|
||||
}
|
||||
html[dir="ltr"] .edit [data-type="list"] li > a > p {
|
||||
transform: translateX(5rem);
|
||||
}
|
||||
html[dir="rtl"] .edit [data-type="list"] li > a > p {
|
||||
transform: translateX(-5rem);
|
||||
}
|
||||
|
||||
/* Currently is used in ConversationView only */
|
||||
html[dir="ltr"] .edit [data-type="list"] .danger {
|
||||
transform: translateX(2.5rem);
|
||||
}
|
||||
html[dir="rtl"] .edit [data-type="list"] .danger {
|
||||
transform: translateX(-2.5rem);
|
||||
}
|
||||
|
||||
form[role="dialog"][data-type="edit"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.edit form[role="dialog"][data-type="edit"] {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.edit form[role="dialog"][data-type="edit"] > menu {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.edit form[role="dialog"][data-type="edit"] gaia-header {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 784 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1 KiB |
BIN
gaia/apps/sms/views/shared/style/images/icons/actionicon_sms_send_30x30.png
Executable file
|
After Width: | Height: | Size: 292 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/actionicon_sms_send_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 833 B |
|
After Width: | Height: | Size: 692 B |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
gaia/apps/sms/views/shared/style/images/icons/actionicon_sms_send_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 572 B |
|
After Width: | Height: | Size: 960 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/actionicon_sms_send_30x30_rtl.png
Executable file
|
After Width: | Height: | Size: 483 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/delete.png
Executable file
|
After Width: | Height: | Size: 289 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/delete@1.5x.png
Executable file
|
After Width: | Height: | Size: 389 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/delete@2.25x.png
Executable file
|
After Width: | Height: | Size: 616 B |
BIN
gaia/apps/sms/views/shared/style/images/icons/delete@2x.png
Executable file
|
After Width: | Height: | Size: 467 B |
34
gaia/apps/sms/views/shared/style/notification.css
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
/* Most of this should probably move to the Building Blocks: Bug 1011418 */
|
||||
|
||||
section[role="notification"] {
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
position: relative; /* so that we can compete
|
||||
with absolutely positioned elements */
|
||||
|
||||
overflow: hidden;
|
||||
min-height: 3.5rem;
|
||||
|
||||
background-color: rgba(77,77,77,0.95);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
section[role="notification"] a {
|
||||
color: #9ef2e7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
section[role="notification"] p {
|
||||
margin: .9rem 1.5rem;
|
||||
line-height: 1.7rem;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section[role="notification"] a {
|
||||
color: #9ef2e7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
25
gaia/apps/sms/views/shared/style/panel.css
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
.panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
background: white;
|
||||
|
||||
animation: var(--transition-duration) both ease;
|
||||
}
|
||||
|
||||
.panel {
|
||||
transform: translate(100%); /* BiDi proof */
|
||||
}
|
||||
|
||||
.panel-active {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
.panel-hidden {
|
||||
display: none;
|
||||
}
|
||||
172
gaia/apps/sms/views/shared/style/root.css
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
/* Global */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: #000;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.2rem;
|
||||
vertical-align: middle;
|
||||
background: var(--header-background);
|
||||
}
|
||||
|
||||
/* Text conventions */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul, ol, dl, dt, dd {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
p, li {
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
q {
|
||||
quotes: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre, blockquote {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
abbr, acronym {
|
||||
cursor: default;
|
||||
border: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
color: #ff4e00;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a abbr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 2rem 0 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
fieldset.first {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
font: 0/0 a;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input, textarea, select, button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
height: 4.6rem;
|
||||
}
|
||||
|
||||
input[type="text"], textarea {
|
||||
font-size: 1.3rem;
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
border-color: #7a7a7a;
|
||||
}
|
||||
|
||||
[contenteditable] {
|
||||
outline: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Media */
|
||||
img {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img:-moz-broken {
|
||||
line-height: 1.1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* HTML 5 */
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup,
|
||||
menu, nav, section, video, audio, canvas, progress, meter, time {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
261
gaia/apps/sms/views/shared/style/sms.css
Executable file
|
|
@ -0,0 +1,261 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.shim-host {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Default alignment in case a dir attribute is specified. */
|
||||
html[dir="ltr"] [dir] { text-align: left; }
|
||||
html[dir="rtl"] [dir] { text-align: right; }
|
||||
|
||||
/*
|
||||
Override BB. Some CSS tweaks for having a better confirm readability
|
||||
*/
|
||||
form[role="dialog"][data-type="confirm"] p {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
form[role="dialog"].error-dialog-show-recipient ul {
|
||||
border: none;
|
||||
}
|
||||
|
||||
form[role="dialog"].error-dialog-show-recipient ul li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* This does not override the building block, that's why we use a shorter
|
||||
* selector */
|
||||
.error-dialog-show-recipient li {
|
||||
/* future-proof in case BB moves to "text-align: start" */
|
||||
width: -moz-fit-content;
|
||||
|
||||
/* List items are phone numbers */
|
||||
direction: ltr;
|
||||
unicode-bidi: -moz-isolate;
|
||||
}
|
||||
|
||||
gaia-header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
position: absolute;
|
||||
z-index: 6; /* should be between 4 and 9 (inclusive). 3 is the z-index for
|
||||
the input form, 10 is the z-index for the header */
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
/* Container for conversation/new message view */
|
||||
#composer-container {
|
||||
position: relative;
|
||||
|
||||
background: #e1f0ec;
|
||||
}
|
||||
|
||||
/*
|
||||
* Flex layout is wrapped by 'flex-composer'. Our flex layout has
|
||||
* 2 well differentiated:
|
||||
* - 'messages-container'
|
||||
* - 'messages-compose-form'
|
||||
*/
|
||||
|
||||
.flex-composer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
top: 0;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Override Building Block [Lists] styles */
|
||||
[data-type="list"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bottom-tab[role="region"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[data-type="list"] header {
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
[data-type="list"] li > label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-type="list"] aside.pack-end {
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
/* Panel handling */
|
||||
#main-wrapper {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Loading screen while deleting
|
||||
*/
|
||||
|
||||
#loading.show-loading {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-header {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
#loading [role="status"] {
|
||||
text-align: center;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
#loading p {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.article-list[data-type="list"] ul.contact-list {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
width: calc(100% - 3.1rem);
|
||||
padding: 0 1.5rem;
|
||||
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: It's an adjustment for line-height/padding to prevent bottom of chars
|
||||
* been cut. This fix will be moved to shared list.css once Bug 1144588 landed
|
||||
*/
|
||||
.contact-list .suggestion > .name {
|
||||
line-height: 2.4rem;
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
|
||||
form[role="dialog"][data-type="action"].contact-prompt > header {
|
||||
height: auto;
|
||||
min-height: 5rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.unknown-contact-header {
|
||||
line-height: 2.5rem;
|
||||
padding: 1.3rem 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.contact-header {
|
||||
/* bottom padding is .9rem because we need .1rem more for the
|
||||
* nice shadow line at the header bottom, defined as a :after element */
|
||||
padding: 0.8rem 0 0.9rem;
|
||||
}
|
||||
|
||||
.contact-header .name {
|
||||
line-height: 3.3rem;
|
||||
padding: 0.1rem 0 0.4rem;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.contact-header .details {
|
||||
line-height: 1.5rem;
|
||||
padding: 0 0 0.7rem;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-header span[data-type=img] {
|
||||
display: block;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.contact-header aside.pack-end {
|
||||
margin-inline-end: -1.5rem; /* compensate the header's 3rem padding */
|
||||
}
|
||||
html[dir="ltr"] .contact-header aside.pack-end { float: right; }
|
||||
html[dir="rtl"] .contact-header aside.pack-end { float: left; }
|
||||
|
||||
.suggestion * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
form[role="dialog"][data-type="action"] > header {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
/* Phone details label styles */
|
||||
.phone-type,
|
||||
.phone-type-separator,
|
||||
.phone-carrier,
|
||||
.phone-carrier-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.has-phone-type > .phone-type,
|
||||
.has-phone-type > .phone-type-separator,
|
||||
.has-phone-carrier > .phone-carrier,
|
||||
.has-phone-carrier > .phone-carrier-separator {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
/* Text selection control */
|
||||
html {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.editable-select-mode [contenteditable="true"] {
|
||||
-moz-user-select: text;
|
||||
}
|
||||
|
||||
/* ellipsis direction hack before bug 883884 fixed */
|
||||
.ellipsis-dir-fix {
|
||||
display: block;
|
||||
width: -moz-fit-content;
|
||||
max-width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
3
gaia/apps/sms/views/shared/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../../../test/unit/.jshintrc"
|
||||
}
|
||||
423
gaia/apps/sms/views/shared/test/unit/activity_handler_test.js
Executable file
|
|
@ -0,0 +1,423 @@
|
|||
/*global ActivityClient,
|
||||
ActivityHandler,
|
||||
App,
|
||||
Attachment,
|
||||
Drafts,
|
||||
MessageManager,
|
||||
MocksHelper,
|
||||
Navigation,
|
||||
Settings,
|
||||
Utils
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/test/unit/mock_attachment.js');
|
||||
require('/services/test/unit/mock_message_manager.js');
|
||||
require('/services/test/unit/activity/mock_activity_shim.js');
|
||||
require('/services/test/unit/activity/mock_activity_client.js');
|
||||
require('/views/shared/test/unit/mock_app.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
require('/views/shared/test/unit/mock_navigation.js');
|
||||
require('/services/test/unit/mock_drafts.js');
|
||||
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
require('/views/shared/js/activity_handler.js');
|
||||
|
||||
var mocksHelperForActivityHandler = new MocksHelper([
|
||||
'ActivityClient',
|
||||
'ActivityShim',
|
||||
'App',
|
||||
'Attachment',
|
||||
'Draft',
|
||||
'Drafts',
|
||||
'MessageManager',
|
||||
'Navigation',
|
||||
'Settings',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
suite('ActivityHandler', function() {
|
||||
mocksHelperForActivityHandler.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
this.sinon.stub(Utils, 'onceDocumentIsVisible').returns(Promise.resolve());
|
||||
this.sinon.stub(Utils, 'alert').returns(Promise.resolve());
|
||||
|
||||
this.sinon.stub(ActivityClient);
|
||||
});
|
||||
|
||||
test('init()', function() {
|
||||
ActivityHandler.init();
|
||||
|
||||
sinon.assert.calledOnce(ActivityClient.init);
|
||||
sinon.assert.calledWith(ActivityClient.init, App.instanceId);
|
||||
|
||||
sinon.assert.calledWith(ActivityClient.on, 'new-activity-request');
|
||||
sinon.assert.calledWith(ActivityClient.on, 'share-activity-request');
|
||||
});
|
||||
|
||||
suite('"share" activity', function() {
|
||||
var activityData;
|
||||
|
||||
function onceShareActivityCompleted() {
|
||||
sinon.assert.called(ActivityHandler._onShareActivity);
|
||||
return ActivityHandler._onShareActivity.lastCall.returnValue;
|
||||
}
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(ActivityHandler, '_onShareActivity');
|
||||
this.sinon.spy(Navigation, 'toPanel');
|
||||
this.sinon.spy(Drafts, 'add');
|
||||
this.sinon.spy(Drafts, 'store');
|
||||
this.sinon.spy(Drafts, 'request');
|
||||
|
||||
activityData = {
|
||||
type: 'video/*',
|
||||
blobs: [
|
||||
new Blob(['test'], { type: 'video/x-video' }),
|
||||
new Blob(['test2'], { type: 'video/x-video' }),
|
||||
new Blob(),
|
||||
new Blob(),
|
||||
new Blob()
|
||||
],
|
||||
filenames: [
|
||||
'testBlob1', 'testBlob2', 'testBlob3', 'testBlob4', 'testBlob5'
|
||||
]
|
||||
};
|
||||
|
||||
ActivityHandler.init();
|
||||
});
|
||||
|
||||
test('moves to the composer panel after saving draft', function(done) {
|
||||
ActivityClient.on.withArgs('share-activity-request').yield(
|
||||
activityData
|
||||
);
|
||||
|
||||
onceShareActivityCompleted().then(() => {
|
||||
sinon.assert.calledWithMatch(Drafts.add, {
|
||||
recipients: null,
|
||||
type: 'mms',
|
||||
content: [
|
||||
sinon.match.instanceOf(Attachment),
|
||||
sinon.match.instanceOf(Attachment),
|
||||
sinon.match.instanceOf(Attachment),
|
||||
sinon.match.instanceOf(Attachment),
|
||||
sinon.match.instanceOf(Attachment)
|
||||
]
|
||||
});
|
||||
sinon.assert.called(Drafts.store);
|
||||
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: sinon.match.falsy }
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Attachment size over max mms should not be appended', function(done) {
|
||||
// Adjust mmsSizeLimitation for verifying alert popup when size over
|
||||
// limitation.
|
||||
Settings.mmsSizeLimitation = 1;
|
||||
|
||||
ActivityClient.on.withArgs('share-activity-request').yield(
|
||||
activityData
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(Utils.alert, {
|
||||
id: 'attached-files-too-large',
|
||||
args: { n: 5, mmsSize: '0' }
|
||||
});
|
||||
|
||||
Utils.alert.lastCall.returnValue.then(() => {
|
||||
sinon.assert.calledWithExactly(ActivityClient.postResult);
|
||||
sinon.assert.notCalled(Navigation.toPanel);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Should append images even when they are big', function(done) {
|
||||
activityData.blobs = [new Blob(['test'], { type: 'image/jpeg' })];
|
||||
|
||||
Settings.mmsSizeLimitation = 1;
|
||||
|
||||
ActivityClient.on.withArgs('share-activity-request').yield(
|
||||
activityData
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(Utils.alert);
|
||||
|
||||
onceShareActivityCompleted().then(() => {
|
||||
sinon.assert.calledWithMatch(Drafts.add, {
|
||||
recipients: null,
|
||||
type: 'mms',
|
||||
content: [sinon.match.instanceOf(Attachment)]
|
||||
});
|
||||
sinon.assert.called(Drafts.store);
|
||||
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: sinon.match.falsy }
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Should append vcard attachment', function(done) {
|
||||
activityData.blobs = [new Blob(['test'], { type: 'text/x-vcard' })];
|
||||
|
||||
ActivityClient.on.withArgs('share-activity-request').yield(
|
||||
activityData
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(Utils.alert);
|
||||
|
||||
onceShareActivityCompleted().then(() => {
|
||||
sinon.assert.calledWithMatch(Drafts.add, {
|
||||
recipients: null,
|
||||
type: 'mms',
|
||||
content: [sinon.match.instanceOf(Attachment)]
|
||||
});
|
||||
sinon.assert.called(Drafts.store);
|
||||
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: sinon.match.falsy }
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Appends URL to the Compose field for activity with URL data type',
|
||||
function(done) {
|
||||
var urlActivityData = {
|
||||
type: 'url',
|
||||
url: 'test_url'
|
||||
};
|
||||
|
||||
ActivityClient.on.withArgs('share-activity-request').yield(
|
||||
urlActivityData
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(Utils.alert);
|
||||
|
||||
onceShareActivityCompleted().then(() => {
|
||||
sinon.assert.calledWithMatch(Drafts.add, {
|
||||
recipients: null,
|
||||
type: 'sms',
|
||||
content: [urlActivityData.url]
|
||||
});
|
||||
sinon.assert.called(Drafts.store);
|
||||
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: sinon.match.falsy }
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Call activity postError if no data to share', function() {
|
||||
ActivityClient.on.withArgs('share-activity-request').yield({
|
||||
type: 'url'
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(Navigation.toPanel);
|
||||
sinon.assert.notCalled(ActivityClient.postResult);
|
||||
sinon.assert.calledOnce(ActivityClient.postError);
|
||||
sinon.assert.calledWith(
|
||||
ActivityClient.postError, 'No data to share found!'
|
||||
);
|
||||
});
|
||||
|
||||
test('Call activity postError on unknown activity data type', function() {
|
||||
ActivityClient.on.withArgs('share-activity-request').yield({
|
||||
type: 'multipart/mixed'
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(Navigation.toPanel);
|
||||
sinon.assert.notCalled(ActivityClient.postResult);
|
||||
sinon.assert.calledOnce(ActivityClient.postError);
|
||||
sinon.assert.calledWith(
|
||||
ActivityClient.postError,
|
||||
'Unsupported activity data type: multipart/mixed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('"new" activity', function() {
|
||||
var emailActivityData;
|
||||
|
||||
function onceNewActivityCompleted() {
|
||||
sinon.assert.called(ActivityHandler._onNewActivity);
|
||||
return ActivityHandler._onNewActivity.lastCall.returnValue;
|
||||
}
|
||||
|
||||
function assertDraftSaved(draft) {
|
||||
sinon.assert.calledWithMatch(Drafts.add, draft);
|
||||
sinon.assert.called(Drafts.store);
|
||||
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
|
||||
}
|
||||
|
||||
var threadDeferred;
|
||||
|
||||
setup(function() {
|
||||
emailActivityData = {
|
||||
target: 'abc@exmple.com',
|
||||
body: 'foo'
|
||||
};
|
||||
|
||||
// configure findThreadFromNumber
|
||||
threadDeferred = Utils.Promise.defer();
|
||||
this.sinon.stub(MessageManager, 'findThreadFromNumber').returns(
|
||||
threadDeferred.promise
|
||||
);
|
||||
this.sinon.spy(Navigation, 'toPanel');
|
||||
this.sinon.spy(Drafts, 'add');
|
||||
this.sinon.spy(Drafts, 'store');
|
||||
this.sinon.spy(Drafts, 'request');
|
||||
this.sinon.spy(ActivityHandler, '_onNewActivity');
|
||||
|
||||
ActivityHandler.init();
|
||||
});
|
||||
|
||||
test('Non-existing thread, with number and body', function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield({
|
||||
number: '123',
|
||||
body: 'foo'
|
||||
});
|
||||
|
||||
// This should not be used but keeping it here to have a consistent
|
||||
// environment for this test case.
|
||||
threadDeferred.reject(new Error('No thread for this test'));
|
||||
|
||||
onceNewActivityCompleted().then(function() {
|
||||
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
|
||||
|
||||
assertDraftSaved({
|
||||
recipients: ['123'],
|
||||
type: 'sms',
|
||||
content: ['foo']
|
||||
});
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: 'composer' }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
|
||||
test('existing thread with specified body, navigate to composer',
|
||||
function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield({
|
||||
number: '123',
|
||||
body: 'foo'
|
||||
});
|
||||
|
||||
// This should not be used but keeping it here to have a consistent
|
||||
// environment for this test case.
|
||||
threadDeferred.resolve(42);
|
||||
|
||||
onceNewActivityCompleted().then(function() {
|
||||
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
|
||||
|
||||
assertDraftSaved({
|
||||
recipients: ['123'],
|
||||
type: 'sms',
|
||||
content: ['foo']
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(
|
||||
Navigation.toPanel,
|
||||
'composer', { draftId: 'draftId' }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
|
||||
test('new message with body only', function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield(
|
||||
{ body: 'foo' }
|
||||
);
|
||||
|
||||
onceNewActivityCompleted().then(() => {
|
||||
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
|
||||
|
||||
assertDraftSaved({
|
||||
recipients: null,
|
||||
type: 'sms',
|
||||
content: ['foo']
|
||||
});
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: sinon.match.falsy }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
|
||||
test('Non-existing thread with recipient only, navigate to composer',
|
||||
function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield({
|
||||
number: '123'
|
||||
});
|
||||
|
||||
threadDeferred.reject(new Error('No thread for this test'));
|
||||
|
||||
onceNewActivityCompleted().then(function() {
|
||||
sinon.assert.calledWith(MessageManager.findThreadFromNumber, '123');
|
||||
assertDraftSaved({
|
||||
recipients: ['123'],
|
||||
type: 'sms',
|
||||
content: null
|
||||
});
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: 'composer' }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
|
||||
test('new message with email and body', function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield(
|
||||
emailActivityData
|
||||
);
|
||||
|
||||
// This is not used but it's kept here for consistency.
|
||||
threadDeferred.reject(new Error('No thread for this test'));
|
||||
|
||||
onceNewActivityCompleted().then(function() {
|
||||
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
|
||||
|
||||
assertDraftSaved({
|
||||
recipients: [emailActivityData.target],
|
||||
type: 'mms',
|
||||
content: [emailActivityData.body]
|
||||
});
|
||||
sinon.assert.calledWith(
|
||||
Navigation.toPanel,
|
||||
'composer',
|
||||
{ draftId: 'draftId', focus: 'composer' }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
|
||||
test('existing thread with recipient only, navigate to the thread',
|
||||
function(done) {
|
||||
ActivityClient.on.withArgs('new-activity-request').yield({
|
||||
number: '123'
|
||||
});
|
||||
|
||||
// this time we can find a thread with id=42
|
||||
threadDeferred.resolve(42);
|
||||
|
||||
onceNewActivityCompleted().then(function() {
|
||||
sinon.assert.calledWithMatch(
|
||||
Navigation.toPanel, 'thread', { id: 42, focus: 'composer' }
|
||||
);
|
||||
}).then(done,done);
|
||||
});
|
||||
});
|
||||
});
|
||||
255
gaia/apps/sms/views/shared/test/unit/activity_picker_test.js
Executable file
|
|
@ -0,0 +1,255 @@
|
|||
/*global MocksHelper, MockL10n, ActivityPicker, MozActivity */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
require('/views/shared/js/activity_picker.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_moz_activity.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
|
||||
var mocksHelperAP = new MocksHelper([
|
||||
'MozActivity',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
suite('ActivityPicker', function() {
|
||||
var realMozL10n, onsuccess, onerror;
|
||||
|
||||
suiteSetup(function() {
|
||||
realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
mocksHelperAP.suiteSetup();
|
||||
|
||||
onsuccess = function() {};
|
||||
onerror = function() {};
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = realMozL10n;
|
||||
mocksHelperAP.suiteTeardown();
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
mocksHelperAP.setup();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mocksHelperAP.teardown();
|
||||
});
|
||||
|
||||
suite('dial', function() {
|
||||
|
||||
test('dial(number) ', function() {
|
||||
ActivityPicker.dial('999');
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'dial',
|
||||
data: { type: 'webtelephony/number', number: '999' }
|
||||
});
|
||||
});
|
||||
|
||||
test('dial(number, success) ', function() {
|
||||
ActivityPicker.dial('999', onsuccess);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'dial',
|
||||
data: { type: 'webtelephony/number', number: '999' }
|
||||
});
|
||||
});
|
||||
|
||||
test('dial(number, success, error) ', function() {
|
||||
ActivityPicker.dial('999', onsuccess, onerror);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onerror, onerror
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'dial',
|
||||
data: { type: 'webtelephony/number', number: '999' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('email', function() {
|
||||
|
||||
test('email(email) ', function() {
|
||||
ActivityPicker.email('a@b.com');
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'mail',
|
||||
URI: 'mailto:a@b.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('email(email, success) ', function() {
|
||||
ActivityPicker.email('a@b.com', onsuccess);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'mail',
|
||||
URI: 'mailto:a@b.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('email(email, success, error) ', function() {
|
||||
ActivityPicker.email('a@b.com', onsuccess, onerror);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onerror, onerror
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'mail',
|
||||
URI: 'mailto:a@b.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('url', function() {
|
||||
|
||||
test('url(url) ', function() {
|
||||
ActivityPicker.url('http://mozilla.com');
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'view',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: 'http://mozilla.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('url(url, success) ', function() {
|
||||
ActivityPicker.url('http://mozilla.com', onsuccess);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'view',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: 'http://mozilla.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('url(url, success, error) ', function() {
|
||||
ActivityPicker.url('http://mozilla.com', onsuccess, onerror);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onsuccess, onsuccess
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
MozActivity.instances[0].onerror, onerror
|
||||
);
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'view',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: 'http://mozilla.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('createNewContact', function() {
|
||||
ActivityPicker.createNewContact({foo: ['bar']});
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: { foo: ['bar'] }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('addToExistingContact', function() {
|
||||
ActivityPicker.addToExistingContact({foo: ['bar']});
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'update',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: { foo: ['bar'] }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('viewContact', function() {
|
||||
ActivityPicker.viewContact({foo: ['bar']});
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], {
|
||||
name: 'open',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: { foo: ['bar'] }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('openSettings', function() {
|
||||
var params = {
|
||||
name: 'configure',
|
||||
data: {
|
||||
target: 'device',
|
||||
section: 'messaging'
|
||||
}
|
||||
};
|
||||
|
||||
test('openSettings() ', function() {
|
||||
ActivityPicker.openSettings();
|
||||
|
||||
assert.deepEqual(MozActivity.calls[0], params);
|
||||
});
|
||||
|
||||
test('openSettings(success) ', function() {
|
||||
ActivityPicker.openSettings(onsuccess);
|
||||
|
||||
assert.equal(MozActivity.instances[0].onsuccess, onsuccess);
|
||||
assert.deepEqual(MozActivity.calls[0], params);
|
||||
});
|
||||
|
||||
test('openSettings(success, error) ', function() {
|
||||
ActivityPicker.openSettings(onsuccess, onerror);
|
||||
|
||||
assert.equal(MozActivity.instances[0].onsuccess, onsuccess);
|
||||
assert.equal(MozActivity.instances[0].onerror, onerror);
|
||||
assert.deepEqual(MozActivity.calls[0], params);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
89
gaia/apps/sms/views/shared/test/unit/app_test.js
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
/*global App, Promise */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/js/app.js');
|
||||
|
||||
suite('App', function() {
|
||||
// Taken from app.js
|
||||
const APPLICATION_READY_CLASS_NAME = 'js-app-ready';
|
||||
|
||||
teardown(function() {
|
||||
document.body.classList.remove(APPLICATION_READY_CLASS_NAME);
|
||||
});
|
||||
|
||||
test('isReady is false by default', function() {
|
||||
assert.equal(App.isReady(), false);
|
||||
});
|
||||
|
||||
test('setReady sets body class and isReady', function() {
|
||||
App.setReady();
|
||||
|
||||
assert.equal(App.isReady(), true);
|
||||
assert.ok(document.body.classList.contains(APPLICATION_READY_CLASS_NAME));
|
||||
});
|
||||
|
||||
test('setReady throws exception if called more than once', function() {
|
||||
App.setReady();
|
||||
|
||||
assert.throws(function() {
|
||||
App.setReady();
|
||||
});
|
||||
});
|
||||
|
||||
test('whenReady is resolved immediately if app is ready', function(done) {
|
||||
App.setReady();
|
||||
App.whenReady().then(function() {
|
||||
assert.ok(App.isReady());
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('whenReady is resolved when setReady is called', function(done) {
|
||||
var stub = sinon.stub();
|
||||
this.sinon.spy(App, 'setReady');
|
||||
|
||||
var whenReadyPromise = App.whenReady();
|
||||
whenReadyPromise.then(stub).then(function() {
|
||||
assert.ok(App.isReady());
|
||||
sinon.assert.callOrder(App.setReady, stub);
|
||||
}).then(done, done);
|
||||
|
||||
Promise.resolve().then(function() {
|
||||
App.setReady();
|
||||
});
|
||||
});
|
||||
|
||||
test('whenReady is rejected in case of error', function(done) {
|
||||
var error = new Error('Test error');
|
||||
|
||||
this.sinon.stub(MutationObserver.prototype, 'observe', function() {
|
||||
throw error;
|
||||
});
|
||||
|
||||
App.whenReady().then(function() {
|
||||
throw new Error('Success callback should not have been called.');
|
||||
}, function(e) {
|
||||
assert.equal(e, error);
|
||||
}).then(done, done);
|
||||
|
||||
App.setReady();
|
||||
});
|
||||
|
||||
test('whenReady is rejected in case of error in MutationObserver callback',
|
||||
function(done) {
|
||||
var error = new Error('Test error');
|
||||
|
||||
// This is called inside MutationObserver callback
|
||||
this.sinon.stub(MutationObserver.prototype, 'disconnect', function() {
|
||||
throw error;
|
||||
});
|
||||
|
||||
App.whenReady().then(function() {
|
||||
throw new Error('Success callback should not have been called.');
|
||||
}, function(e) {
|
||||
assert.equal(e, error);
|
||||
}).then(done, done);
|
||||
|
||||
App.setReady();
|
||||
});
|
||||
});
|
||||
61
gaia/apps/sms/views/shared/test/unit/assets_helper.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
/*global Promise */
|
||||
|
||||
/*exported AssetsHelper */
|
||||
|
||||
'use strict';
|
||||
|
||||
var AssetsHelper = {
|
||||
generateCanvas: function(width, height) {
|
||||
var canvas = document.createElement('canvas'),
|
||||
context = canvas.getContext('2d');
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
var linearGradient = context.createLinearGradient(0, 0, width, height);
|
||||
linearGradient.addColorStop(0, 'blue');
|
||||
linearGradient.addColorStop(1, 'red');
|
||||
|
||||
context.fillStyle = linearGradient;
|
||||
context.fillRect (0, 0, width, height);
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
||||
generateImageDataURL: function(width, height, type, quality) {
|
||||
var canvas = null;
|
||||
try {
|
||||
canvas = this.generateCanvas(width, height);
|
||||
return canvas.toDataURL(type, quality);
|
||||
} finally {
|
||||
canvas.width = canvas.height = 0;
|
||||
canvas = null;
|
||||
}
|
||||
},
|
||||
|
||||
generateImageBlob: function(width, height, type, quality) {
|
||||
var canvas = this.generateCanvas(width, height);
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
canvas.toBlob(function(blob) {
|
||||
canvas.width = canvas.height = 0;
|
||||
canvas = null;
|
||||
|
||||
resolve(blob);
|
||||
}, type, quality);
|
||||
});
|
||||
},
|
||||
|
||||
loadFileBlob: function(filePath) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', filePath, true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onload = function() {
|
||||
resolve(xhr.response);
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
};
|
||||
891
gaia/apps/sms/views/shared/test/unit/contact_renderer_test.js
Executable file
|
|
@ -0,0 +1,891 @@
|
|||
|
||||
/*global ContactRenderer, loadBodyHTML, MockContact, MockL10n, MocksHelper,
|
||||
Utils, Template, MockContactPhotoHelper, SharedComponents,
|
||||
MockSettings,
|
||||
AssetsHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/js/shared_components.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_contact.js');
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
require('/shared/test/unit/mocks/mock_contact_photo_helper.js');
|
||||
require('/views/shared/js/contact_renderer.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
|
||||
var mocksHelperForContactRenderer = new MocksHelper([
|
||||
'Utils',
|
||||
'ContactPhotoHelper',
|
||||
'Settings'
|
||||
]).init();
|
||||
|
||||
suite('ContactRenderer', function() {
|
||||
var realMozL10n;
|
||||
|
||||
var ul, contact;
|
||||
var testImageBlob;
|
||||
var renderer, unknownRender;
|
||||
|
||||
mocksHelperForContactRenderer.attachTestHelpers();
|
||||
|
||||
suiteSetup(function(done) {
|
||||
realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
|
||||
AssetsHelper.generateImageBlob(400, 400, 'image/jpeg', 0.5).then(
|
||||
(blob) => {
|
||||
testImageBlob = blob;
|
||||
}
|
||||
).then(done, done);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = realMozL10n;
|
||||
realMozL10n = null;
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
|
||||
this.sinon.spy(Template.prototype, 'interpolate');
|
||||
ul = document.createElement('ul');
|
||||
contact = MockContact();
|
||||
});
|
||||
|
||||
suite('suggestion', function() {
|
||||
setup(function() {
|
||||
renderer = ContactRenderer.flavor('suggestion');
|
||||
});
|
||||
|
||||
test('Rendered Contact "givenName familyName"', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
|
||||
carrier: 'TEF',
|
||||
name: 'Pepito O\'Hare',
|
||||
nameHTML: 'Pepito O'Hare',
|
||||
number: '+346578888888',
|
||||
phoneDetailsHTML: SharedComponents.phoneDetails(
|
||||
{
|
||||
number: '+346578888888',
|
||||
type: 'Mobile',
|
||||
carrier: 'TEF'
|
||||
}
|
||||
).toString(),
|
||||
photoHTML: '',
|
||||
type: 'Mobile'
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
assert.include(html, 'Pepito O\'Hare');
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "givenName familyName"', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'Pepito O\'Hare',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.include(html, '<span class="highlight">Pepito</span>');
|
||||
assert.include(html, '<span class="highlight">O\'Hare</span>');
|
||||
});
|
||||
|
||||
test('Rendered Contact "number"', function() {
|
||||
var html;
|
||||
|
||||
contact.tel[0].carrier = null;
|
||||
contact.tel[0].type = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.ok(html.includes('+346578888888'));
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "number"', function() {
|
||||
var html;
|
||||
|
||||
contact.tel[0].carrier = null;
|
||||
contact.tel[0].type = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '346578888888',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
|
||||
carrier: '',
|
||||
name: 'Pepito O\'Hare',
|
||||
nameHTML: 'Pepito O'Hare',
|
||||
number: '+346578888888',
|
||||
phoneDetailsHTML: SharedComponents.phoneDetails(
|
||||
{ number: '+<span class="highlight">346578888888</span>' },
|
||||
{ safe: ['number'] }
|
||||
).toString(),
|
||||
photoHTML: '',
|
||||
type: ''
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.ok(
|
||||
html.includes('+<span class="highlight">346578888888</span>')
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "name number"', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'Pepito 346578888888',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.include(html, '<span class="highlight">Pepito</span>');
|
||||
assert.include(html, '+<span class="highlight">346578888888</span>');
|
||||
});
|
||||
|
||||
test('Rendered Contact with type and number', function() {
|
||||
this.sinon.spy(SharedComponents, 'phoneDetails');
|
||||
|
||||
contact.tel[0].carrier = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(
|
||||
SharedComponents.phoneDetails,
|
||||
{
|
||||
number: contact.tel[0].value,
|
||||
type: contact.tel[0].type[0],
|
||||
carrier: ''
|
||||
},
|
||||
{ safe: ['number'] }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact with type and highlighted number', function() {
|
||||
this.sinon.spy(SharedComponents, 'phoneDetails');
|
||||
|
||||
contact.tel[0].carrier = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '346578888888',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(
|
||||
SharedComponents.phoneDetails,
|
||||
{
|
||||
number: '+<span class="highlight">346578888888</span>',
|
||||
type: contact.tel[0].type[0],
|
||||
carrier: ''
|
||||
},
|
||||
{ safe: ['number'] }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact with type, number and carrier', function() {
|
||||
this.sinon.spy(SharedComponents, 'phoneDetails');
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(
|
||||
SharedComponents.phoneDetails,
|
||||
{
|
||||
number: contact.tel[0].value,
|
||||
type: contact.tel[0].type[0],
|
||||
carrier: contact.tel[0].carrier
|
||||
},
|
||||
{ safe: ['number'] }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact with type, highlighted number and carrier',
|
||||
function() {
|
||||
this.sinon.spy(SharedComponents, 'phoneDetails');
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '346578888888',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(
|
||||
SharedComponents.phoneDetails,
|
||||
{
|
||||
number: '+<span class="highlight">346578888888</span>',
|
||||
type: contact.tel[0].type[0],
|
||||
carrier: contact.tel[0].carrier
|
||||
},
|
||||
{ safe: ['number'] }
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact "email"', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var html;
|
||||
|
||||
contact.email[0].type = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.lastElementChild.innerHTML;
|
||||
|
||||
assert.ok(html.includes('a@b.com'));
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "email"', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var html;
|
||||
|
||||
contact.email[0].type = null;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'a@b.com',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
|
||||
carrier: '',
|
||||
name: 'Pepito O\'Hare',
|
||||
nameHTML: 'Pepito O'Hare',
|
||||
number: 'a@b.com',
|
||||
numberHTML: '<span class="highlight">a@b.com</span>',
|
||||
photoHTML: '',
|
||||
type: ''
|
||||
});
|
||||
|
||||
html = ul.lastElementChild.innerHTML;
|
||||
|
||||
assert.ok(
|
||||
html.includes('<span class="highlight">a@b.com</span>')
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "name email"', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'Pepito a@b.com',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.lastElementChild.innerHTML;
|
||||
|
||||
assert.include(html, '<span class="highlight">Pepito</span>');
|
||||
assert.include(html, '<span class="highlight">a@b.com</span>');
|
||||
});
|
||||
|
||||
test('Rendered Contact "type | email"', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.lastElementChild.innerHTML;
|
||||
|
||||
assert.isFalse(html.includes(
|
||||
'<span data-l10n-id="Personal">Personal</span> | ' + 'a@b.com'));
|
||||
});
|
||||
|
||||
test('Rendered Contact highlighted "type | email"', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'a@b.com',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.lastElementChild.innerHTML;
|
||||
|
||||
assert.isFalse(html.includes(
|
||||
'<span data-l10n-id="Personal">Personal</span> | ' +
|
||||
'<span class="highlight">a@b.com</span>'
|
||||
));
|
||||
});
|
||||
|
||||
test('Rendered Contact w/ multiple: all (isSuggestion) ' +
|
||||
'and No Support EmailRecipient ',
|
||||
function() {
|
||||
MockSettings.supportEmailRecipient = false;
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '+12125559999',
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.equal(ul.children.length, 2);
|
||||
});
|
||||
|
||||
test('Rendered Contact w/ multiple: all (isSuggestion) ' +
|
||||
'and Support EmailRecipient ',
|
||||
function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '+12125559999',
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.equal(ul.children.length, 3);
|
||||
});
|
||||
|
||||
test('Rendered Contact omit numbers already in recipient list ' +
|
||||
'and No Support EmailRecipient ',
|
||||
function() {
|
||||
MockSettings.supportEmailRecipient = false;
|
||||
var html;
|
||||
|
||||
var skip = ['+346578888888'];
|
||||
|
||||
// This contact has two tel entries.
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '+346578888888',
|
||||
target: ul,
|
||||
skip: skip
|
||||
});
|
||||
|
||||
html = ul.innerHTML;
|
||||
|
||||
assert.ok(!html.includes('346578888888'));
|
||||
assert.equal(ul.children.length, 1);
|
||||
});
|
||||
|
||||
test('Rendered Contact omit numbers already in recipient list ' +
|
||||
'and Support EmailRecipient ',
|
||||
function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var html;
|
||||
|
||||
var skip = ['+346578888888'];
|
||||
|
||||
// This contact has two tel entries.
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '+346578888888',
|
||||
target: ul,
|
||||
skip: skip
|
||||
});
|
||||
|
||||
html = ul.innerHTML;
|
||||
|
||||
assert.ok(!html.includes('346578888888'));
|
||||
assert.equal(ul.children.length, 2);
|
||||
});
|
||||
|
||||
test('Rendered Contact omit emails already in recipient list ' +
|
||||
'and Support EmailRecipient ',
|
||||
function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var html;
|
||||
|
||||
var skip = ['a@b.com'];
|
||||
|
||||
// This contact has three tel entries.
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'a@b.com',
|
||||
target: ul,
|
||||
skip: skip
|
||||
});
|
||||
|
||||
html = ul.innerHTML;
|
||||
|
||||
assert.ok(!html.includes('a@b.com'));
|
||||
assert.equal(ul.children.length, 2);
|
||||
});
|
||||
|
||||
test('does not include photo', function() {
|
||||
var html;
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.isFalse(html.includes('span[data-type=img]'));
|
||||
});
|
||||
|
||||
test('append information block in the li', function() {
|
||||
var li;
|
||||
var block = document.createElement('div');
|
||||
var selector = '.suggestion';
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: 'Additional info',
|
||||
infoBlock: block,
|
||||
infoBlockParentSelector: selector,
|
||||
target: ul
|
||||
});
|
||||
li = ul.lastElementChild;
|
||||
|
||||
assert.isTrue(!!li.querySelector(selector));
|
||||
assert.equal(li.querySelector(selector).lastElementChild, block);
|
||||
});
|
||||
|
||||
test('Rendered no "Tel" and No Support EmailRecipient ', function() {
|
||||
|
||||
contact.tel = null;
|
||||
MockSettings.supportEmailRecipient = false;
|
||||
var result = renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
assert.isFalse(result);
|
||||
});
|
||||
|
||||
test('Rendered no "email" and No Support EmailRecipient ', function() {
|
||||
|
||||
contact.email = null;
|
||||
MockSettings.supportEmailRecipient = false;
|
||||
var result = renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('Rendered no "Tel" and Support EmailRecipient ', function() {
|
||||
|
||||
contact.tel = null;
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('Rendered no "email" and Support EmailRecipient ', function() {
|
||||
|
||||
contact.email = null;
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('Rendered no "Tel"/"email" and Support EmailRecipient ', function() {
|
||||
|
||||
contact.tel = null;
|
||||
contact.email = null;
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = renderer.render({
|
||||
contact: contact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
assert.isFalse(result);
|
||||
});
|
||||
});
|
||||
|
||||
suite('suggestionUnknown', function() {
|
||||
var html;
|
||||
var unknownContact = {
|
||||
name: ['unknown'],
|
||||
tel: [{value: '+346578888888'}]
|
||||
};
|
||||
setup(function() {
|
||||
unknownRender = ContactRenderer.flavor('suggestionUnknown');
|
||||
});
|
||||
|
||||
test('Rendered unknownContact "number"', function() {
|
||||
|
||||
unknownContact.tel[0].carrier = null;
|
||||
unknownContact.tel[0].type = null;
|
||||
|
||||
unknownRender.render({
|
||||
contact: unknownContact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.ok(html.includes('+346578888888'));
|
||||
});
|
||||
|
||||
test('Rendered unknownContact highlighted "number"', function() {
|
||||
|
||||
unknownContact.tel[0].carrier = null;
|
||||
unknownContact.tel[0].type = null;
|
||||
|
||||
unknownRender.render({
|
||||
contact: unknownContact,
|
||||
input: '346578888888',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
|
||||
carrier: '',
|
||||
name: 'unknown',
|
||||
nameHTML: 'unknown',
|
||||
number: '+346578888888',
|
||||
phoneDetailsHTML: SharedComponents.phoneDetails(
|
||||
{ number: '+<span class="highlight">346578888888</span>' },
|
||||
{ safe: ['number'] }
|
||||
).toString(),
|
||||
photoHTML: '',
|
||||
type: ''
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.ok(
|
||||
html.includes('+<span class="highlight">346578888888</span>')
|
||||
);
|
||||
});
|
||||
|
||||
test('Rendered unknownContact omit numbers already in recipient list',
|
||||
function() {
|
||||
|
||||
var skip = ['+346578888888'];
|
||||
|
||||
// This unknownContact has two tel entries.
|
||||
unknownRender.render({
|
||||
contact: unknownContact,
|
||||
input: '+346578888888',
|
||||
target: ul,
|
||||
skip: skip
|
||||
});
|
||||
|
||||
html = ul.innerHTML;
|
||||
|
||||
assert.ok(!html.includes('346578888888'));
|
||||
assert.equal(ul.children.length, 0);
|
||||
});
|
||||
|
||||
test('does not include photo', function() {
|
||||
|
||||
unknownRender.render({
|
||||
contact: unknownContact,
|
||||
input: 'foo',
|
||||
target: ul
|
||||
});
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.isFalse(html.includes('span[data-type=img]'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
suite('prompt', function() {
|
||||
setup(function() {
|
||||
renderer = ContactRenderer.flavor('prompt');
|
||||
});
|
||||
|
||||
test('Rendered Contact w/ multiple: one', function() {
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '+12125559999',
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.equal(ul.children.length, 1);
|
||||
});
|
||||
|
||||
test('Rendered Contact w/ multiple: one w/ minimal match', function() {
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '5559999',
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.equal(ul.children.length, 1);
|
||||
});
|
||||
|
||||
test('Render contact without photo does not render the photo', function() {
|
||||
var html;
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '5559999',
|
||||
target: ul
|
||||
});
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
|
||||
assert.isFalse(html.includes('span[data-type=img]'));
|
||||
});
|
||||
|
||||
test('Render contact with photo renders the image', function() {
|
||||
var html;
|
||||
var blob = testImageBlob;
|
||||
this.sinon.stub(MockContactPhotoHelper, 'getThumbnail').returns(blob);
|
||||
this.sinon.spy(Utils, 'asyncLoadRevokeURL');
|
||||
this.sinon.spy(Utils, 'getContactDetails');
|
||||
this.sinon.spy(window, 'encodeURI');
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
input: '5559999',
|
||||
target: ul
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(Template.prototype.interpolate, undefined);
|
||||
|
||||
var photo = 'data-type="img"';
|
||||
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
|
||||
carrier: 'XXX',
|
||||
name: 'Pepito O\'Hare',
|
||||
nameHTML: 'Pepito O'Hare',
|
||||
number: '+12125559999',
|
||||
phoneDetailsHTML: SharedComponents.phoneDetails(
|
||||
{
|
||||
number: '+12125559999',
|
||||
type: 'B',
|
||||
carrier: 'XXX'
|
||||
},
|
||||
{ safe: ['number'] }
|
||||
).toString(),
|
||||
photoHTML: sinon.match(photo),
|
||||
type: 'B'
|
||||
});
|
||||
|
||||
html = ul.firstElementChild.innerHTML;
|
||||
var contactPhotoElement = ul.firstElementChild.querySelector(
|
||||
'.contact-photo'
|
||||
);
|
||||
|
||||
assert.ok(html.includes('span'));
|
||||
assert.ok(contactPhotoElement.style.backgroundImage.indexOf('blob:') > 0);
|
||||
sinon.assert.calledWith(
|
||||
encodeURI,
|
||||
Utils.getContactDetails.returnValues[0].photoURL
|
||||
);
|
||||
sinon.assert.calledWith(
|
||||
Utils.asyncLoadRevokeURL,
|
||||
Utils.getContactDetails.returnValues[0].photoURL
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Secure User Input', function() {
|
||||
setup(function() {
|
||||
this.sinon.stub(Utils, 'getContactDetails', function(number, contacts) {
|
||||
return {
|
||||
isContact: !!contacts,
|
||||
title: number
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
test('+99', function() {
|
||||
var ul = document.createElement('ul');
|
||||
|
||||
assert.doesNotThrow(function() {
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
|
||||
renderer.render({
|
||||
contact: {
|
||||
name: 'Spider Monkey',
|
||||
tel: [{ value: '...' }]
|
||||
},
|
||||
input: '+99',
|
||||
target: ul
|
||||
});
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(Utils.getContactDetails, '...');
|
||||
});
|
||||
|
||||
test('*67 [800]-555-1212', function() {
|
||||
var ul = document.createElement('ul');
|
||||
|
||||
assert.doesNotThrow(function() {
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
renderer.render({
|
||||
contact: {
|
||||
name: 'Spider Monkey',
|
||||
tel: [{ value: '...' }]
|
||||
},
|
||||
input: '*67 [800]-555-1212',
|
||||
target: ul
|
||||
});
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(Utils.getContactDetails, '...');
|
||||
});
|
||||
|
||||
test('\\^$*+?.', function() {
|
||||
var ul = document.createElement('ul');
|
||||
assert.doesNotThrow(function() {
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
renderer.render({
|
||||
contact: {
|
||||
name: 'Spider Monkey',
|
||||
tel: [{ value: '...' }]
|
||||
},
|
||||
input: '\\^$*+?.',
|
||||
target: ul
|
||||
});
|
||||
});
|
||||
sinon.assert.calledWith(Utils.getContactDetails, '...');
|
||||
});
|
||||
});
|
||||
|
||||
suite('Defensive Contact Rendering', function() {
|
||||
test('has tel number', function() {
|
||||
var ul = document.createElement('ul');
|
||||
var contact = new MockContact();
|
||||
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
var isRendered = renderer.render({
|
||||
contact: contact,
|
||||
input: contact.tel[0].value,
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.isTrue(isRendered);
|
||||
});
|
||||
|
||||
test('no tel number, has email address', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var ul = document.createElement('ul');
|
||||
var contact = new MockContact();
|
||||
contact.tel = null;
|
||||
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
var isRendered = renderer.render({
|
||||
contact: contact,
|
||||
input: contact.email[0].value,
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.isTrue(isRendered);
|
||||
});
|
||||
|
||||
test('has tel number, no email address', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var ul = document.createElement('ul');
|
||||
var contact = new MockContact();
|
||||
contact.email = null;
|
||||
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
var isRendered = renderer.render({
|
||||
contact: contact,
|
||||
input: contact.tel[0].value,
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.isTrue(isRendered);
|
||||
});
|
||||
|
||||
test('no tel number, no email address', function() {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var ul = document.createElement('ul');
|
||||
var contact = new MockContact();
|
||||
contact.tel = null;
|
||||
contact.email = null;
|
||||
|
||||
var renderer = ContactRenderer.flavor('suggestion');
|
||||
var isNotRendered = renderer.render({
|
||||
contact: contact,
|
||||
input: null,
|
||||
target: ul
|
||||
});
|
||||
|
||||
assert.isFalse(isNotRendered);
|
||||
});
|
||||
});
|
||||
|
||||
suite('report-view', function() {
|
||||
var renderer;
|
||||
|
||||
setup(function() {
|
||||
renderer = ContactRenderer.flavor('report-view');
|
||||
});
|
||||
|
||||
test('correct rendering', function() {
|
||||
var selector = '.js-contact-info';
|
||||
var infoBlock = document.createElement('div');
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
target: ul,
|
||||
input: contact.tel[0].value,
|
||||
infoBlock: infoBlock,
|
||||
infoBlockParentSelector: selector,
|
||||
});
|
||||
|
||||
var contactInfo = ul.querySelector(selector);
|
||||
assert.isTrue(contactInfo.contains(infoBlock));
|
||||
assert.notEqual(contactInfo.tagName, 'A');
|
||||
});
|
||||
});
|
||||
|
||||
suite('group-view', function() {
|
||||
var renderer;
|
||||
|
||||
setup(function() {
|
||||
renderer = ContactRenderer.flavor('group-view');
|
||||
});
|
||||
|
||||
test('correct rendering', function() {
|
||||
var selector = '.js-contact-info';
|
||||
|
||||
renderer.render({
|
||||
contact: contact,
|
||||
target: ul,
|
||||
input: contact.tel[0].value
|
||||
});
|
||||
|
||||
var contactInfo = ul.querySelector(selector);
|
||||
assert.equal(contactInfo.tagName, 'A');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
890
gaia/apps/sms/views/shared/test/unit/contacts_test.js
Executable file
|
|
@ -0,0 +1,890 @@
|
|||
/*global MockContact, Contacts, fb, MockFbReaderUtilsObj,
|
||||
MockSettings, MocksHelper */
|
||||
'use strict';
|
||||
|
||||
require('/shared/test/unit/mocks/mock_moz_phone_number_service.js');
|
||||
require('/shared/test/unit/mocks/mock_fb_reader_utils.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/test/unit/mock_contact.js');
|
||||
require('/views/shared/js/contacts.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
var mocksHelperForContactsUnitTest = new MocksHelper([
|
||||
'Settings',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
function withFilter(str) {
|
||||
return sinon.match( { filterValue: str } );
|
||||
}
|
||||
|
||||
suite('Contacts', function(done) {
|
||||
mocksHelperForContactsUnitTest.attachTestHelpers();
|
||||
var realFb = window.fb;
|
||||
|
||||
var targetFbNumber = '+34658789147';
|
||||
var fbContactName = 'Carlos Facebook';
|
||||
var targetLocalNumber = '+34698745123';
|
||||
var targetLocalEmail = 'a@b.com';
|
||||
var localContactName = 'Jose Local';
|
||||
var notFoundNumber = '+34633789102';
|
||||
var notFoundEmail = 'a@c.com';
|
||||
|
||||
suiteSetup(function() {
|
||||
var mockReaderUtils = new MockFbReaderUtilsObj();
|
||||
mockReaderUtils.targetFbNumber = targetFbNumber;
|
||||
mockReaderUtils.fbContactName = fbContactName;
|
||||
window.fb = mockReaderUtils;
|
||||
});
|
||||
|
||||
setup(function(){
|
||||
// Do not use the Native API
|
||||
this.sinon.stub(navigator.mozContacts, 'find');
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
window.fb = realFb;
|
||||
});
|
||||
|
||||
suite('Contacts.findContactByString, single-term', function() {
|
||||
|
||||
test('(string[tel,givenName,familyName], ...) Match', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('O\'Hare')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('O\'Hare').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(string[tel,givenName,familyName], ...) No Match', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('wontmatch')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('wontmatch').then(function(contacts) {
|
||||
// contacts were not found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(string[tel,email,givenName,familyName], ...) Match', function(done) {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('a@b.com')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('a@b.com').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(string[tel,email,givenName,familyName], ...) No Match',
|
||||
function(done) {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('z@y.com')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('z@y.com').then(function(contacts) {
|
||||
// contacts were not found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(string[tel], ...) Match', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('+346578888888')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('+346578888888').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(string[tel], ...) No Match', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('911')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('911').then(function(contacts) {
|
||||
// contacts were not found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findContactByString, multi-term', function() {
|
||||
|
||||
test('no predominate', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('Pepito O\'Hare').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('no predominate, reversed', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('O\'Hare')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('O\'Hare Pepito').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('predominate first, upper', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepi')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('Pepi O').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('predominate last, upper', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepi')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('O Pepi').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
|
||||
test('predominate first, lower', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('pepi')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('pepi o').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('predominate last, lower', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('pepi')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('o pepi').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('multi-word name', function(done) {
|
||||
var result = MockContact.list([
|
||||
{ givenName: ['Mary Anne'], familyName: ['Jones'] }
|
||||
]);
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('mary')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('mary anne').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('name first, part of tel number last', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('Pepito 8888').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('part of tel number first, name last', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('8888 Pepito').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('name first, part of email address last', function(done) {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByString('Pepito a@b').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('part of email address first, name last', function(done) {
|
||||
MockSettings.supportEmailRecipient = true;
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByString('a@b Pepito').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('string search yields a contact without familyName', function(done) {
|
||||
var result = MockContact.list([
|
||||
{ givenName: ['julien'] }
|
||||
]);
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('julien')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('julien 123').then((contacts) => {
|
||||
// "julien" yields a result that has only a givenName, no familyName.
|
||||
// This test checks that our algorithm works also in such cases.
|
||||
// See Bug 952533
|
||||
assert.isNotNull(contacts);
|
||||
assert.lengthOf(contacts, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('no matches, predominate first, upper', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('Pepito S').then(function(contacts) {
|
||||
// No contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('no matches, predominate last, upper', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('Pepito')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('S Pepito').then(function(contacts) {
|
||||
// No contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('no matches, predominate first, lower', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('pepi')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('pepi s').then(function(contacts) {
|
||||
// No contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('no matches, predominate last, lower', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('pepi')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findContactByString('s pepi').then(function(contacts) {
|
||||
// No contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
suite('Contacts.findExact', function() {
|
||||
|
||||
test('yields a match ', function(done) {
|
||||
var result = MockContact.list([
|
||||
{ givenName: ['Jane'], familyName: ['Doozer'] }
|
||||
]);
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('doozer')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findExact('jane doozer').then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('yields no matches ', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('doozer')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findExact('j doozer').then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findByPhoneNumber', function() {
|
||||
|
||||
test('removes spaces', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('+33123456789')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber('+33 1 23 45 67 89').then(function(contacts){
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('The mozContacts find() call returned an error', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('callonerror')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber('callonerror').then((results) => {
|
||||
assert.isArray(results);
|
||||
assert.lengthOf(results, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number found.', function(done) {
|
||||
var result = [{
|
||||
name: [localContactName]
|
||||
}];
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetLocalNumber)
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber(targetLocalNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.isTrue(!contacts[0].isFbContact);
|
||||
assert.equal(contacts[0].name[0], localContactName);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB Number found', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetFbNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber(targetFbNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.equal(contacts[0].name[0], fbContactName);
|
||||
assert.equal(contacts[0].isFbContact, true);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB Number not found either', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(notFoundNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber(notFoundNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB returns error', function(done) {
|
||||
fb.inError = true;
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetFbNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByPhoneNumber(targetFbNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
delete window.fb.inError;
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findByAddress', function() {
|
||||
|
||||
test('removes spaces', function(done) {
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('+33123456789')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByAddress('+33 1 23 45 67 89').then(function(contacts){
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('The mozContacts find() call returned an error', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('callonerror')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByAddress('callonerror').then((results) => {
|
||||
assert.isArray(results);
|
||||
assert.lengthOf(results, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number found.', function(done) {
|
||||
var result = [{
|
||||
name: [localContactName]
|
||||
}];
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetLocalNumber)
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByAddress(targetLocalNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.isTrue(!contacts[0].isFbContact);
|
||||
assert.equal(contacts[0].name[0], localContactName);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local email found.', function(done) {
|
||||
var result = [{
|
||||
name: [localContactName]
|
||||
}];
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetLocalEmail)
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findByAddress(targetLocalEmail).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.isTrue(!contacts[0].isFbContact);
|
||||
assert.equal(contacts[0].name[0], localContactName);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB Number found', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetFbNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByAddress(targetFbNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.equal(contacts[0].name[0], fbContactName);
|
||||
assert.equal(contacts[0].isFbContact, true);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB Number not found either', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(notFoundNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByAddress(notFoundNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local email not found. FB Number not found either', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(notFoundEmail)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByAddress(notFoundEmail).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local number not found. FB returns error', function(done) {
|
||||
fb.inError = true;
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetFbNumber)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findByAddress(targetFbNumber).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
delete window.fb.inError;
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findExactByEmail', function() {
|
||||
|
||||
test('Local email found.', function(done) {
|
||||
var result = [{
|
||||
name: [localContactName]
|
||||
}];
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetLocalEmail)
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findExactByEmail(targetLocalEmail).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
assert.isTrue(!contacts[0].isFbContact);
|
||||
assert.equal(contacts[0].name[0], localContactName);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local email not found. FB Email not found either', function(done) {
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(notFoundEmail)
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findExactByEmail(notFoundEmail).then(function(contacts) {
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Local email not found. FB returns error', function(done) {
|
||||
fb.inError = true;
|
||||
var result = [{
|
||||
name: [localContactName]
|
||||
}];
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter(targetLocalEmail)
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findExactByEmail(targetLocalEmail).then(function(contacts) {
|
||||
assert.equal(contacts.length, 1);
|
||||
delete window.fb.inError;
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.addUnknown', function() {
|
||||
|
||||
test('For adding an unknown contact', function() {
|
||||
var length = Contacts.getunknownLength();
|
||||
Contacts.addUnknown('123456');
|
||||
|
||||
assert.equal(length + 1, Contacts.getunknownLength());
|
||||
});
|
||||
|
||||
test('For not adding the duplicate unknown contact', function() {
|
||||
var length = Contacts.getunknownLength();
|
||||
|
||||
Contacts.addUnknown('123456');
|
||||
assert.equal(length, Contacts.getunknownLength());
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.clearUnknown', function() {
|
||||
|
||||
test('For clearing unknown contact', function() {
|
||||
Contacts.clearUnknown();
|
||||
var length = Contacts.getunknownLength();
|
||||
|
||||
assert.equal(length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findByUnknown function', function() {
|
||||
|
||||
setup(function() {
|
||||
Contacts.clearUnknown();
|
||||
Contacts.addUnknown('123123456');
|
||||
Contacts.addUnknown('789789789');
|
||||
Contacts.addUnknown('456456456');
|
||||
Contacts.addUnknown('123456789');
|
||||
});
|
||||
|
||||
test('checking single unknown contact using substring', function(done) {
|
||||
|
||||
Contacts.findByUnknown('6456').then(function(list) {
|
||||
assert.equal(list[0].name == '456456456', true);
|
||||
assert.equal(list.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('checking multiple unknown contact using substring', function(done) {
|
||||
|
||||
Contacts.findByUnknown('456').then(function(list) {
|
||||
assert.equal(list[0].name == '123123456', true);
|
||||
assert.equal(list[1].name == '456456456', true);
|
||||
assert.equal(list[2].name == '123456789', true);
|
||||
assert.equal(list.length, 3);//List can have at max 3 unknown elements
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('checking multiple unknown contact using fullstring', function(done) {
|
||||
|
||||
Contacts.findByUnknown('456456456').then(function(list) {
|
||||
assert.equal(list[0].name == '456456456', true);
|
||||
assert.equal(list.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('checking unknown contact after clearUnknown', function(done) {
|
||||
Contacts.clearUnknown();
|
||||
|
||||
Contacts.findByUnknown('456').then(function(list) {
|
||||
assert.equal(list.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Unknown List cannot have more than 3 prediction', function(done) {
|
||||
Contacts.clearUnknown();
|
||||
Contacts.addUnknown('123123456');
|
||||
Contacts.addUnknown('789123789');
|
||||
Contacts.addUnknown('456456123');
|
||||
Contacts.addUnknown('123456789');
|
||||
|
||||
Contacts.findByUnknown('123').then(function(list) {
|
||||
//Prediction should had been 4 but since we restict it to a max of 3
|
||||
assert.equal(list.length, 3);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('Contacts.findBy (success)', function() {
|
||||
|
||||
test('(object, ...), Match', function(done) {
|
||||
var filter = {
|
||||
filterBy: ['tel'],
|
||||
filterOp: 'contains',
|
||||
filterValue: '12125559999'
|
||||
};
|
||||
var result = MockContact.list();
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('12125559999')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findBy(filter).then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('(object, ...), No Match', function(done) {
|
||||
var filter = {
|
||||
filterBy: ['tel'],
|
||||
filterOp: 'contains',
|
||||
filterValue: '911'
|
||||
};
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('911')
|
||||
).returns(
|
||||
Promise.resolve([])
|
||||
);
|
||||
|
||||
Contacts.findBy(filter).then(function(contacts) {
|
||||
// contacts were not found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts.findBy (error)', function() {
|
||||
test('({}, ...)', function(done) {
|
||||
Contacts.findBy({}).then(function(contacts) {
|
||||
assert.isTrue(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Contacts validation', function() {
|
||||
|
||||
test('Contact validation, predominate first', function(done) {
|
||||
var result = MockContact.list([
|
||||
{ givenName: ['Jane'], familyName: ['D'] },
|
||||
|
||||
{ givenName: ['jane'], familyName: ['doe'] },
|
||||
|
||||
{ givenName: ['jane'], familyName: ['dow'] },
|
||||
|
||||
{ givenName: ['Jane'], familyName: ['Doozer'] }
|
||||
]);
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('jane')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('jane d').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
// This was relaxed by the change from "startsWith" to "contains"
|
||||
assert.equal(contacts.length, 4);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Contact validation, predominate last', function(done) {
|
||||
var result = MockContact.list([
|
||||
{ givenName: ['Jane'], familyName: ['Doozer'] },
|
||||
|
||||
{ givenName: ['jane'], familyName: ['doe'] },
|
||||
|
||||
{ givenName: ['jerry'], familyName: ['doe'] },
|
||||
|
||||
{ givenName: ['j'], familyName: ['dow'] },
|
||||
|
||||
{ givenName: ['john'], familyName: ['doland'] }
|
||||
]);
|
||||
|
||||
navigator.mozContacts.find.withArgs(
|
||||
withFilter('do')
|
||||
).returns(
|
||||
Promise.resolve(result)
|
||||
);
|
||||
|
||||
Contacts.findContactByString('j do').then(function(contacts) {
|
||||
// contacts were found
|
||||
assert.ok(Array.isArray(contacts));
|
||||
assert.equal(contacts.length, 5);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
288
gaia/apps/sms/views/shared/test/unit/dialog_test.js
Executable file
|
|
@ -0,0 +1,288 @@
|
|||
/*global loadBodyHTML,
|
||||
Dialog,
|
||||
MockL10n,
|
||||
TransitionEvent
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
require('/views/shared/js/dialog.js');
|
||||
|
||||
suite('Dialog', function() {
|
||||
var nativeMozL10n = document.l10n;
|
||||
var params = null;
|
||||
|
||||
suiteSetup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
params = {
|
||||
title: {
|
||||
raw: 'Foo Title'
|
||||
},
|
||||
body: {
|
||||
raw: 'Foo Body'
|
||||
},
|
||||
options: {
|
||||
cancel: {
|
||||
text: {
|
||||
raw: 'Foo Cancel'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeMozL10n;
|
||||
params = null;
|
||||
});
|
||||
|
||||
test('Creation', function() {
|
||||
var dialog = new Dialog(params);
|
||||
// We check if the object is created properly
|
||||
assert.isFalse(!dialog.show);
|
||||
assert.equal(typeof dialog.show, 'function');
|
||||
assert.isFalse(!dialog.hide);
|
||||
assert.equal(typeof dialog.hide, 'function');
|
||||
});
|
||||
|
||||
test('Appending to DOM', function() {
|
||||
var previouslyDefinedForms = document.getElementsByTagName('form').length;
|
||||
// In this case we have several forms pre-defined (5):
|
||||
// - "messages-compose-form"
|
||||
// - "messages-edit-form"
|
||||
// - "loading"
|
||||
// - "threads-edit-form"
|
||||
assert.equal(previouslyDefinedForms, 4);
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We check if the object is appended to the DOM
|
||||
dialog.show();
|
||||
// Is appended properly?
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
assert.equal(currentlyDefinedFormsLength, 5);
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
assert.equal(dialogForm.dataset.type, 'confirm');
|
||||
});
|
||||
|
||||
test('Has GaiaSimPicker', function() {
|
||||
assert.ok(document.getElementById('sim-picker'));
|
||||
});
|
||||
|
||||
test('Focus', function() {
|
||||
var dialog = new Dialog(params);
|
||||
|
||||
this.sinon.stub(dialog.form, 'focus');
|
||||
this.sinon.stub(HTMLElement.prototype, 'blur');
|
||||
|
||||
dialog.show();
|
||||
|
||||
sinon.assert.called(HTMLElement.prototype.blur);
|
||||
|
||||
var transitionend = new TransitionEvent('transitionend', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
propertyName: 'transform'
|
||||
});
|
||||
dialog.form.dispatchEvent(transitionend);
|
||||
|
||||
sinon.assert.called(dialog.form.focus);
|
||||
});
|
||||
|
||||
test('Redundant shows have no effect', function() {
|
||||
var dialog = new Dialog(params);
|
||||
|
||||
var spy = this.sinon.spy(document.body, 'appendChild');
|
||||
|
||||
dialog.show();
|
||||
dialog.show();
|
||||
|
||||
assert.ok(spy.calledOnce);
|
||||
});
|
||||
|
||||
test('Hiding removes element after transition', function() {
|
||||
var dialog = new Dialog(params);
|
||||
|
||||
dialog.show();
|
||||
assert.notEqual(dialog.form.parentElement, null);
|
||||
|
||||
dialog.hide();
|
||||
assert.notEqual(dialog.form.parentElement, null);
|
||||
|
||||
var transitionend = new TransitionEvent('transitionend', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
propertyName: 'transform'
|
||||
});
|
||||
dialog.form.dispatchEvent(transitionend);
|
||||
assert.equal(dialog.form.parentElement, null);
|
||||
});
|
||||
|
||||
test('Checking the structure. Default.', function() {
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
// We retrieve the last created form
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
// We check how many buttons we have (only the mandatory one)
|
||||
var formOptions = dialogForm.getElementsByTagName('button');
|
||||
assert.equal(formOptions.length, 1);
|
||||
});
|
||||
|
||||
test('Checking the structure. Confirm.', function() {
|
||||
// We add the confirm
|
||||
params.options.confirm = {
|
||||
text: {
|
||||
raw: 'Foo Cancel'
|
||||
}
|
||||
};
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
// We retrieve the last created form
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
// We check how many buttons we have (mandatory + confirm one)
|
||||
var formOptions = dialogForm.getElementsByTagName('button');
|
||||
assert.equal(formOptions.length, 2);
|
||||
// We check if there is a 'recommend' style
|
||||
var optionalOptions = dialogForm.getElementsByClassName('recommend');
|
||||
assert.equal(optionalOptions.length, 1);
|
||||
});
|
||||
|
||||
test('Checking the structure. Confirm (with custom class name).', function() {
|
||||
// We add the confirm
|
||||
params.options.confirm = {
|
||||
text: {
|
||||
raw: 'Foo Cancel'
|
||||
},
|
||||
className: 'test-class'
|
||||
};
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
// We retrieve the last created form
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
|
||||
// We check whether default 'recommend' class wasn't applied
|
||||
var optionalOptions = dialogForm.getElementsByClassName('recommend');
|
||||
assert.equal(optionalOptions.length, 0);
|
||||
// We check whether custom class name was applied
|
||||
optionalOptions = dialogForm.getElementsByClassName('test-class');
|
||||
assert.equal(optionalOptions.length, 1);
|
||||
});
|
||||
|
||||
test('Checking the localization.', function() {
|
||||
params.title = 'l10n Title',
|
||||
params.body = { raw: 'non-l10n Body' },
|
||||
params.options.cancel = {
|
||||
text: { id: 'l10n keyCancel' }
|
||||
};
|
||||
params.options.confirm = {
|
||||
text: { id: 'l10n keyConfirm', args: { n: 1 } }
|
||||
};
|
||||
var l10nSpy = this.sinon.spy(document.l10n, 'setAttributes');
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
// We retrieve the last created form
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
// We check how many buttons we have (mandatory + confirm one)
|
||||
var titleDOM = dialogForm.querySelector('h1');
|
||||
var bodyDOM = dialogForm.querySelector('p');
|
||||
var formOptions = dialogForm.getElementsByTagName('button');
|
||||
assert.equal(formOptions.length, 2);
|
||||
|
||||
// We check localization
|
||||
assert.equal(titleDOM.getAttribute('data-l10n-id'), params.title);
|
||||
assert.equal(bodyDOM.textContent, params.body.raw);
|
||||
assert.isFalse(bodyDOM.hasAttribute('data-l10n-id'));
|
||||
sinon.assert.calledWith(
|
||||
l10nSpy, formOptions[0], params.options.cancel.text.id
|
||||
);
|
||||
sinon.assert.calledWith(
|
||||
l10nSpy, formOptions[1],
|
||||
params.options.confirm.text.id, params.options.confirm.text.args
|
||||
);
|
||||
});
|
||||
|
||||
test('Checking parametrized body localization.', function() {
|
||||
params.title = 'l10n Title';
|
||||
params.body = {
|
||||
id: 'l10n Body',
|
||||
args: {
|
||||
n: 3,
|
||||
numbers: ['123', '456', '789']
|
||||
}
|
||||
};
|
||||
params.options.cancel = {
|
||||
text: 'l10n keyCancel'
|
||||
};
|
||||
params.options.confirm = {
|
||||
text: 'l10n keyConfirm'
|
||||
};
|
||||
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
// We retrieve the last created form
|
||||
var currentlyDefinedForms = document.getElementsByTagName('form');
|
||||
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
|
||||
// We check the type
|
||||
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
|
||||
// We check how many buttons we have (mandatory + confirm one)
|
||||
var bodyDOM = dialogForm.querySelector('p');
|
||||
// We check localization
|
||||
assert.equal(
|
||||
bodyDOM.getAttribute('data-l10n-id'), params.body.id,
|
||||
'Body DOM localized with proper string'
|
||||
);
|
||||
assert.equal(
|
||||
bodyDOM.getAttribute('data-l10n-args'), JSON.stringify(params.body.args),
|
||||
'Body DOM localized with proper string'
|
||||
);
|
||||
});
|
||||
|
||||
test('Should prevent pointer events before transitionend', function() {
|
||||
// Now we create the new element
|
||||
var dialog = new Dialog(params);
|
||||
// We append the element to the DOM
|
||||
dialog.show();
|
||||
assert.isTrue(document.body.classList.contains('dialog-animating'));
|
||||
var transitionend = new TransitionEvent('transitionend', {
|
||||
bubbles: true,
|
||||
propertyName: 'transform'
|
||||
});
|
||||
dialog.form.dispatchEvent(transitionend);
|
||||
assert.isFalse(document.body.classList.contains('dialog-animating'));
|
||||
});
|
||||
|
||||
test('adding a class name', function() {
|
||||
params.classes = ['specific-class1', 'specific-class2'];
|
||||
var dialog = new Dialog(params);
|
||||
dialog.show();
|
||||
|
||||
var elt = document.querySelector('.specific-class1');
|
||||
assert.ok(elt);
|
||||
assert.ok(elt.classList.contains('specific-class2'));
|
||||
});
|
||||
});
|
||||
306
gaia/apps/sms/views/shared/test/unit/error_dialog_test.js
Executable file
|
|
@ -0,0 +1,306 @@
|
|||
/*global Dialog,
|
||||
ErrorDialog,
|
||||
MockL10n,
|
||||
MocksHelper,
|
||||
Settings
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_dialog.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
|
||||
require('/views/shared/js/dialog.js');
|
||||
require('/views/shared/js/error_dialog.js');
|
||||
|
||||
var mocksHelperForDialog = new MocksHelper([
|
||||
'Dialog',
|
||||
'Settings'
|
||||
]).init();
|
||||
|
||||
suite('ErrorDialog', function() {
|
||||
// Silencing jshint complaints related to "new ErrorDialog(..)" invocations
|
||||
/*jshint nonew: false */
|
||||
var realL10n;
|
||||
|
||||
mocksHelperForDialog.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
realL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
this.sinon.spy(Dialog, 'call');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
document.l10n = realL10n;
|
||||
});
|
||||
|
||||
suite('basic tests', function() {
|
||||
test('uses correct Dialog title, body and cancel button l10nIds',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST'
|
||||
};
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: errorDescription.prefix + 'Title',
|
||||
body: errorDescription.prefix + 'Body',
|
||||
options: {
|
||||
cancel: {
|
||||
text: errorDescription.prefix + 'BtnOk'
|
||||
}
|
||||
},
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('throws if prefix is not passed', function() {
|
||||
assert.throws(function() {
|
||||
(new ErrorDialog({}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('uses correct Dialog options', function() {
|
||||
test('confirm handler is not set, errorDescription.executeHandler is true',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
executeHandler: true
|
||||
};
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: sinon.match.any,
|
||||
options: {
|
||||
cancel: {
|
||||
text: errorDescription.prefix + 'BtnOk'
|
||||
}
|
||||
},
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('confirm handler is set, errorDescription.executeHandler is false',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
executeHandler: false
|
||||
};
|
||||
var dialogOptions = { confirmHandler: () => {} };
|
||||
|
||||
(new ErrorDialog(errorDescription, dialogOptions));
|
||||
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: sinon.match.any,
|
||||
options: {
|
||||
cancel: {
|
||||
text: errorDescription.prefix + 'BtnOk'
|
||||
}
|
||||
},
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('confirm handler is set, errorDescription.executeHandler is true',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
executeHandler: true
|
||||
};
|
||||
var dialogOptions = { confirmHandler: () => {} };
|
||||
|
||||
(new ErrorDialog(errorDescription, dialogOptions));
|
||||
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: sinon.match.any,
|
||||
options: {
|
||||
cancel: {
|
||||
text: errorDescription.prefix + 'BtnOk'
|
||||
},
|
||||
confirm: {
|
||||
text: errorDescription.prefix + 'Confirm',
|
||||
method: dialogOptions.confirmHandler
|
||||
}
|
||||
},
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('respects errorDescription.showRecipient', function() {
|
||||
test('errorDescription.showRecipient is true, recipients array is set',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showRecipient: true
|
||||
};
|
||||
|
||||
var dialogOptions = {
|
||||
recipients: ['111', '222', '333']
|
||||
};
|
||||
|
||||
(new ErrorDialog(errorDescription, dialogOptions));
|
||||
|
||||
var fragmentContainsBody = sinon.match((fragment) => {
|
||||
var body = fragment.querySelector('span');
|
||||
var expectedL10nId = errorDescription.prefix + 'Body';
|
||||
var hasL10nId = (body.dataset.l10nId === expectedL10nId);
|
||||
var expectedArgs = JSON.stringify(
|
||||
{ n: dialogOptions.recipients.length }
|
||||
);
|
||||
var hasL10nArgs = (body.dataset.l10nArgs === expectedArgs);
|
||||
|
||||
return hasL10nId && hasL10nArgs;
|
||||
}, 'fragment contains correct body');
|
||||
|
||||
var fragmentContainsRecipients = sinon.match((fragment) => {
|
||||
var recipients = fragment.querySelector('ul');
|
||||
return dialogOptions.recipients.every((recipient, i) => {
|
||||
var li = recipients.children[i];
|
||||
return li.textContent === recipient;
|
||||
});
|
||||
}, 'fragment contains all recipients');
|
||||
|
||||
var fragmentContainsBodyAndRecipients =
|
||||
sinon.match.instanceOf(DocumentFragment)
|
||||
.and(fragmentContainsBody)
|
||||
.and(fragmentContainsRecipients);
|
||||
|
||||
var expectedClasses = [
|
||||
'error-dialog-TEST', 'error-dialog', 'error-dialog-show-recipient'
|
||||
];
|
||||
|
||||
sinon.assert.calledWithMatch(Dialog.call, sinon.match.any, {
|
||||
body: {
|
||||
raw: fragmentContainsBodyAndRecipients
|
||||
},
|
||||
classes: expectedClasses
|
||||
});
|
||||
});
|
||||
|
||||
test('errorDescription.showRecipient is true, recipients array is not set',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showRecipient: true
|
||||
};
|
||||
|
||||
assert.throws(function() {
|
||||
(new ErrorDialog(errorDescription));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('respects errorDescription.showDsdsStatus', function() {
|
||||
test('errorDescription.showDsdsStatus is true, SIM 1 is active',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showDsdsStatus: true
|
||||
};
|
||||
|
||||
Settings.mmsServiceId = 0;
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: {
|
||||
id: errorDescription.prefix + 'Body',
|
||||
args: {
|
||||
activeSimId: '1',
|
||||
nonActiveSimId: '2'
|
||||
}
|
||||
},
|
||||
options: sinon.match.any,
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('errorDescription.showDsdsStatus is true, SIM 2 is active',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showDsdsStatus: true
|
||||
};
|
||||
|
||||
Settings.mmsServiceId = 1;
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: {
|
||||
id: errorDescription.prefix + 'Body',
|
||||
args: {
|
||||
activeSimId: '2',
|
||||
nonActiveSimId: '1'
|
||||
}
|
||||
},
|
||||
options: sinon.match.any,
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('errorDescription.showDsdsStatus is false, any SIM is active',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showDsdsStatus: false
|
||||
};
|
||||
|
||||
Settings.mmsServiceId = 0;
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: errorDescription.prefix + 'Body',
|
||||
options: sinon.match.any,
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
|
||||
test('errorDescription.showDsdsStatus is true, mmsServiceId is not valid',
|
||||
function() {
|
||||
var errorDescription = {
|
||||
prefix: 'TEST',
|
||||
showDsdsStatus: true
|
||||
};
|
||||
|
||||
// No mmsServiceId available
|
||||
Settings.mmsServiceId = null;
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: {
|
||||
id: errorDescription.prefix + 'Body',
|
||||
args: null
|
||||
},
|
||||
options: sinon.match.any,
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
Dialog.call.reset();
|
||||
|
||||
// Unknown mmsServiceId
|
||||
Settings.mmsServiceId = 2;
|
||||
|
||||
(new ErrorDialog(errorDescription));
|
||||
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
|
||||
title: sinon.match.any,
|
||||
body: {
|
||||
id: errorDescription.prefix + 'Body',
|
||||
args: null
|
||||
},
|
||||
options: sinon.match.any,
|
||||
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
34
gaia/apps/sms/views/shared/test/unit/errors_test.js
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
/*global Errors */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/js/errors.js');
|
||||
|
||||
suite('Errors', function() {
|
||||
test('Gets default value for unknown error code', function() {
|
||||
var defaultError = Errors.get('UnknownError');
|
||||
|
||||
assert.isNotNull(defaultError);
|
||||
assert.isString(defaultError.prefix);
|
||||
assert.deepEqual(defaultError, Errors.get('I do not know this error!'));
|
||||
});
|
||||
|
||||
test('Gets correct error description for known error code', function() {
|
||||
var defaultError = Errors.get('UnknownError');
|
||||
|
||||
var knownErrorCodes = [
|
||||
'NoSignalError', 'NotFoundError', 'FdnCheckError', 'InvalidAddressError',
|
||||
'NoSimCardError', 'RadioDisabledError', 'NonActiveSimCardError',
|
||||
'SimNotMatchedError', 'NonActiveSimCardToSendError',
|
||||
'RadioDisabledToDownloadError'
|
||||
];
|
||||
|
||||
knownErrorCodes.forEach((code) => {
|
||||
var error = Errors.get(code);
|
||||
|
||||
assert.isNotNull(error);
|
||||
assert.isString(error.prefix);
|
||||
assert.notEqual(error, defaultError);
|
||||
});
|
||||
});
|
||||
});
|
||||
100
gaia/apps/sms/views/shared/test/unit/fixture_phones.js
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
/*exported FixturePhones */
|
||||
|
||||
'use strict';
|
||||
/**
|
||||
* Derived from
|
||||
* /dom/phonenumberutils/tests/test_phonenumber.xul
|
||||
*
|
||||
* isTestable: true|false
|
||||
*
|
||||
* Fixtures marked isTestable: false are effectively
|
||||
* placeholders that stand-in to show the correct
|
||||
* behaviour in cases where
|
||||
* navigator.mozPhoneNumberService.normalize is
|
||||
* available (sufficient permissions required)
|
||||
*
|
||||
*
|
||||
* title: string
|
||||
*
|
||||
* Description used for the suite title.
|
||||
*
|
||||
* values: array
|
||||
*
|
||||
* Array of values that are tested by comparing
|
||||
* each value to every other value.
|
||||
*
|
||||
*/
|
||||
var FixturePhones = [
|
||||
{
|
||||
// This is only tested where there are sufficient permissions
|
||||
// to use navigator.mozPhoneNumberService.normalize
|
||||
isTestable: !!(navigator.mozPhoneNumberService &&
|
||||
navigator.mozPhoneNumberService.normalize),
|
||||
title: 'Non-Digit in Contact',
|
||||
values: [
|
||||
'1-800-BUY-A-CAR', '1 800 BUY A CAR', '1 (800) BUY A CAR',
|
||||
'+18002892227', '18002892227', '8002892227'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'US',
|
||||
values: [
|
||||
'9995551234', '+19995551234', '(999) 555-1234',
|
||||
'1 (999) 555-1234', '+1 (999) 555-1234', '+1 999-555-1234'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'DE',
|
||||
values: [
|
||||
'01149451491934', '49451491934', '451491934',
|
||||
'0451 491934', '+49 451 491934', '+49451491934'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'IT',
|
||||
values: [
|
||||
'0577-555-555', '0577555555', '05 7755 5555', '+39 05 7755 5555'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'ES',
|
||||
values: [
|
||||
'612123123', '612 12 31 23', '+34 612 12 31 23'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'BR',
|
||||
values: [
|
||||
'01187654321', '0411187654321', '551187654321',
|
||||
'90411187654321', '+551187654321'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'CL',
|
||||
values: [
|
||||
'0997654321', '997654321', '(99) 765 4321', '+56 99 765 4321'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'CO',
|
||||
values: [
|
||||
'5712234567', '12234567', '(1) 2234567', '+57 1 2234567'
|
||||
]
|
||||
},
|
||||
{
|
||||
isTestable: true,
|
||||
title: 'FR',
|
||||
values: [
|
||||
'0123456789', '+33123456789', '0033123456789',
|
||||
'01.23.45.67.89', '01 23 45 67 89', '01-23-45-67-89',
|
||||
'+33 1 23 45 67 89', '+33 (1) 23456789'
|
||||
]
|
||||
}
|
||||
];
|
||||
121
gaia/apps/sms/views/shared/test/unit/inter_instance_event_dispatcher_test.js
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
/*global InterInstanceEventDispatcher,
|
||||
MockBroadcastChannel,
|
||||
BroadcastChannel
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
|
||||
require('/views/shared/js/inter_instance_event_dispatcher.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
|
||||
suite('InterInstanceEventDispatcher >', function() {
|
||||
var realBroadcastChannel;
|
||||
|
||||
suiteSetup(function() {
|
||||
realBroadcastChannel = Object.getOwnPropertyDescriptor(
|
||||
window, 'BroadcastChannel'
|
||||
);
|
||||
window.BroadcastChannel = MockBroadcastChannel;
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
Object.defineProperty(window, 'BroadcastChannel', realBroadcastChannel);
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(window, 'BroadcastChannel');
|
||||
this.sinon.stub(BroadcastChannel.prototype, 'addEventListener');
|
||||
this.sinon.stub(BroadcastChannel.prototype, 'removeEventListener');
|
||||
this.sinon.stub(BroadcastChannel.prototype, 'close');
|
||||
this.sinon.stub(BroadcastChannel.prototype, 'postMessage');
|
||||
this.sinon.spy(window, 'addEventListener');
|
||||
|
||||
InterInstanceEventDispatcher.connect();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
InterInstanceEventDispatcher.disconnect();
|
||||
});
|
||||
|
||||
test('correctly connects to BroadcastChannel', function() {
|
||||
sinon.assert.calledWith(BroadcastChannel, 'messages-channel');
|
||||
sinon.assert.calledWith(
|
||||
BroadcastChannel.prototype.addEventListener,
|
||||
'message'
|
||||
);
|
||||
|
||||
InterInstanceEventDispatcher.connect();
|
||||
InterInstanceEventDispatcher.connect();
|
||||
sinon.assert.calledOnce(BroadcastChannel);
|
||||
|
||||
InterInstanceEventDispatcher.disconnect();
|
||||
InterInstanceEventDispatcher.connect();
|
||||
sinon.assert.calledTwice(BroadcastChannel);
|
||||
});
|
||||
|
||||
test('correctly disconnects from BroadcastChannel', function() {
|
||||
var addEventListenerArgs =
|
||||
BroadcastChannel.prototype.addEventListener.lastCall.args;
|
||||
window.addEventListener.withArgs('unload').yield();
|
||||
|
||||
sinon.assert.called(BroadcastChannel.prototype.close);
|
||||
sinon.assert.calledWith(
|
||||
BroadcastChannel.prototype.removeEventListener,
|
||||
addEventListenerArgs[0],
|
||||
// Reference to "onmessage" listener function
|
||||
addEventListenerArgs[1]
|
||||
);
|
||||
});
|
||||
|
||||
test('correctly handles incoming BroadcastChannel messages', function() {
|
||||
BroadcastChannel.prototype.postMessage.reset();
|
||||
|
||||
var onDraftsChangedStub = sinon.stub();
|
||||
InterInstanceEventDispatcher.on('draft-saved', onDraftsChangedStub);
|
||||
|
||||
BroadcastChannel.prototype.addEventListener.withArgs('message').yield({
|
||||
data: {
|
||||
name: 'draft-saved',
|
||||
parameters: { key: 'value' }
|
||||
}
|
||||
});
|
||||
|
||||
sinon.assert.calledWith(onDraftsChangedStub, { key: 'value' });
|
||||
|
||||
BroadcastChannel.prototype.addEventListener.withArgs('message').yield({
|
||||
data: {
|
||||
name: 'draft-saved',
|
||||
parameters: { key: 'value#2' }
|
||||
}
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(onDraftsChangedStub);
|
||||
sinon.assert.calledWith(onDraftsChangedStub, { key: 'value#2' });
|
||||
|
||||
sinon.assert.notCalled(BroadcastChannel.prototype.postMessage);
|
||||
});
|
||||
|
||||
test('correctly handles outgoing BroadcastChannel messages', function() {
|
||||
var onDraftsChangedStub = sinon.stub();
|
||||
InterInstanceEventDispatcher.on('draft-saved', onDraftsChangedStub);
|
||||
|
||||
assert.throws(() => {
|
||||
InterInstanceEventDispatcher.emit('something-changed');
|
||||
}, 'Event "something-changed" is not allowed!');
|
||||
|
||||
InterInstanceEventDispatcher.disconnect();
|
||||
assert.throws(() => {
|
||||
InterInstanceEventDispatcher.emit('draft-saved', { key: 'value' });
|
||||
}, 'Channel is not created!');
|
||||
InterInstanceEventDispatcher.connect();
|
||||
|
||||
InterInstanceEventDispatcher.emit('draft-saved', { key: 'value' });
|
||||
sinon.assert.calledWith(BroadcastChannel.prototype.postMessage, {
|
||||
name: 'draft-saved',
|
||||
parameters: { key: 'value' }
|
||||
});
|
||||
sinon.assert.notCalled(onDraftsChangedStub, 'Does not broadcast to itself');
|
||||
});
|
||||
});
|
||||
217
gaia/apps/sms/views/shared/test/unit/localization_helper_test.js
Executable file
|
|
@ -0,0 +1,217 @@
|
|||
/*global LocalizationHelper,
|
||||
MockL10n
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/js/localization_helper.js');
|
||||
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
suite('LocalizationHelper >', function() {
|
||||
var navigatorMozL10n;
|
||||
|
||||
function onDOMRetranslated() {
|
||||
document.addEventListener.withArgs('DOMRetranslated').yield();
|
||||
return false;
|
||||
}
|
||||
|
||||
function onTimeFormatChange() {
|
||||
window.addEventListener.withArgs('timeformatchange').yield();
|
||||
return true;
|
||||
}
|
||||
|
||||
setup(function(done) {
|
||||
navigatorMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
|
||||
this.sinon.stub(document.l10n, 'translateFragment');
|
||||
this.sinon.stub(window, 'addEventListener');
|
||||
this.sinon.stub(document, 'addEventListener');
|
||||
|
||||
const getAttr = this.sinon.stub(document.documentElement, 'getAttribute');
|
||||
getAttr.withArgs('lang').returns('be-BY');
|
||||
getAttr.withArgs('dir').returns('rtl');
|
||||
|
||||
[
|
||||
'attachment-container',
|
||||
'attachment-container',
|
||||
'custom'
|
||||
].forEach((iframeClassName) => {
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.className = iframeClassName;
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
|
||||
var customIframeDocument = document.querySelector(
|
||||
'iframe.custom'
|
||||
).contentDocument;
|
||||
customIframeDocument.documentElement.lang = 'en-US';
|
||||
customIframeDocument.documentElement.dir = 'ltr';
|
||||
|
||||
LocalizationHelper.init().then(
|
||||
() => undefined).then(done, done);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
document.l10n = navigatorMozL10n;
|
||||
});
|
||||
|
||||
test('localizes only attachment iframes', function() {
|
||||
sinon.assert.calledTwice(document.l10n.translateFragment);
|
||||
var attachmentContainers = document.querySelectorAll(
|
||||
'iframe.attachment-container'
|
||||
);
|
||||
Array.forEach(attachmentContainers, (iframe) => {
|
||||
var doc = iframe.contentDocument;
|
||||
assert.equal(doc.documentElement.lang, 'be-BY');
|
||||
assert.equal(doc.documentElement.dir, 'rtl');
|
||||
sinon.assert.calledWith(document.l10n.translateFragment, doc.body);
|
||||
});
|
||||
|
||||
// Custom iframe should stay untouched.
|
||||
var customIframeDocument = document.querySelector(
|
||||
'iframe.custom'
|
||||
).contentDocument;
|
||||
assert.equal(customIframeDocument.documentElement.lang, 'en-US');
|
||||
assert.equal(customIframeDocument.documentElement.dir, 'ltr');
|
||||
});
|
||||
|
||||
[onDOMRetranslated, onTimeFormatChange].forEach(function(forceUpdateMethod) {
|
||||
suite('localizes date & time ' + forceUpdateMethod.name +' >', function() {
|
||||
var node;
|
||||
|
||||
setup(function() {
|
||||
node = document.createElement('div');
|
||||
node.classList.add('l10n-contains-date');
|
||||
document.body.appendChild(node);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
node.remove();
|
||||
});
|
||||
|
||||
test('ignores if date or format are unavailable', function() {
|
||||
node.dataset.l10nDate = '';
|
||||
node.dataset.l10nDateFormat = 'format';
|
||||
node.textContent = 'not-changed-content';
|
||||
|
||||
forceUpdateMethod();
|
||||
|
||||
assert.equal(node.textContent, 'not-changed-content');
|
||||
assert.isFalse(node.hasAttribute('data-l10n-id'));
|
||||
assert.isFalse(node.hasAttribute('data-l10n-args'));
|
||||
|
||||
node.dataset.l10nDate = Date.now();
|
||||
node.dataset.l10nDateFormat = '';
|
||||
node.textContent = 'not-changed-content';
|
||||
|
||||
forceUpdateMethod();
|
||||
|
||||
assert.equal(node.textContent, 'not-changed-content');
|
||||
assert.isFalse(node.hasAttribute('data-l10n-id'));
|
||||
assert.isFalse(node.hasAttribute('data-l10n-args'));
|
||||
});
|
||||
|
||||
test('correctly formats node content', function() {
|
||||
var timestamp = Date.now();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
function assertChange(options, doNotFormat) {
|
||||
options.hour12 = navigator.mozHour12;
|
||||
var formatter =
|
||||
new Intl.DateTimeFormat(navigator.languages, options);
|
||||
// In case of time format change date-only elements aren't updated
|
||||
if (doNotFormat) {
|
||||
assert.equal(
|
||||
node.textContent, '', 'Date-only elements should not be updated'
|
||||
);
|
||||
} else {
|
||||
assert.equal(
|
||||
node.textContent,
|
||||
formatter.format(new Date(timestamp))
|
||||
);
|
||||
}
|
||||
|
||||
assert.isFalse(node.hasAttribute('data-l10n-id'));
|
||||
assert.isFalse(node.hasAttribute('data-l10n-args'));
|
||||
}
|
||||
|
||||
node.dataset.l10nDate = timestamp;
|
||||
node.dataset.l10nDateFormat = JSON.stringify(options);
|
||||
// In case of time format change date-only elements shouldn't be updated
|
||||
assertChange(options, forceUpdateMethod());
|
||||
|
||||
options.hour = 'numeric';
|
||||
options.minute = 'numeric';
|
||||
node.dataset.l10nDateFormat = JSON.stringify(options);
|
||||
|
||||
navigator.mozHour12 = true;
|
||||
forceUpdateMethod();
|
||||
assertChange(options);
|
||||
|
||||
navigator.mozHour12 = false;
|
||||
forceUpdateMethod();
|
||||
|
||||
assertChange(options);
|
||||
});
|
||||
|
||||
test('correctly formats node l10n attributes', function() {
|
||||
var timestamp = Date.now();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
function assertChange(options, doNotFormat) {
|
||||
options.hour12 = navigator.mozHour12;
|
||||
var formatter =
|
||||
new Intl.DateTimeFormat(navigator.languages, options);
|
||||
|
||||
var l10nAttributes = document.l10n.getAttributes(node);
|
||||
// In case of time format change date-only elements aren't updated
|
||||
if (doNotFormat) {
|
||||
assert.deepEqual(l10nAttributes.args, {
|
||||
data: 'custom'
|
||||
}, 'Date-only elements should not be updated');
|
||||
} else {
|
||||
assert.deepEqual(l10nAttributes.args, {
|
||||
data: 'custom',
|
||||
date: formatter.format(new Date(timestamp))
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(l10nAttributes.id, 'custom');
|
||||
assert.equal(node.textContent, 'not-changed-content');
|
||||
}
|
||||
|
||||
document.l10n.setAttributes(node, 'custom', {
|
||||
data: 'custom'
|
||||
});
|
||||
node.dataset.l10nDate = timestamp;
|
||||
node.dataset.l10nDateFormat = JSON.stringify(options);
|
||||
node.textContent = 'not-changed-content';
|
||||
// In case of time format change date-only elements shouldn't be updated
|
||||
assertChange(options, forceUpdateMethod());
|
||||
|
||||
options.hour = 'numeric';
|
||||
options.minute = 'numeric';
|
||||
node.dataset.l10nDateFormat = JSON.stringify(options);
|
||||
|
||||
navigator.mozHour12 = true;
|
||||
forceUpdateMethod();
|
||||
|
||||
assertChange(options);
|
||||
|
||||
navigator.mozHour12 = false;
|
||||
forceUpdateMethod();
|
||||
|
||||
assertChange(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
gaia/apps/sms/views/shared/test/unit/media/audio.oga
Executable file
6
gaia/apps/sms/views/shared/test/unit/media/contacts.vcf
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:dennis
|
||||
N:;dennis;;;
|
||||
TEL;TYPE=CELL:0975557726
|
||||
END:VCARD
|
||||
BIN
gaia/apps/sms/views/shared/test/unit/media/grid.png
Executable file
|
After Width: | Height: | Size: 363 B |
BIN
gaia/apps/sms/views/shared/test/unit/media/grid.wbmp
Executable file
BIN
gaia/apps/sms/views/shared/test/unit/media/video.ogv
Executable file
88
gaia/apps/sms/views/shared/test/unit/messages_mockup.js
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
/*global getMockupedDate */
|
||||
/*exported MockThreadMessages */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockThreadMessages() {
|
||||
|
||||
var messagesMockup = [
|
||||
{
|
||||
sender: null,
|
||||
receiver: '197746797',
|
||||
body: 'Error message:)',
|
||||
delivery: 'sending',
|
||||
deliveryInfo: [{
|
||||
receiver: '197746797',
|
||||
deliveryStatus: 'pending',
|
||||
deliveryTimestamp: null,
|
||||
readStatus: 'not-applicable',
|
||||
readTimestamp: null
|
||||
}],
|
||||
error: true,
|
||||
id: 47,
|
||||
threadId: 1,
|
||||
timestamp: +getMockupedDate(0)
|
||||
},
|
||||
{
|
||||
sender: null,
|
||||
receiver: '197746797',
|
||||
body: 'Nothing :)',
|
||||
delivery: 'sent',
|
||||
deliveryInfo: [{
|
||||
receiver: '197746797',
|
||||
deliveryStatus: 'success',
|
||||
deliveryTimestamp: +getMockupedDate(0),
|
||||
readStatus: 'not-applicable',
|
||||
readTimestamp: null
|
||||
}],
|
||||
id: 46,
|
||||
threadId: 1,
|
||||
timestamp: +getMockupedDate(0)
|
||||
},
|
||||
{
|
||||
sender: '197746797',
|
||||
body: 'Recibido!',
|
||||
delivery: 'received',
|
||||
id: 40,
|
||||
threadId: 1,
|
||||
timestamp: +getMockupedDate(2)
|
||||
},
|
||||
{
|
||||
sender: null,
|
||||
receiver: '197746797',
|
||||
body: 'Nothing :)',
|
||||
delivery: 'error',
|
||||
deliveryInfo: [{
|
||||
receiver: '197746797',
|
||||
deliveryStatus: 'errpr',
|
||||
deliveryTimestamp: null,
|
||||
readStatus: 'not-applicable',
|
||||
readTimestamp: null
|
||||
}],
|
||||
id: 460,
|
||||
threadId: 1,
|
||||
timestamp: +getMockupedDate(6)
|
||||
},
|
||||
{
|
||||
sender: null,
|
||||
receiver: '197746797',
|
||||
body: 'Nothing at all :)',
|
||||
delivery: 'error',
|
||||
deliveryInfo: [{
|
||||
receiver: '197746797',
|
||||
deliveryStatus: 'error',
|
||||
deliveryTimestamp: null,
|
||||
readStatus: 'not-applicable',
|
||||
readTimestamp: null
|
||||
}],
|
||||
id: 461,
|
||||
threadId: 1,
|
||||
timestamp: +getMockupedDate(6)
|
||||
}];
|
||||
|
||||
messagesMockup.sort(function(a, b) {
|
||||
return b.timestamp - a.timestamp;
|
||||
});
|
||||
|
||||
return messagesMockup;
|
||||
}
|
||||
6
gaia/apps/sms/views/shared/test/unit/mock_activity_handler.js
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
/*exported MockActivityHandler */
|
||||
'use strict';
|
||||
|
||||
var MockActivityHandler = {
|
||||
init() {}
|
||||
};
|
||||
25
gaia/apps/sms/views/shared/test/unit/mock_activity_picker.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
var MockActivityPicker = {};
|
||||
|
||||
|
||||
[
|
||||
'email', 'dial', 'url',
|
||||
'createNewContact', 'addToExistingContact'
|
||||
].forEach(function(fn) {
|
||||
MockActivityPicker[fn] = function() {
|
||||
MockActivityPicker[fn].called = true;
|
||||
MockActivityPicker[fn].calledWith = [].slice.call(arguments);
|
||||
};
|
||||
|
||||
MockActivityPicker[fn].mSetup = function() {
|
||||
MockActivityPicker[fn].called = false;
|
||||
MockActivityPicker[fn].calledWith = null;
|
||||
};
|
||||
|
||||
MockActivityPicker[fn].mTeardown = function() {
|
||||
delete MockActivityPicker[fn].called;
|
||||
delete MockActivityPicker[fn].calledWith;
|
||||
};
|
||||
});
|
||||
11
gaia/apps/sms/views/shared/test/unit/mock_app.js
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
/* exported MockApp */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockApp = {
|
||||
instanceId: 'app-instance-id',
|
||||
whenReady: () => Promise.resolve(),
|
||||
setReady: () => {}
|
||||
};
|
||||
})(window);
|
||||
36
gaia/apps/sms/views/shared/test/unit/mock_async_storage.js
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
/*exported MockasyncStorage */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockasyncStorage = {
|
||||
keys: {},
|
||||
|
||||
getItem: function(key, cb) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
cb(this.keys[key]);
|
||||
},
|
||||
|
||||
setItem: function(key, value, cb) {
|
||||
this.keys[key] = value;
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
|
||||
removeItem: function(key) {
|
||||
delete this.keys[key];
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.keys = {};
|
||||
},
|
||||
mSetup: function() {
|
||||
this.clear();
|
||||
},
|
||||
mTeardown: function() {
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
27
gaia/apps/sms/views/shared/test/unit/mock_attachment.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
/*global Utils */
|
||||
/*exported MockAttachment */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockAttachment(blob, options) {
|
||||
this.blob = blob;
|
||||
options = options || {};
|
||||
this.name = options.name;
|
||||
this.isDraft = !!options.isDraft;
|
||||
this.mNextRender = document.createElement('iframe');
|
||||
this.mNextRender.className = 'attachment-container';
|
||||
}
|
||||
|
||||
MockAttachment.prototype = {
|
||||
get size() {
|
||||
return this.blob && this.blob.size;
|
||||
},
|
||||
get type() {
|
||||
return Utils.typeFromMimeType(this.blob.type);
|
||||
},
|
||||
render: function() {
|
||||
return this.mNextRender;
|
||||
},
|
||||
view: function() {},
|
||||
updateFileSize: function() {}
|
||||
};
|
||||
15
gaia/apps/sms/views/shared/test/unit/mock_attachment_renderer.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
function MockAttachmentRenderer() {}
|
||||
|
||||
MockAttachmentRenderer.prototype = {
|
||||
render: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getAttachmentContainer: function() {},
|
||||
updateFileSize: function() {}
|
||||
};
|
||||
|
||||
MockAttachmentRenderer.for = function() {
|
||||
return new MockAttachmentRenderer();
|
||||
};
|
||||
12
gaia/apps/sms/views/shared/test/unit/mock_broadcast_channel.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
/*exported MockBroadcastChannel */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockBroadcastChannel(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
MockBroadcastChannel.prototype.addEventListener = () => {};
|
||||
MockBroadcastChannel.prototype.removeEventListener = () => {};
|
||||
MockBroadcastChannel.prototype.postMessage = () => {};
|
||||
MockBroadcastChannel.prototype.close = () => {};
|
||||
35
gaia/apps/sms/views/shared/test/unit/mock_compose.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
/*exported MockCompose */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockCompose = {
|
||||
isSubjectVisible: false,
|
||||
|
||||
init: function() {},
|
||||
on: function(type, handler) {},
|
||||
off: function(type, handler) {},
|
||||
offAll: function() {},
|
||||
getContent: function() {},
|
||||
getText: function() {},
|
||||
isEmpty: () => true,
|
||||
isSubjectMaxLength: () => false,
|
||||
setupLock() {},
|
||||
refresh() {},
|
||||
scrollToTarget: function(target) {},
|
||||
scrollMessageContent: function() {},
|
||||
prepend: function(item) {},
|
||||
append: () => {},
|
||||
clear: () => {},
|
||||
focus: function() {},
|
||||
|
||||
getSubject: function() {},
|
||||
setSubject: function() {},
|
||||
showSubject: function() {},
|
||||
hideSubject: function() {},
|
||||
fromDraft: function() {},
|
||||
|
||||
mSetup: function() {
|
||||
this.size = null;
|
||||
this.type = 'sms';
|
||||
}
|
||||
};
|
||||
70
gaia/apps/sms/views/shared/test/unit/mock_contact.js
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
function MockContact(name) {
|
||||
name = typeof name !== 'undefined' ? name : {
|
||||
familyName: ['O\'Hare'],
|
||||
givenName: ['Pepito']
|
||||
};
|
||||
if (!(this instanceof MockContact)) {
|
||||
return new MockContact(name);
|
||||
}
|
||||
this.id = '1';
|
||||
|
||||
this.familyName = name.familyName;
|
||||
this.givenName = name.givenName;
|
||||
this.name = [
|
||||
[this.givenName, this.familyName].join(' ')
|
||||
];
|
||||
|
||||
this.tel = [
|
||||
{
|
||||
'value': '+346578888888',
|
||||
'type': ['Mobile'],
|
||||
'carrier': 'TEF'
|
||||
},
|
||||
{
|
||||
'value': '+12125559999',
|
||||
'type': 'Batphone', // Make sure we cover both strings and arrays
|
||||
'carrier': 'XXX'
|
||||
}
|
||||
];
|
||||
|
||||
this.email = [
|
||||
{
|
||||
'value': 'a@b.com'
|
||||
}
|
||||
];
|
||||
|
||||
this.category = [
|
||||
'favorite'
|
||||
];
|
||||
|
||||
this.org = [
|
||||
''
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
MockContact.list([names])
|
||||
|
||||
Create contacts lists
|
||||
|
||||
MockContact.list();
|
||||
|
||||
MockContact.list([
|
||||
{ givenName: ['Jane'], familyName: ['Doozer'] },
|
||||
{ givenName: ['doug'], familyName: ['dooley'] }
|
||||
]);
|
||||
*/
|
||||
|
||||
MockContact.list = function(names) {
|
||||
if (names && names.length) {
|
||||
return names.map(function(name) {
|
||||
return new MockContact(name);
|
||||
});
|
||||
} else {
|
||||
return [new MockContact()];
|
||||
}
|
||||
};
|
||||
14
gaia/apps/sms/views/shared/test/unit/mock_contact_renderer.js
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
/*exported MockContactRenderer */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockContactRenderer(opts) {
|
||||
}
|
||||
|
||||
MockContactRenderer.prototype = {
|
||||
render: function() {}
|
||||
};
|
||||
|
||||
MockContactRenderer.flavor = function() {
|
||||
return new MockContactRenderer();
|
||||
};
|
||||
23
gaia/apps/sms/views/shared/test/unit/mock_contacts.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
/*global MockContact */
|
||||
/*exported MockContacts */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/test/unit/mock_contact.js');
|
||||
|
||||
(function(exports) {
|
||||
function returnMock() {
|
||||
var result = MockContact.list();
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
exports.MockContacts = {
|
||||
findBy: returnMock,
|
||||
findByPhoneNumber: returnMock,
|
||||
findByAddress: returnMock,
|
||||
findExactByEmail: returnMock,
|
||||
findByString: returnMock,
|
||||
findExact: returnMock,
|
||||
addUnknown: () => {},
|
||||
};
|
||||
})(window);
|
||||
102
gaia/apps/sms/views/shared/test/unit/mock_conversation.js
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
/*global MockRecipients, Template */
|
||||
/*exported MockConversationView */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/test/unit/mock_recipients.js');
|
||||
|
||||
var MockConversationView = {
|
||||
CHUNK_SIZE: 10,
|
||||
CONVERTED_MESSAGE_DURATION: 3000,
|
||||
IMAGE_RESIZE_DURATION: 3000,
|
||||
recipients: {
|
||||
add: function() {},
|
||||
focus: function() {}
|
||||
},
|
||||
recipientsList: document.createElement('div'),
|
||||
|
||||
// For Information view testing. Need to be updated with Conversation View
|
||||
// layout.
|
||||
optionsButton: document.createElement('button'),
|
||||
callNumberButton: document.createElement('button'),
|
||||
attachButton: document.createElement('button'),
|
||||
subheader: document.createElement('div'),
|
||||
container: document.createElement('article'),
|
||||
composeForm: document.createElement('form'),
|
||||
headerText: document.createElement('h1'),
|
||||
|
||||
inEditMode: false,
|
||||
init: function() {},
|
||||
initRecipients: function() {
|
||||
this.recipients = new MockRecipients({
|
||||
outer: 'messages-to-field',
|
||||
inner: 'messages-recipients-list',
|
||||
template: new Template('messages-recipient-tmpl')
|
||||
});
|
||||
},
|
||||
on: function() {},
|
||||
once: () => {},
|
||||
initSentAudio: function() {},
|
||||
getIdIterator: function() {},
|
||||
onMessageContentChange: function() {},
|
||||
assimilateRecipients: function() {},
|
||||
resizeHandler: function() {},
|
||||
requestContact: function() {},
|
||||
updateComposerHeader: function() {},
|
||||
isScrolledManually: false,
|
||||
manageScroll: function() {},
|
||||
scrollViewToBottom: function() {},
|
||||
back: function() {},
|
||||
isKeyboardDisplayed: function() {},
|
||||
getMessageContainer: function() {},
|
||||
updateHeaderData: function() {},
|
||||
initializeRendering: function() {},
|
||||
stopRendering: function() {},
|
||||
showFirstChunk: function() {},
|
||||
createMmsContent: function() {},
|
||||
renderMessages: function() {},
|
||||
_createNotDownloadedHTML: function() {},
|
||||
buildMessageDOM: function() {},
|
||||
appendMessage: function() {},
|
||||
showChunkOfMessages: function() {},
|
||||
setHeaderAction: function() {},
|
||||
setHeaderContent: function() {},
|
||||
cleanForm: function() {},
|
||||
clear: function() {},
|
||||
toggleCheckedAll: function() {},
|
||||
startEdit: function() {},
|
||||
delete: function() {},
|
||||
cancelEdit: function() {},
|
||||
updateSelectionStatus: function() {},
|
||||
handleMessageClick: function() {},
|
||||
handleEvent: function() {},
|
||||
cleanFields: function() {},
|
||||
onSendClick: function() {},
|
||||
onMessageSending: function() {},
|
||||
onMessageSent: function() {},
|
||||
onMessageFailed: function() {},
|
||||
onDeliverySuccess: function() {},
|
||||
onReadSuccess: function() {},
|
||||
removeMessageDOM: function() {},
|
||||
retrieveMMS: function() {},
|
||||
resendMessage: function() {},
|
||||
toFieldKeypress: function() {},
|
||||
toFieldInput: function() {},
|
||||
searchContact: function() {},
|
||||
onHeaderActivation: function() {},
|
||||
promptContact: () => Promise.resolve(),
|
||||
prompt: () => Promise.resolve(),
|
||||
saveDraft: function() {},
|
||||
discardDraft: function() {},
|
||||
isShowMessageErrorCalledTimes: 0,
|
||||
showMessageError: function() {
|
||||
this.isShowMessageErrorCalledTimes += 1;
|
||||
},
|
||||
mSetup: function() {
|
||||
this.isShowMessageErrorCalledTimes = 0;
|
||||
},
|
||||
|
||||
mTeardown: function() {
|
||||
this.isShowMessageErrorCalledTimes = 0;
|
||||
}
|
||||
};
|
||||
57
gaia/apps/sms/views/shared/test/unit/mock_dialog.js
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
function MockDialog(params) {
|
||||
MockDialog.calls.push(params);
|
||||
MockDialog.instances.push(this);
|
||||
|
||||
Object.keys(params.options).forEach(function(option) {
|
||||
MockDialog.triggers[option] = function() {
|
||||
MockDialog.triggers[option].called = true;
|
||||
params.options[option].method();
|
||||
};
|
||||
|
||||
// Setup the initial |false| value, this prevents assert.isFalse from
|
||||
// failing on |undefined| trigger properties
|
||||
MockDialog.triggers[option].called = false;
|
||||
});
|
||||
}
|
||||
|
||||
MockDialog.triggers = {};
|
||||
|
||||
MockDialog.prototype = {
|
||||
show: function() {
|
||||
this.show.called = true;
|
||||
}
|
||||
};
|
||||
|
||||
MockDialog.mSetup = function() {
|
||||
MockDialog.calls = [];
|
||||
MockDialog.instances = [];
|
||||
};
|
||||
|
||||
MockDialog.mTeardown = function() {
|
||||
delete MockDialog.calls;
|
||||
delete MockDialog.instances;
|
||||
|
||||
Object.keys(MockDialog.triggers).forEach(function(trigger) {
|
||||
MockDialog.triggers[trigger].called = false;
|
||||
});
|
||||
};
|
||||
|
||||
function MockErrorDialog() {
|
||||
MockErrorDialog.calls.push(arguments);
|
||||
}
|
||||
|
||||
MockErrorDialog.prototype = {
|
||||
show: function() {
|
||||
this.show.called = true;
|
||||
}
|
||||
};
|
||||
|
||||
MockErrorDialog.mSetup = function() {
|
||||
MockErrorDialog.calls = [];
|
||||
};
|
||||
|
||||
MockErrorDialog.mTeardown = function() {
|
||||
delete MockErrorDialog.calls;
|
||||
};
|
||||
38
gaia/apps/sms/views/shared/test/unit/mock_inbox.js
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
/*exported MockInboxView */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockInboxView = {
|
||||
count: 0,
|
||||
inEditMode: false,
|
||||
container: document.createElement('div'),
|
||||
init: function() {},
|
||||
updateThread: function() {},
|
||||
getIdIterator: function() {},
|
||||
setContact: function() {},
|
||||
handleEvent: function() {},
|
||||
updateSelectionStatus: function() {},
|
||||
cleanForm: function() {},
|
||||
toggleCheckedAll: function() {},
|
||||
removeConversationDOM: function() {},
|
||||
delete: function() {},
|
||||
setEmpty: function() {},
|
||||
showOptions: function() {},
|
||||
startEdit: function() {},
|
||||
cancelEdit: function() {},
|
||||
renderThreads: () => {},
|
||||
createThread: function() {},
|
||||
insertThreadContainer: function() {},
|
||||
onMessageSending: function() {},
|
||||
onMessageReceived: function() {},
|
||||
onThreadsDeleted: function() {},
|
||||
appendThread: function() {},
|
||||
onDraftSaved: function() {},
|
||||
onDraftDeleted: () => {},
|
||||
showDraftSavedBanner: () => {},
|
||||
createThreadContainer: function() {},
|
||||
updateContactsInfo: function() {},
|
||||
mark: function() {},
|
||||
markReadUnread: () => {},
|
||||
once: () => {}
|
||||
};
|
||||
18
gaia/apps/sms/views/shared/test/unit/mock_information.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
/*exported MockInformation, MockGroupView, MockReportView */
|
||||
'use strict';
|
||||
|
||||
function MockInformation(type) {
|
||||
}
|
||||
|
||||
MockInformation.initDefaultViews = () => {};
|
||||
|
||||
MockInformation.prototype = {
|
||||
onDeliverySuccess: function() {},
|
||||
onReadSuccess: function() {},
|
||||
show: function() {},
|
||||
refresh: function() {},
|
||||
reset: function() {}
|
||||
};
|
||||
|
||||
var MockGroupView = new MockInformation();
|
||||
var MockReportView = new MockInformation();
|
||||
10
gaia/apps/sms/views/shared/test/unit/mock_inter_instance_event_dispatcher.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
/*exported MockInterInstanceEventDispatcher */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockInterInstanceEventDispatcher() {}
|
||||
|
||||
MockInterInstanceEventDispatcher.on = () => {};
|
||||
MockInterInstanceEventDispatcher.emit = () => {};
|
||||
MockInterInstanceEventDispatcher.connect = () => {};
|
||||
MockInterInstanceEventDispatcher.disconnect = () => {};
|
||||
8
gaia/apps/sms/views/shared/test/unit/mock_link_action_handler.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
/*exported MockLinkActionHandler */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockLinkActionHandler = {
|
||||
onContextMenu: function() {},
|
||||
onClick: function() {}
|
||||
};
|
||||
9
gaia/apps/sms/views/shared/test/unit/mock_link_helper.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
/*exported MockLinkHelper */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockLinkHelper = {
|
||||
searchAndLinkClickableData: function(input) {
|
||||
return input;
|
||||
}
|
||||
};
|
||||
9
gaia/apps/sms/views/shared/test/unit/mock_localization_helper.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
/*exported MockLocalizationHelper*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockLocalizationHelper = {
|
||||
init: () => {}
|
||||
};
|
||||
})(window);
|
||||
82
gaia/apps/sms/views/shared/test/unit/mock_messages.js
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
/*exported MockMessages */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockMessages = {
|
||||
smilMockup: '<smil><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><body><par dur="5000ms"><img src="IMG_0011.jpg" ' +
|
||||
'region="Image"/></par></body></smil>',
|
||||
sms: function(opts = {}) {
|
||||
// default sms message
|
||||
var message = {
|
||||
id: 1,
|
||||
iccId: null,
|
||||
threadId: 1,
|
||||
sender: 'sender',
|
||||
receiver: 'receiver',
|
||||
body: 'body',
|
||||
delivery: 'received',
|
||||
deliveryStatus: 'success',
|
||||
deliveryTimestamp: Date.now(),
|
||||
type: 'sms',
|
||||
messageClass: 'normal',
|
||||
timestamp: Date.now(),
|
||||
sentTimestamp: Date.now(),
|
||||
read: true
|
||||
};
|
||||
|
||||
for (var key in message) {
|
||||
if (opts.hasOwnProperty(key)) {
|
||||
message[key] = opts[key];
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
mms: function(opts = {}) {
|
||||
var now = new Date();
|
||||
var tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
// default mms message
|
||||
var message = {
|
||||
id: 1,
|
||||
iccId: null,
|
||||
threadId: 1,
|
||||
sender: 'sender',
|
||||
receivers: ['receiver'],
|
||||
delivery: 'received',
|
||||
deliveryInfo: [{
|
||||
receiver: 'receiver',
|
||||
deliveryStatus: 'success',
|
||||
readStatus: 'success',
|
||||
deliveryTimestamp: +now,
|
||||
readTimestamp: +now,
|
||||
}],
|
||||
type: 'mms',
|
||||
timestamp: +now,
|
||||
sentTimestamp: +now,
|
||||
read: true,
|
||||
subject: '',
|
||||
smil: this.smilMockup,
|
||||
attachments: [{
|
||||
location: 'text1',
|
||||
content: new Blob(['This is a plain text'], { type: 'text/plain' })
|
||||
}],
|
||||
expiryDate: +tomorrow
|
||||
};
|
||||
|
||||
for (var key in message) {
|
||||
if (opts.hasOwnProperty(key)) {
|
||||
message[key] = opts[key];
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
};
|
||||
16
gaia/apps/sms/views/shared/test/unit/mock_mime_mapper.js
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
/*exported MockMimeMapper */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockMimeMapper = {
|
||||
guessTypeFromFileProperties: function(filename, mimetype) {
|
||||
return mimetype;
|
||||
},
|
||||
|
||||
ensureFilenameMatchesType: function(filename, mimetype) {
|
||||
if (filename.indexOf('.') !== -1) {
|
||||
return filename;
|
||||
}
|
||||
return filename + '.' + 'jpg';
|
||||
}
|
||||
};
|
||||
21
gaia/apps/sms/views/shared/test/unit/mock_moz_activity.js
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
function MockMozActivity(activity) {
|
||||
MockMozActivity.calls.push(activity);
|
||||
MockMozActivity.instances.push(this);
|
||||
|
||||
this.onsuccess = function() {};
|
||||
this.onerror = function() {};
|
||||
}
|
||||
|
||||
// create an array to hold any calls to this function to test them
|
||||
MockMozActivity.mSetup = function() {
|
||||
MockMozActivity.calls = [];
|
||||
MockMozActivity.instances = [];
|
||||
};
|
||||
|
||||
// destroy the array so it resets every time
|
||||
MockMozActivity.mTeardown = function() {
|
||||
delete MockMozActivity.calls;
|
||||
delete MockMozActivity.instances;
|
||||
};
|
||||
8
gaia/apps/sms/views/shared/test/unit/mock_moz_alarm.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
/* exported MockMozAlarms */
|
||||
'use strict';
|
||||
|
||||
var MockMozAlarms = {
|
||||
add: function() {},
|
||||
remove: function() {}
|
||||
};
|
||||
|
||||
19
gaia/apps/sms/views/shared/test/unit/mock_navigation.js
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
/* global
|
||||
Promise
|
||||
*/
|
||||
/* exported MockNavigation */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockNavigation = {
|
||||
back: () => Promise.resolve(),
|
||||
init: () => Promise.resolve(),
|
||||
isCurrentPanel: () => {},
|
||||
isDefaultPanel: () => false,
|
||||
toPanel: () => Promise.resolve(),
|
||||
setReady: () => {},
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
once: () => {},
|
||||
hasPendingInit: () => {}
|
||||
};
|
||||
10
gaia/apps/sms/views/shared/test/unit/mock_navigator_mozphonenumberservice.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
/*exported MockMozPhoneNumberService */
|
||||
'use strict';
|
||||
|
||||
var MockMozPhoneNumberService = {
|
||||
normalize: function nmpns_normalize() {
|
||||
return true;
|
||||
},
|
||||
fuzzyMatch: function nmpns_fuzzyMatch() {
|
||||
}
|
||||
};
|
||||
5
gaia/apps/sms/views/shared/test/unit/mock_navigator_vibrate.js
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
/*exported MockVibrate */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockVibrate() {}
|
||||
7
gaia/apps/sms/views/shared/test/unit/mock_notify.js
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
/* exported MockNotify */
|
||||
'use strict';
|
||||
|
||||
var MockNotify = {
|
||||
ringtone: function() {},
|
||||
vibrate: function() {}
|
||||
};
|
||||
98
gaia/apps/sms/views/shared/test/unit/mock_recipients.js
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
'use strict';
|
||||
|
||||
function MockRecipients(setup) {
|
||||
this.setup = setup;
|
||||
this.recipientsList = document.getElementById(setup.inner);
|
||||
Object.defineProperty(this, 'length', {
|
||||
get: function() {
|
||||
return this.numbersList.length;
|
||||
},
|
||||
set: function(val) {
|
||||
this.numbersList.length = val;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'numbers', {
|
||||
get: function() {
|
||||
return Array.from(this.numbersList);
|
||||
}
|
||||
});
|
||||
|
||||
this.events = {
|
||||
add: [],
|
||||
remove: [],
|
||||
modechange: []
|
||||
};
|
||||
this.numbersList = [];
|
||||
this.inputValue = '';
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
MockRecipients.prototype.add = function(contact) {
|
||||
var span = document.createElement('span');
|
||||
span.textContent = contact.number;
|
||||
this.recipientsList.appendChild(span);
|
||||
this.numbersList.push(contact.number);
|
||||
this.list.push(contact);
|
||||
this.emit('add', this.length, contact);
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.remove = function(phone) {
|
||||
var index = this.numbersList.indexOf(phone);
|
||||
if (index != -1) {
|
||||
this.numbersList.splice(this.numbersList.indexOf(phone), 1);
|
||||
this.emit('remove', this.length);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.render = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.focus = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.update = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.visible = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.on = function(event, callback) {
|
||||
this.events[event].push(callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.off = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
MockRecipients.prototype.emit = function(type) {
|
||||
var handlers = this.events;
|
||||
var args = [].slice.call(arguments, 1);
|
||||
var handler, stack;
|
||||
|
||||
if (!handlers[type]) {
|
||||
throw new Error('Invalid event type: ' + type);
|
||||
}
|
||||
|
||||
stack = handlers[type].slice();
|
||||
|
||||
if (stack.length) {
|
||||
while ((handler = stack.pop())) {
|
||||
handler.apply(null, args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
MockRecipients.View = function() {};
|
||||
|
||||
MockRecipients.View.isFocusable = true;
|
||||
22
gaia/apps/sms/views/shared/test/unit/mock_selection_handler.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
/*exported MockSelectionHandler */
|
||||
'use strict';
|
||||
|
||||
function MockSelectionHandler(options) {
|
||||
this.selected = null;
|
||||
}
|
||||
|
||||
MockSelectionHandler.prototype = {
|
||||
get selectedCount() {
|
||||
return this.selected.size;
|
||||
},
|
||||
|
||||
get selectedList() {
|
||||
return Array.from(this.selected);
|
||||
},
|
||||
|
||||
select: function() {},
|
||||
unselect: function() {},
|
||||
toggleCheckedAll: function() {},
|
||||
cleanForm: function() {},
|
||||
allSelected: function() {}
|
||||
};
|
||||
32
gaia/apps/sms/views/shared/test/unit/mock_settings.js
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockSettings = {
|
||||
SERVICE_ID_KEYS: {
|
||||
mmsServiceId: 'ril.mms.defaultServiceId',
|
||||
smsServiceId: 'ril.sms.defaultServiceId'
|
||||
},
|
||||
mmsSizeLimitation: 295 * 1024,
|
||||
maxConcatenatedMessages: 10,
|
||||
mmsServiceId: null,
|
||||
smsServiceId: null,
|
||||
supportEmailRecipient: false,
|
||||
init: () => {},
|
||||
switchMmsSimHandler: function() {},
|
||||
isDualSimDevice: function() { return false; },
|
||||
hasSeveralSim: function() { return false; },
|
||||
getServiceIdByIccId: function() { return null; },
|
||||
setReadAheadThreadRetrieval: function() {},
|
||||
|
||||
mSetup: function() {
|
||||
MockSettings.mmsSizeLimitation = 295 * 1024;
|
||||
MockSettings.maxConcatenatedMessages = 10;
|
||||
MockSettings.mmsServiceId = null;
|
||||
MockSettings.smsServiceId = null;
|
||||
MockSettings.supportEmailRecipient = false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.MockSettings = MockSettings;
|
||||
|
||||
}(this));
|
||||
18
gaia/apps/sms/views/shared/test/unit/mock_silent_sms.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
/* global Promise */
|
||||
/* exported MockSilentSms */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockSilentSms = {
|
||||
mSilentSmsNumber: 9090,
|
||||
checkSilentModeFor: function (smsNumber) {
|
||||
return Promise.resolve(smsNumber === this.mSilentSmsNumber);
|
||||
},
|
||||
init: () => {},
|
||||
mSetup: function() {
|
||||
this.mSilentSmsNumber = 9090;
|
||||
},
|
||||
mTeardown: function() {
|
||||
this.mSilentSmsNumber = 9090;
|
||||
}
|
||||
};
|
||||