Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View 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 })
);
});
}
};

View 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));

View file

@ -0,0 +1,3 @@
;;;(function(exports){'use strict';const APPLICATION_READY_CLASS_NAME='js-app-ready';const INSTANCE_ID=Date.now();var app={isReady:function app_isReady(){return document.body.classList.contains(APPLICATION_READY_CLASS_NAME);},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);if(navigator.mozAudioChannelManager){navigator.mozAudioChannelManager.volumeControlChannel='notification';}},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);

View file

@ -0,0 +1 @@
;;(function(){'use strict';Startup.init();})(self);

View 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);

View file

@ -0,0 +1,15 @@
;;;(function(exports){'use strict';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;}};function isMatch(contact,criteria,filterFn){var found={};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;}}}}
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){return Promise.resolve([]);}
terms=filterValue.split(rspaces);filter.filterValue=terms.length===1?terms[0]:terms.reduce(function(initial,term){lower.push(term.toLowerCase());return term.length>initial.length?term:initial;},'');if(filter.filterValue.length<3){filter.filterLimit=10;}
lower.splice(lower.indexOf(filter.filterValue.toLowerCase()),1);lower.push.apply(lower,terms);return navigator.mozContacts.find(filter).then((contacts)=>{var results;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++){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();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));

View 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));

View 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));

View 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);

View file

@ -0,0 +1,8 @@
;;;(function(exports){'use strict';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);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);

View 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);

View file

@ -0,0 +1,58 @@
;;;(function(window){'use strict';var NavigationFactory=window.NavigationFactory=function(window){const debug=[()=>{},(arg1,...args)=>console.log('[Navigation] '+arg1,...args),(arg1,...args)=>console.log('[Navigation] (%s) '+arg1,Date.now(),...args),][0];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'})]);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;function setLocation(url){debug('Setting location',url);window.location.assign(url);}
function isUsingOldArchitecture(){debug('isUsingOldArchitecture()');var pathname=window.location.pathname;var result=(pathname==='/'||pathname==='/index.html');debug('isUsingOldArchitecture() =>',result);return result;}
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);}
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;}
function findViewFromName(name){debug('findViewFromName(%s)',name);var view=VIEWS.find((view)=>view.name===name)||null;debug('findViewFromName() => %s',view&&view.name);return 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;}
var readyDefer=Utils.Promise.defer();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});}
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){return waitForReady().then(()=>startNavigation(opts));}}
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();}
function setFutureState(){debug('setFutureState()');currentState=navigationTransition.states.to;}
function endNavigation(){debug('endNavigation()');if(navigationTransition){navigationTransition.defer.resolve();navigationTransition=null;}}
function onNavigationError(e=new Error('RejectedPromise')){console.error('Navigation error',e);endNavigation();return Promise.reject(e);}
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');}}
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){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]){resultPromise=resultPromise.then(()=>viewObject[stepName](navigationTransition.transitionArgs));}
return resultPromise.then(()=>{navigationTransition.stepsRun.push(stepName);debug(`Finished executeNavigationStep(${stepName})`);});}
var previousAnimationDefer;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);}
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');});}
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);}
function attachHistoryListener(){window.addEventListener('hashchange',onHashChange);}
function detachHistoryListener(){window.removeEventListener('hashchange',onHashChange);}
var Navigation={back(){debug('back()');return startNavigation({partial:true}).then(()=>executeNavigationStep('beforeLeave')).then(()=>window.history.back()).then(()=>navigationTransition.defer.promise,onNavigationError);},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));}
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);},init(){debug('init()');attachHistoryListener();if(navigator.mozHasPendingMessage('notification')){this[priv.pendingInit]=true;return Promise.reject();}
this[priv.pendingInit]=false;return startNavigationFromCurrentLocation().then(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];},cleanup(){detachHistoryListener();},isDefaultPanel(){return currentState?!currentState.panel.partOf:!findViewNameFromHash(window.location.hash);},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;},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);

View 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));

View 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));

View file

@ -0,0 +1,9 @@
;;;'use strict';var Settings={SERVICE_ID_KEYS:{mmsServiceId:'ril.mms.defaultServiceId',smsServiceId:'ril.sms.defaultServiceId'},READ_AHEAD_THREADS_KEY:'ril.sms.maxReadAheadEntries',MMS_SIZE_OVERHEAD:5*1024,_serviceIds:null,supportEmailRecipient:true,maxConcatenatedMessages:10,mmsSizeLimitation:295*1024,mmsServiceId:null,smsServiceId:null,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;}
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);}
for(var i=0,l=conns.length;i<l;i++){this._serviceIds.push(conns[i].iccId);}}
for(var key in keyHandlerSet){setHandlerMap(key);}},initSmsMaxConcatenatedMsg:function initSmsMaxConcatenatedMsg(num){if(num&&!isNaN(num)){this.maxConcatenatedMessages=num;}},initMmsSizeLimitation:function initMmsSizeLimitation(size){if(size&&!isNaN(size)){this.mmsSizeLimitation=size-this.MMS_SIZE_OVERHEAD;}},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){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'){resolve();}else{conn.addEventListener('datachange',function onDataChange(){if(conn.data.state==='registered'){conn.removeEventListener('datachange',onDataChange);resolve();}});this.setMmsSimServiceId(targetId);}}else{reject('Invalid connection');}}.bind(this));},isDualSimDevice:function isDualSimDevice(){return this._serviceIds&&this._serviceIds.length>1;},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);}};

View file

@ -0,0 +1,3 @@
;'use strict';(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);

View 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);

View 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
};
}());

View file

@ -0,0 +1,29 @@
;(function(){'use strict';var unsafeFilenamePattern=/[^a-zA-Z0-9_#.()?&%-]/g;var encoder=new TextEncoder('UTF-8');function SMIL_generateSlides(data,slide,slideIndex){const DURATION=5000;var id;var blobType;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"';if(tagName==='ref'){region='';}
name=slide.name.substr(slide.name.lastIndexOf('/')+1);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){id='text_'+slideIndex+'.txt';text='<text src="'+id+'" region="Text"/>';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;}
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;var FILENAME_LIMIT=40;function SMIL_uniqueLocationMatches(attachment){return attachment.location===result;}
var index=location.lastIndexOf('.');if(index===-1){name=location;extension='';}else{extension=location.slice(index);name=location.slice(0,index);}
if(name.length+extension.length>FILENAME_LIMIT){name=name.slice(0,FILENAME_LIMIT-extension.length);}
result=name+extension;var duplicateIndex=2;while(data.attachments.some(SMIL_uniqueLocationMatches)){var truncIndex=0;result=name+'_'+duplicateIndex++ +extension;while(result.length>FILENAME_LIMIT){duplicateIndex=2;name=name.slice(0,--truncIndex);result=name+extension;}}
return result;}
window.SMIL={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){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('');};textReader.readAsText(blob,'UTF-8');return defer.promise;}
function findAttachment(name){var index=0;var length=attachments.length;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;}
function SMIL_parseWithoutSMIL(attachment,idx){var blob=attachment.content;if(!blob){return Promise.resolve();}
var result;var type=Utils.typeFromMimeType(blob.type);if(type==='text'&&blob.type==='text/plain'){result=readTextBlob(blob).then((text)=>{slides[idx]={text:text};});}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){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){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){var slide=slides[slides.length-1];if(!slide||typeof slide.text!=='undefined'){slide={};slides.push(slide);}
slide.text='';promises.push(readTextBlob(attachment.content).then((text)=>slide.text=text));}else{attachmentsNotFound=true;}}
return Promise.all(promises);}
var result;if(smil){doc=(new DOMParser()).parseFromString(smil,'application/xml');parTags=doc.documentElement.getElementsByTagName('par');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));}}
if(!smil||attachmentsNotFound||!slides.length||smilMismatched){slides=Array(attachments.length);result=Promise.all(attachments.map(SMIL_parseWithoutSMIL));}
return(result||Promise.resolve()).then(()=>slides);},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>';var data=slides.reduce(SMIL_generateSlides,{attachments:[],parts:[]});data.smil='<smil>'+HEADER+'<body>'+
data.parts.join('')+'</body></smil>';delete data.parts;return data;}};})();

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){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();SilentSms.init();if(navigator.mozHasPendingMessage('activity')){ActivityHandler.init();}else{SystemMessageHandler.init();}
TimeHeaders.init();ConversationView.init();MessagingClient.init(App.instanceId);MozMobileConnectionsClient.init(App.instanceId);Information.initDefaultViews();Navigation.setReady();window.performance.mark('contentInteractive');Settings.init();window.performance.mark('objectsInitEnd');});return lazyLoadPromise;},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(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();}
window.performance.mark('navigationInteractive');}.bind(this);window.addEventListener('DOMContentLoaded',loaded);}};Startup.init();})(window);

View 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);

View file

@ -0,0 +1,2 @@
;'use strict';function TaskRunner(){this._currentTask=Promise.resolve();}
TaskRunner.prototype.push=function(task){return(this._currentTask=this._currentTask.then(task,task));};

View file

@ -0,0 +1,6 @@
;;;(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);this.stopScheduler();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;}
if(element.dataset.dateOnly==='true'){Utils.setHeaderDate({time:ts,element:element,withTime:false});}else if(element.dataset.timeOnly==='true'){element.removeAttribute('data-l10n-id');element.textContent=Utils.getFormattedHour(ts);}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));

View file

@ -0,0 +1,55 @@
;;;(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(){for(var type in this.date.formatters){this.date.formatters[type].formatter=null;}},_getFormatter:function ut_getFormatter(type,withTime){if(withTime){type+='-wt';}
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){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){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);}},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;for(i=0;i<length;i++){if(contact.tel[i].value.indexOf(subscriber)!==-1){phone=contact.tel[i];break;}}
details.isContact=true;if(include.photoURL){var photo=ContactPhotoHelper.getThumbnail(contact);if(photo){details.photoURL=window.URL.createObjectURL(photo);}}
details.name=name;details.org=details.org||org;}
if(!contacts||(Array.isArray(contacts)&&contacts.length===0)){details.title='';}else if(!Array.isArray(contacts)){updateDetails(contacts);details.title=details.name||details.org;}else{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;},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};},removeNonDialables:function ut_removeNonDialables(input){return input.replace(rnondialablechars,'');},probablyMatches:function ut_probablyMatches(a,b){var service=navigator.mozPhoneNumberService;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:function ut_multiRecipientMatch(a,b){a=[].concat(a);b=[].concat(b);var blen=b.length;if(a.length!==blen){return false;}
return a.every(function(number){for(var i=0;i<blen;i++){if(Utils.probablyMatches(number,b[i])){return true;}}});},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});},getSizeForL10n:function ut_getSizeForL10n(size){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: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='';var level=0;function cleanup(){canvas.width=canvas.height=0;canvas=null;}
function ensureSizeLimit(resizedBlob){if(resizedBlob.size<limit){cleanup();setTimeout(callback.bind(null,resizedBlob));}else{resizedBlob=null;if(level<qualities.length){canvas.toBlob(ensureSizeLimit,'image/jpeg',qualities[level++]);}else{cleanup();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;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){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;}
if(!record&&!number){return;}
var telLength=(record&&record.tel)?record.tel.length:0;var tel;for(var i=0;i<telLength;i++){if(record.tel[i].value===number){tel=record.tel[i];break;}}
if(!tel){tel={type:[''],value:number,carrier:''};}
var details=Utils.getContactDetails(tel,record);var info=Utils.getDisplayObject(details.title||null,tel);return info;},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;},asyncLoadRevokeURL:function(url){setTimeout(function(){var image=new Image();image.src=url;image.onload=image.onerror=function revokePhotoURL(){window.URL.revokeObjectURL(this.src);};});},isEmailAddress:function(email){return rmail.test(email);},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);});},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);};},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);}};},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;},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:{defer:function(){var deferred={};deferred.promise=new Promise(function(resolve,reject){deferred.resolve=resolve;deferred.reject=reject;});return deferred;},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);}};}},getSimNameByIccId(iccId){var index=Settings.getServiceIdByIccId(iccId);if(index===null){return Promise.resolve('');}
return document.l10n.formatValue('sim-id-label',{id:index+1});},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(()=>{window.mozSetMessageHandler=function(type,handler){navigator.mozSetMessageHandler(type,(request)=>handler(request));};shimHostIframe.contentWindow.bootstrap(appInstanceId);});},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));

View 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');
}
};

View file

@ -0,0 +1,11 @@
;'use strict';var WBMP=(function(document){function decode(arrayBuffer,callback){var bytes=new Uint8Array(arrayBuffer);var ptr=0;function readOctet(){return bytes[ptr++]&0xff;}
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;}}}
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{if(readMultiByteInteger()!==0){return false;}
if(readOctet()!==0){return false;}
var width=readMultiByteInteger();var height=readMultiByteInteger();if(width===0||width>65535||height===0||height>65535){return false;}
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;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;}
ctx.putImageData(imageData,0,0);canvas.toBlob(callback);}catch(e){return null;}}
return{decode:decode};})(document);

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Messages Shim Host</title>
<!-- Firefox Desktop shims -->
<!-- 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>

View 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;
}

View 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;
}

View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

View 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;
}

View 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;
}

View 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;
}

View 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;
}